1use crate::model::*;
13use orbit_ir::{
14 eval, handler_key, parse_handler_key, Action, Invocation, Scope, StepIr, WorkflowIr,
15};
16use serde_json::Value;
17
18pub struct StateScope<'a> {
22 pub state: &'a ExecutionState,
23 pub local: Option<(&'a str, &'a Value)>,
24 pub own_output: Option<(&'a str, &'a Value)>,
25 pub failure: Option<Value>,
27}
28
29impl<'a> StateScope<'a> {
30 pub fn new(state: &'a ExecutionState) -> Self {
31 StateScope {
32 state,
33 local: None,
34 own_output: None,
35 failure: None,
36 }
37 }
38}
39
40impl Scope for StateScope<'_> {
41 fn input(&self) -> &Value {
42 &self.state.input
43 }
44
45 fn step_output(&self, step: &str) -> Option<&Value> {
46 if let Some((s, v)) = self.own_output {
47 if s == step {
48 return Some(v);
49 }
50 }
51 let handler_unit = match (&self.failure, &self.state.handlers) {
54 (Some(_), Some(phase)) => self.state.units.get(&handler_key(&phase.kind, step)),
55 _ => None,
56 };
57 let found = handler_unit
58 .or_else(|| self.state.units.get(step))
59 .and_then(|u| match u.status() {
60 UnitStatus::Succeeded | UnitStatus::Skipped => u.output.as_ref(),
61 _ => None,
62 });
63 static NULL: Value = Value::Null;
66 match (found, &self.failure) {
67 (None, Some(_)) => Some(&NULL),
68 (found, _) => found,
69 }
70 }
71
72 fn execution_field(&self, field: &str) -> Option<Value> {
73 Some(Value::String(match field {
74 "id" => self.state.id.clone(),
75 "workflow" => self.state.workflow.clone(),
76 "version" => self.state.version.clone(),
77 _ => return None,
78 }))
79 }
80
81 fn local(&self, name: &str) -> Option<&Value> {
82 if name == "failure" {
83 if let Some(f) = &self.failure {
84 return Some(f);
85 }
86 }
87 self.local.filter(|(n, _)| *n == name).map(|(_, v)| v)
88 }
89}
90
91pub fn failure_value(state: &ExecutionState) -> Value {
93 let (reason, code, message) = match &state.abort {
94 Some(AbortReason::Failed { error }) => {
95 ("failed", error.code.clone(), error.message.clone())
96 }
97 Some(AbortReason::Cancelled { reason }) => {
98 ("cancelled", codes::CANCELLED.to_string(), reason.clone())
99 }
100 Some(AbortReason::TimedOut) => (
101 "timed_out",
102 codes::WORKFLOW_TIMEOUT.to_string(),
103 "workflow timed out".to_string(),
104 ),
105 None => ("failed", "UNKNOWN".to_string(), String::new()),
106 };
107 serde_json::json!({ "reason": reason, "code": code, "message": message })
108}
109
110fn handler_step<'a>(def: &'a WorkflowIr, key: &str) -> Option<&'a StepIr> {
112 let (kind, id) = parse_handler_key(key)?;
113 def.handler_step(kind, id)
114}
115
116pub fn invocation<'a>(def: &'a WorkflowIr, key: &str) -> Option<&'a Invocation> {
118 if let Some(step) = handler_step(def, key) {
119 return step.invocation();
120 }
121 let k = parse_key(key);
122 let step = def.step(k.step)?;
123 if k.compensation {
124 step.compensation.as_ref()
125 } else {
126 step.invocation()
127 }
128}
129
130fn unit(state: &ExecutionState, key: &str) -> UnitState {
131 state.units.get(key).cloned().unwrap_or_default()
132}
133
134fn element<'a>(
136 state: &'a ExecutionState,
137 def: &'a WorkflowIr,
138 key: &str,
139) -> Option<(&'a str, &'a Value)> {
140 let k = parse_key(key);
141 let index = k.index?;
142 let fe = def.step(k.step)?.for_each.as_ref()?;
143 let items = state.units.get(k.step)?.input.as_ref()?.as_array()?;
144 Some((fe.variable.as_str(), items.get(index)?))
145}
146
147fn schedule(state: &ExecutionState, def: &WorkflowIr, key: &str, inv: &Invocation) -> Event {
149 let k = parse_key(key);
150 let mut scope = StateScope::new(state);
151 scope.local = element(state, def, key);
152 if parse_handler_key(key).is_some() {
153 scope.failure = Some(failure_value(state));
154 }
155 let element_unit = if k.compensation && k.index.is_some() {
157 key.strip_prefix("compensate:")
158 } else {
159 None
160 };
161 let own = element_unit
162 .and_then(|e| state.units.get(e))
163 .and_then(|u| u.output.as_ref());
164 if let Some(v) = own {
165 scope.own_output = Some((k.step, v));
166 }
167 let fail = |code: &str, msg: String| Event::StepFailed {
168 step: key.into(),
169 error: TaskError::new(code, msg, false),
170 };
171 let mut input = match eval(&inv.input, &scope) {
172 Ok(v) => v,
173 Err(e) => return fail(codes::EXPRESSION_ERROR, format!("evaluating input: {e}")),
174 };
175 if let Some(contract) = def.contracts.get(&inv.task) {
176 contract.input.apply_defaults(&mut input);
177 if let Err(e) = contract.input.validate(&input, false) {
178 return fail(
179 codes::INPUT_CONTRACT_VIOLATION,
180 format!("input for `{}` is invalid at {e}", inv.task),
181 );
182 }
183 }
184 let idempotency_key = match &inv.idempotency_key {
185 Some(expr) => match eval(expr, &scope) {
186 Ok(Value::String(s)) => s,
187 Ok(Value::Null) => format!("{}/{key}", state.id),
188 Ok(other) => other.to_string(),
189 Err(e) => {
190 return fail(
191 codes::EXPRESSION_ERROR,
192 format!("evaluating idempotency_key: {e}"),
193 )
194 }
195 },
196 None => format!("{}/{key}", state.id),
197 };
198 Event::StepScheduled {
199 step: key.into(),
200 attempt: 1,
201 input,
202 idempotency_key,
203 redelivery: false,
204 }
205}
206
207fn after_failure(
209 state: &ExecutionState,
210 key: &str,
211 u: &UnitState,
212 inv: Option<&Invocation>,
213 now: u64,
214) -> Event {
215 let error = u
216 .last_error
217 .clone()
218 .unwrap_or_else(|| TaskError::new("UNKNOWN", "attempt failed", true));
219 let aborting = state.abort.is_some() && !is_cleanup_key(key);
220 if let Some(inv) = inv {
221 let policy = &inv.retry;
222 let retryable = error.retryable && !policy.non_retryable.contains(&error.code);
223 if retryable && !aborting && u.attempt < policy.max_attempts {
224 let next = u.attempt + 1;
225 let delay = policy.delay_before(next);
226 return Event::RetryScheduled {
227 step: key.into(),
228 next_attempt: next,
229 delay_ms: delay,
230 fire_at_ms: now + delay,
231 };
232 }
233 if retryable && u.attempt >= policy.max_attempts && policy.max_attempts > 1 {
234 let mut e = error.clone();
235 e.message = format!("{} (gave up after {} attempts)", e.message, u.attempt);
236 return Event::StepFailed {
237 step: key.into(),
238 error: e,
239 };
240 }
241 }
242 Event::StepFailed {
243 step: key.into(),
244 error,
245 }
246}
247
248fn finish(state: &ExecutionState, outcome: CompensationOutcome) -> Event {
249 match &state.abort {
250 Some(AbortReason::Cancelled { .. }) => Event::WorkflowCancelled {
251 compensation: outcome,
252 },
253 Some(AbortReason::TimedOut) => Event::WorkflowTimedOut {
254 compensation: outcome,
255 },
256 Some(AbortReason::Failed { error }) => Event::WorkflowFailed {
257 error: error.clone(),
258 compensation: outcome,
259 },
260 None => Event::WorkflowFailed {
261 error: TaskError::new("UNKNOWN", "workflow aborted", false),
262 compensation: outcome,
263 },
264 }
265}
266
267pub fn decide(state: &ExecutionState, def: &WorkflowIr, now: u64) -> Vec<Event> {
269 if state.status.is_terminal() {
270 return vec![];
271 }
272 let mut out = vec![];
273
274 if state.status == WorkflowStatus::Running && state.abort.is_none() {
276 if let Some(t) = def.timeout_ms {
277 if now >= state.created_at_ms + t {
278 return vec![Event::WorkflowTimeoutReached {}];
279 }
280 }
281 }
282
283 let mut taken: Vec<u64> = vec![];
286 for (key, u) in &state.units {
287 let inv = invocation(def, key);
288 match u.status() {
289 UnitStatus::Dispatched => {
290 if u.deadline_ms.is_some_and(|d| now >= d) {
291 out.push(Event::StepAttemptFailed {
292 step: key.clone(),
293 attempt: u.attempt,
294 error: TaskError::new(
295 codes::TIMEOUT,
296 format!("attempt {} exceeded its timeout", u.attempt),
297 true,
298 ),
299 });
300 }
301 }
302 UnitStatus::Scheduled => {
303 let qt = inv.and_then(|i| i.queue_timeout_ms);
304 if let (Some(qt), Some(at)) = (qt, u.scheduled_at_ms) {
305 if !u.redelivery && now >= at + qt {
306 out.push(Event::StepAttemptFailed {
307 step: key.clone(),
308 attempt: u.attempt,
309 error: TaskError::new(
310 codes::QUEUE_TIMEOUT,
311 "no worker picked up the task before queue_timeout",
312 false,
313 ),
314 });
315 }
316 }
317 }
318 UnitStatus::Waiting => {
319 if let Some(signal) = awaited_signal(def, key) {
320 let next = state.signals.iter().find(|r| {
322 r.name == signal && r.delivered_to.is_none() && !taken.contains(&r.seq)
323 });
324 if let Some(r) = next {
325 taken.push(r.seq);
326 out.push(Event::SignalDelivered {
327 step: key.clone(),
328 signal_seq: r.seq,
329 });
330 } else if u.wake_at_ms.is_some_and(|w| now >= w) {
331 out.push(Event::StepFailed {
332 step: key.clone(),
333 error: TaskError::new(
334 codes::SIGNAL_TIMEOUT,
335 format!("no `{signal}` signal arrived before the step's timeout"),
336 false,
337 ),
338 });
339 }
340 } else if u.wake_at_ms.is_some_and(|w| now >= w) {
341 out.push(Event::TimerFired { step: key.clone() });
342 }
343 }
344 UnitStatus::RetryWait => {
345 if u.wake_at_ms.is_some_and(|w| now >= w) {
346 if state.abort.is_some() && !is_cleanup_key(key) {
347 out.push(Event::StepFailed {
348 step: key.clone(),
349 error: u.last_error.clone().unwrap_or_else(|| {
350 TaskError::new(codes::CANCELLED, "aborted", false)
351 }),
352 });
353 } else {
354 out.push(Event::StepScheduled {
355 step: key.clone(),
356 attempt: u.attempt,
357 input: u.input.clone().unwrap_or(Value::Null),
358 idempotency_key: u.idempotency_key.clone().unwrap_or_default(),
359 redelivery: false,
360 });
361 }
362 }
363 }
364 UnitStatus::AttemptFailed => out.push(after_failure(state, key, u, inv, now)),
365 UnitStatus::Uncertain => out.push(Event::StepScheduled {
366 step: key.clone(),
367 attempt: u.attempt,
368 input: u.input.clone().unwrap_or(Value::Null),
369 idempotency_key: u.idempotency_key.clone().unwrap_or_default(),
370 redelivery: true,
371 }),
372 _ => {}
373 }
374 }
375 if !out.is_empty() {
376 return out;
377 }
378
379 if let Some(phase) = &state.handlers {
380 return decide_handlers(state, def, phase, now);
381 }
382 match state.status {
383 WorkflowStatus::Running => decide_running(state, def, now),
384 WorkflowStatus::Compensating => decide_compensating(state, def),
385 _ => vec![],
386 }
387}
388
389fn awaited_signal<'a>(def: &'a WorkflowIr, key: &str) -> Option<&'a str> {
391 let k = parse_key(key);
392 if k.compensation {
393 return None;
394 }
395 match &def.step(k.step)?.action {
396 Action::Await { signal, .. } => Some(signal),
397 _ => None,
398 }
399}
400
401fn compute(state: &ExecutionState, def: &WorkflowIr, key: &str, expr: &orbit_ir::Expr) -> Event {
403 let mut scope = StateScope::new(state);
404 scope.local = element(state, def, key);
405 if parse_handler_key(key).is_some() {
406 scope.failure = Some(failure_value(state));
407 }
408 match eval(expr, &scope) {
409 Ok(output) => Event::StepSucceeded {
410 step: key.into(),
411 attempt: 1,
412 output,
413 },
414 Err(e) => Event::StepFailed {
415 step: key.into(),
416 error: TaskError::new(
417 codes::EXPRESSION_ERROR,
418 format!("evaluating `compute`: {e}"),
419 false,
420 ),
421 },
422 }
423}
424
425fn deps_done(state: &ExecutionState, step: &StepIr) -> bool {
426 step.dependencies.iter().all(|d| {
427 matches!(
428 unit(state, d).status(),
429 UnitStatus::Succeeded | UnitStatus::Skipped
430 )
431 })
432}
433
434fn element_keys(state: &ExecutionState, step: &str) -> Vec<String> {
436 let n = state
437 .units
438 .get(step)
439 .and_then(|u| u.input.as_ref())
440 .and_then(Value::as_array)
441 .map(Vec::len)
442 .unwrap_or(0);
443 (0..n).map(|i| element_key(step, i)).collect()
444}
445
446fn in_flight(status: UnitStatus) -> bool {
447 matches!(
448 status,
449 UnitStatus::Scheduled
450 | UnitStatus::Dispatched
451 | UnitStatus::AttemptFailed
452 | UnitStatus::Uncertain
453 | UnitStatus::RetryWait
454 )
455}
456
457fn advance_fan_out(state: &ExecutionState, def: &WorkflowIr, step: &StepIr, out: &mut Vec<Event>) {
460 let keys = element_keys(state, &step.id);
461 let units: Vec<UnitState> = keys.iter().map(|k| unit(state, k)).collect();
462 if let Some(failed) = units.iter().find(|u| u.status() == UnitStatus::Failed) {
463 out.push(Event::StepFailed {
464 step: step.id.clone(),
465 error: failed
466 .error
467 .clone()
468 .unwrap_or_else(|| TaskError::new("UNKNOWN", "element failed", false)),
469 });
470 return;
471 }
472 if units.iter().all(|u| u.status() == UnitStatus::Succeeded) {
473 let output = Value::Array(
474 units
475 .iter()
476 .map(|u| u.output.clone().unwrap_or(Value::Null))
477 .collect(),
478 );
479 out.push(Event::StepSucceeded {
480 step: step.id.clone(),
481 attempt: 1,
482 output,
483 });
484 return;
485 }
486 if let Action::Compute { expr } = &step.action {
487 for (key, u) in keys.iter().zip(&units) {
488 if u.status() == UnitStatus::Pending {
489 out.push(compute(state, def, key, expr));
490 }
491 }
492 return;
493 }
494 let Some(inv) = step.invocation() else { return };
495 let limit = step
496 .for_each
497 .as_ref()
498 .and_then(|f| f.max_parallel)
499 .map(|n| n as usize)
500 .unwrap_or(usize::MAX);
501 let mut running = units.iter().filter(|u| in_flight(u.status())).count();
502 for (key, u) in keys.iter().zip(&units) {
503 if running >= limit {
504 break;
505 }
506 if u.status() == UnitStatus::Pending {
507 out.push(schedule(state, def, key, inv));
508 running += 1;
509 }
510 }
511}
512
513fn decide_running(state: &ExecutionState, def: &WorkflowIr, now: u64) -> Vec<Event> {
514 let mut out = vec![];
515 if state.abort.is_none() {
516 for step in &def.steps {
517 let u = unit(state, &step.id);
518 if u.status() == UnitStatus::Running {
519 advance_fan_out(state, def, step, &mut out);
520 continue;
521 }
522 if u.status() != UnitStatus::Pending || !deps_done(state, step) {
523 continue;
524 }
525 if let Some(cond) = &step.condition {
526 match eval(cond, &StateScope::new(state)) {
527 Ok(Value::Bool(true)) => {}
528 Ok(Value::Bool(false)) | Ok(Value::Null) => {
529 out.push(Event::StepSkipped {
530 step: step.id.clone(),
531 reason: "`when` condition was false".into(),
532 });
533 continue;
534 }
535 Ok(other) => {
536 out.push(Event::StepFailed {
537 step: step.id.clone(),
538 error: TaskError::new(
539 codes::EXPRESSION_ERROR,
540 format!("`when` evaluated to {other}, expected Bool"),
541 false,
542 ),
543 });
544 continue;
545 }
546 Err(e) => {
547 out.push(Event::StepFailed {
548 step: step.id.clone(),
549 error: TaskError::new(
550 codes::EXPRESSION_ERROR,
551 format!("evaluating `when`: {e}"),
552 false,
553 ),
554 });
555 continue;
556 }
557 }
558 }
559 if let Some(fe) = &step.for_each {
560 match eval(&fe.items, &StateScope::new(state)) {
561 Ok(Value::Array(items)) => out.push(Event::StepFanOut {
562 step: step.id.clone(),
563 items,
564 }),
565 Ok(Value::Null) => out.push(Event::StepFanOut {
566 step: step.id.clone(),
567 items: vec![],
568 }),
569 Ok(other) => out.push(Event::StepFailed {
570 step: step.id.clone(),
571 error: TaskError::new(
572 codes::EXPRESSION_ERROR,
573 format!("`for` expected a list, got {other}"),
574 false,
575 ),
576 }),
577 Err(e) => out.push(Event::StepFailed {
578 step: step.id.clone(),
579 error: TaskError::new(
580 codes::EXPRESSION_ERROR,
581 format!("evaluating `for`: {e}"),
582 false,
583 ),
584 }),
585 }
586 continue;
587 }
588 match &step.action {
589 Action::Sleep { duration_ms } => out.push(Event::TimerStarted {
590 step: step.id.clone(),
591 fire_at_ms: now + duration_ms,
592 }),
593 Action::Task(inv) | Action::Workflow(inv) => {
594 out.push(schedule(state, def, &step.id, inv))
595 }
596 Action::Await { signal, timeout_ms } => out.push(Event::SignalAwaitStarted {
597 step: step.id.clone(),
598 signal: signal.clone(),
599 deadline_ms: timeout_ms.map(|t| now + t),
600 }),
601 Action::Compute { expr } => out.push(compute(state, def, &step.id, expr)),
602 }
603 }
604 if !out.is_empty() {
605 return out;
606 }
607 let all_done = def
608 .steps
609 .iter()
610 .all(|s| unit(state, &s.id).status().is_terminal());
611 if all_done {
612 let output = match &def.result {
613 Some(expr) => match eval(expr, &StateScope::new(state)) {
614 Ok(v) => v,
615 Err(e) => {
616 return vec![Event::WorkflowFailed {
617 error: TaskError::new(
618 codes::EXPRESSION_ERROR,
619 format!("evaluating `return`: {e}"),
620 false,
621 ),
622 compensation: CompensationOutcome::None,
623 }]
624 }
625 },
626 None => Value::Object(Default::default()),
627 };
628 if let Err(e) = def.output.validate(&output, false) {
629 return vec![Event::WorkflowFailed {
630 error: TaskError::new(
631 codes::OUTPUT_CONTRACT_VIOLATION,
632 format!("workflow output is invalid at {e}"),
633 false,
634 ),
635 compensation: CompensationOutcome::None,
636 }];
637 }
638 return vec![Event::WorkflowCompleted { output }];
639 }
640 return out;
641 }
642
643 let mut waiting = false;
646 let withdraw =
647 |key: &str, u: &UnitState, out: &mut Vec<Event>, waiting: &mut bool| match u.status() {
648 UnitStatus::Pending | UnitStatus::Waiting | UnitStatus::RetryWait => {
649 out.push(Event::StepCancelled { step: key.into() })
650 }
651 UnitStatus::Scheduled if !u.redelivery => {
652 out.push(Event::StepCancelled { step: key.into() })
653 }
654 UnitStatus::Scheduled
655 | UnitStatus::Dispatched
656 | UnitStatus::AttemptFailed
657 | UnitStatus::Uncertain => *waiting = true,
658 _ => {}
659 };
660 for step in &def.steps {
661 let u = unit(state, &step.id);
662 if u.status() == UnitStatus::Running {
663 let mut element_waiting = false;
664 let mut failed = None;
665 for key in element_keys(state, &step.id) {
666 let eu = unit(state, &key);
667 if eu.status() == UnitStatus::Failed && failed.is_none() {
668 failed = eu.error.clone();
669 }
670 withdraw(&key, &eu, &mut out, &mut element_waiting);
671 }
672 if element_waiting {
673 waiting = true;
674 } else {
675 out.push(match failed {
677 Some(error) => Event::StepFailed {
678 step: step.id.clone(),
679 error,
680 },
681 None => Event::StepCancelled {
682 step: step.id.clone(),
683 },
684 });
685 }
686 } else {
687 withdraw(&step.id, &u, &mut out, &mut waiting);
688 }
689 }
690 if !out.is_empty() || waiting {
691 return out;
692 }
693 let mut done: Vec<(String, u64)> = vec![];
696 for s in def.steps.iter().filter(|s| s.compensation.is_some()) {
697 let keys = if s.for_each.is_some() {
698 element_keys(state, &s.id)
699 } else {
700 vec![s.id.clone()]
701 };
702 for key in keys {
703 if let Some(u) = state.units.get(&key) {
704 if u.status() == UnitStatus::Succeeded {
705 done.push((key, u.succeeded_seq.unwrap_or(0)));
706 }
707 }
708 }
709 }
710 done.sort_by_key(|d| std::cmp::Reverse(d.1));
711 if done.is_empty() {
712 return conclude(state, def, CompensationOutcome::None);
713 }
714 let reason = match &state.abort {
715 Some(AbortReason::Failed { error }) => error.message.clone(),
716 Some(AbortReason::Cancelled { reason }) => format!("cancelled: {reason}"),
717 Some(AbortReason::TimedOut) => "workflow timed out".into(),
718 None => String::new(),
719 };
720 vec![Event::CompensationStarted {
721 reason,
722 steps: done.into_iter().map(|(s, _)| s).collect(),
723 }]
724}
725
726fn decide_compensating(state: &ExecutionState, def: &WorkflowIr) -> Vec<Event> {
727 let mut failures = vec![];
731 for step in &state.compensation_order {
732 let key = compensation_key(step);
733 let u = unit(state, &key);
734 match u.status() {
735 UnitStatus::Succeeded => continue,
736 UnitStatus::Failed | UnitStatus::Cancelled => {
737 let error = u
738 .error
739 .unwrap_or_else(|| TaskError::new("UNKNOWN", "compensation failed", false));
740 failures.push(Event::CompensationFailed {
741 step: step.clone(),
742 error,
743 });
744 continue;
745 }
746 UnitStatus::Pending => {
747 return match invocation(def, &key) {
748 Some(inv) => vec![schedule(state, def, &key, inv)],
749 None => vec![Event::StepFailed {
750 step: key,
751 error: TaskError::new("UNKNOWN", "missing compensation", false),
752 }],
753 };
754 }
755 _ => return vec![],
756 }
757 }
758 if !failures.is_empty() {
759 failures.extend(conclude(state, def, CompensationOutcome::Failed));
760 return failures;
761 }
762 let mut out = vec![Event::CompensationCompleted {}];
763 out.extend(conclude(state, def, CompensationOutcome::Completed));
764 out
765}
766
767fn handler_kind(state: &ExecutionState) -> &'static str {
769 match &state.abort {
770 Some(AbortReason::Cancelled { .. }) => "cancel",
771 Some(AbortReason::TimedOut) => "timeout",
772 _ => "failure",
773 }
774}
775
776fn conclude(state: &ExecutionState, def: &WorkflowIr, outcome: CompensationOutcome) -> Vec<Event> {
778 let kind = handler_kind(state);
779 match def.handlers.get(kind) {
780 Some(steps) if !steps.is_empty() => vec![Event::HandlersStarted {
781 kind: kind.to_string(),
782 compensation: outcome,
783 }],
784 _ => vec![finish(state, outcome)],
785 }
786}
787
788fn decide_handlers(
792 state: &ExecutionState,
793 def: &WorkflowIr,
794 phase: &HandlerPhase,
795 now: u64,
796) -> Vec<Event> {
797 let steps = def
798 .handlers
799 .get(&phase.kind)
800 .map(Vec::as_slice)
801 .unwrap_or(&[]);
802 let mut out = vec![];
803 let mut all_done = true;
804 for step in steps {
805 let key = handler_key(&phase.kind, &step.id);
806 let u = unit(state, &key);
807 if u.status().is_terminal() {
808 continue;
809 }
810 all_done = false;
811 if u.status() != UnitStatus::Pending {
812 continue;
813 }
814 let dep_status: Vec<UnitStatus> = step
815 .dependencies
816 .iter()
817 .map(|d| unit(state, &handler_key(&phase.kind, d)).status())
818 .collect();
819 if dep_status
820 .iter()
821 .any(|s| matches!(s, UnitStatus::Failed | UnitStatus::Cancelled))
822 {
823 out.push(Event::StepCancelled { step: key });
824 continue;
825 }
826 if !dep_status
827 .iter()
828 .all(|s| matches!(s, UnitStatus::Succeeded | UnitStatus::Skipped))
829 {
830 continue;
831 }
832 let mut scope = StateScope::new(state);
833 scope.failure = Some(failure_value(state));
834 if let Some(cond) = &step.condition {
835 match eval(cond, &scope) {
836 Ok(Value::Bool(true)) => {}
837 Ok(Value::Bool(false)) | Ok(Value::Null) => {
838 out.push(Event::StepSkipped {
839 step: key,
840 reason: "`when` condition was false".into(),
841 });
842 continue;
843 }
844 Ok(other) => {
845 out.push(Event::StepFailed {
846 step: key,
847 error: TaskError::new(
848 codes::EXPRESSION_ERROR,
849 format!("`when` evaluated to {other}, expected Bool"),
850 false,
851 ),
852 });
853 continue;
854 }
855 Err(e) => {
856 out.push(Event::StepFailed {
857 step: key,
858 error: TaskError::new(
859 codes::EXPRESSION_ERROR,
860 format!("evaluating `when`: {e}"),
861 false,
862 ),
863 });
864 continue;
865 }
866 }
867 }
868 match &step.action {
869 Action::Task(inv) | Action::Workflow(inv) => out.push(schedule(state, def, &key, inv)),
870 Action::Sleep { duration_ms } => out.push(Event::TimerStarted {
871 step: key,
872 fire_at_ms: now + duration_ms,
873 }),
874 Action::Compute { expr } => out.push(compute(state, def, &key, expr)),
875 Action::Await { .. } => out.push(Event::StepFailed {
876 step: key,
877 error: TaskError::new("UNKNOWN", "`await` is not allowed in a handler", false),
878 }),
879 }
880 }
881 if all_done && out.is_empty() {
882 return vec![finish(state, phase.compensation)];
883 }
884 out
885}