Skip to main content

orbit_runtime/
decide.rs

1//! The deterministic workflow decider.
2//!
3//! `decide(state, definition, now)` inspects an execution and returns the
4//! events that should happen next (schedule ready steps, fan out `for` steps,
5//! fire timers, apply retry policy, start compensation, finish the
6//! workflow...). It performs no I/O. The engine applies the events, persists
7//! them, and calls `decide` again until it returns nothing.
8//!
9//! Unit keys: `step` for a step, `step[i]` for element `i` of a `for` step,
10//! and `compensate:<unit>` for the compensation of a unit.
11
12use crate::model::*;
13use orbit_ir::{
14    eval, event_instance_key, handler_key, parse_event_key, parse_handler_key, Action, Invocation,
15    Scope, StepIr, WorkflowIr,
16};
17use serde_json::Value;
18
19/// Expression scope over an execution, optionally with a loop variable bound
20/// and with one step's output overridden (an element's own output inside its
21/// compensation).
22pub struct StateScope<'a> {
23    pub state: &'a ExecutionState,
24    pub local: Option<(&'a str, &'a Value)>,
25    pub own_output: Option<(&'a str, &'a Value)>,
26    /// `failure` inside `on` handlers: `{ reason, code, message }`.
27    pub failure: Option<Value>,
28    /// Inside an `on event` instance: its key (`on_event:<signal>@<seq>`) and
29    /// the signal payload (`event`).
30    pub event: Option<(String, Value)>,
31}
32
33impl<'a> StateScope<'a> {
34    pub fn new(state: &'a ExecutionState) -> Self {
35        StateScope {
36            state,
37            local: None,
38            own_output: None,
39            failure: None,
40            event: None,
41        }
42    }
43}
44
45impl Scope for StateScope<'_> {
46    fn input(&self) -> &Value {
47        &self.state.input
48    }
49
50    fn step_output(&self, step: &str) -> Option<&Value> {
51        if let Some((s, v)) = self.own_output {
52            if s == step {
53                return Some(v);
54            }
55        }
56        // Inside a handler, a name refers to a step of the same handler block
57        // first (handler step names never clash with workflow step names).
58        let handler_unit = match (&self.failure, &self.state.handlers, &self.event) {
59            (_, _, Some((instance, _))) => self.state.units.get(&format!("{instance}:{step}")),
60            (Some(_), Some(phase), _) => self.state.units.get(&handler_key(&phase.kind, step)),
61            _ => None,
62        };
63        let found = handler_unit
64            .or_else(|| self.state.units.get(step))
65            .and_then(|u| match u.status() {
66                UnitStatus::Succeeded | UnitStatus::Skipped => u.output.as_ref(),
67                _ => None,
68            });
69        // Handlers see steps that never ran (or did not succeed) as `null`;
70        // the compiler types them as optional there.
71        static NULL: Value = Value::Null;
72        match (found, self.failure.is_some() || self.event.is_some()) {
73            (None, true) => Some(&NULL),
74            (found, _) => found,
75        }
76    }
77
78    fn execution_field(&self, field: &str) -> Option<Value> {
79        Some(Value::String(match field {
80            "id" => self.state.id.clone(),
81            "workflow" => self.state.workflow.clone(),
82            "version" => self.state.version.clone(),
83            _ => return None,
84        }))
85    }
86
87    fn local(&self, name: &str) -> Option<&Value> {
88        if name == "failure" {
89            if let Some(f) = &self.failure {
90                return Some(f);
91            }
92        }
93        if name == "event" {
94            if let Some((_, payload)) = &self.event {
95                return Some(payload);
96            }
97        }
98        self.local.filter(|(n, _)| *n == name).map(|(_, v)| v)
99    }
100}
101
102/// The `failure` value handlers see, from the abort reason.
103pub fn failure_value(state: &ExecutionState) -> Value {
104    let (reason, code, message) = match &state.abort {
105        Some(AbortReason::Failed { error }) => {
106            ("failed", error.code.clone(), error.message.clone())
107        }
108        Some(AbortReason::Cancelled { reason }) => {
109            ("cancelled", codes::CANCELLED.to_string(), reason.clone())
110        }
111        Some(AbortReason::TimedOut) => (
112            "timed_out",
113            codes::WORKFLOW_TIMEOUT.to_string(),
114            "workflow timed out".to_string(),
115        ),
116        None => ("failed", "UNKNOWN".to_string(), String::new()),
117    };
118    serde_json::json!({ "reason": reason, "code": code, "message": message })
119}
120
121/// `(instance key, payload)` for a unit of an `on event` instance.
122fn event_scope(state: &ExecutionState, key: &str) -> Option<(String, Value)> {
123    let (instance, _, _) = parse_event_key(key)?;
124    let payload = state
125        .units
126        .get(instance)?
127        .output
128        .clone()
129        .unwrap_or(Value::Null);
130    Some((instance.to_string(), payload))
131}
132
133/// The handler step behind a handler unit key.
134fn handler_step<'a>(def: &'a WorkflowIr, key: &str) -> Option<&'a StepIr> {
135    let (kind, id) = parse_handler_key(key)?;
136    def.handler_step(kind, id)
137}
138
139/// The invocation behind a unit key.
140pub fn invocation<'a>(def: &'a WorkflowIr, key: &str) -> Option<&'a Invocation> {
141    if let Some(step) = handler_step(def, key) {
142        return step.invocation();
143    }
144    if let Some((_, signal, id)) = parse_event_key(key) {
145        return def.event_step(signal, id)?.invocation();
146    }
147    let k = parse_key(key);
148    let step = def.step(k.step)?;
149    if k.compensation {
150        step.compensation.as_ref()
151    } else {
152        step.invocation()
153    }
154}
155
156fn unit(state: &ExecutionState, key: &str) -> UnitState {
157    state.units.get(key).cloned().unwrap_or_default()
158}
159
160/// The `for` element bound to a unit, if any: (variable, value).
161fn element<'a>(
162    state: &'a ExecutionState,
163    def: &'a WorkflowIr,
164    key: &str,
165) -> Option<(&'a str, &'a Value)> {
166    let k = parse_key(key);
167    let index = k.index?;
168    let fe = def.step(k.step)?.for_each.as_ref()?;
169    let items = state.units.get(k.step)?.input.as_ref()?.as_array()?;
170    Some((fe.variable.as_str(), items.get(index)?))
171}
172
173/// Evaluate the payload and idempotency key for a unit's first attempt.
174fn schedule(state: &ExecutionState, def: &WorkflowIr, key: &str, inv: &Invocation) -> Event {
175    let k = parse_key(key);
176    let mut scope = StateScope::new(state);
177    scope.local = element(state, def, key);
178    if parse_handler_key(key).is_some() {
179        scope.failure = Some(failure_value(state));
180    }
181    scope.event = event_scope(state, key);
182    // Inside the compensation of an element, `<step>` is that element's output.
183    let element_unit = if k.compensation && k.index.is_some() {
184        key.strip_prefix("compensate:")
185    } else {
186        None
187    };
188    let own = element_unit
189        .and_then(|e| state.units.get(e))
190        .and_then(|u| u.output.as_ref());
191    if let Some(v) = own {
192        scope.own_output = Some((k.step, v));
193    }
194    let fail = |code: &str, msg: String| Event::StepFailed {
195        step: key.into(),
196        error: TaskError::new(code, msg, false),
197    };
198    let mut input = match eval(&inv.input, &scope) {
199        Ok(v) => v,
200        Err(e) => return fail(codes::EXPRESSION_ERROR, format!("evaluating input: {e}")),
201    };
202    if let Some(contract) = def.contracts.get(&inv.task) {
203        contract.input.apply_defaults(&mut input);
204        if let Err(e) = contract.input.validate(&input, false) {
205            return fail(
206                codes::INPUT_CONTRACT_VIOLATION,
207                format!("input for `{}` is invalid at {e}", inv.task),
208            );
209        }
210    }
211    let idempotency_key = match &inv.idempotency_key {
212        Some(expr) => match eval(expr, &scope) {
213            Ok(Value::String(s)) => s,
214            Ok(Value::Null) => format!("{}/{key}", state.id),
215            Ok(other) => other.to_string(),
216            Err(e) => {
217                return fail(
218                    codes::EXPRESSION_ERROR,
219                    format!("evaluating idempotency_key: {e}"),
220                )
221            }
222        },
223        None => format!("{}/{key}", state.id),
224    };
225    Event::StepScheduled {
226        step: key.into(),
227        attempt: 1,
228        input,
229        idempotency_key,
230        redelivery: false,
231    }
232}
233
234/// Retry-or-fail decision after a failed attempt.
235fn after_failure(
236    state: &ExecutionState,
237    key: &str,
238    u: &UnitState,
239    inv: Option<&Invocation>,
240    now: u64,
241) -> Event {
242    let error = u
243        .last_error
244        .clone()
245        .unwrap_or_else(|| TaskError::new("UNKNOWN", "attempt failed", true));
246    let aborting = state.abort.is_some() && !is_cleanup_key(key);
247    if let Some(inv) = inv {
248        let policy = &inv.retry;
249        let retryable = error.retryable && !policy.non_retryable.contains(&error.code);
250        if retryable && !aborting && u.attempt < policy.max_attempts {
251            let next = u.attempt + 1;
252            let delay = policy.delay_before(next);
253            return Event::RetryScheduled {
254                step: key.into(),
255                next_attempt: next,
256                delay_ms: delay,
257                fire_at_ms: now + delay,
258            };
259        }
260        if retryable && u.attempt >= policy.max_attempts && policy.max_attempts > 1 {
261            let mut e = error.clone();
262            e.message = format!("{} (gave up after {} attempts)", e.message, u.attempt);
263            return Event::StepFailed {
264                step: key.into(),
265                error: e,
266            };
267        }
268    }
269    Event::StepFailed {
270        step: key.into(),
271        error,
272    }
273}
274
275fn finish(state: &ExecutionState, outcome: CompensationOutcome) -> Event {
276    match &state.abort {
277        Some(AbortReason::Cancelled { .. }) => Event::WorkflowCancelled {
278            compensation: outcome,
279        },
280        Some(AbortReason::TimedOut) => Event::WorkflowTimedOut {
281            compensation: outcome,
282        },
283        Some(AbortReason::Failed { error }) => Event::WorkflowFailed {
284            error: error.clone(),
285            compensation: outcome,
286        },
287        None => Event::WorkflowFailed {
288            error: TaskError::new("UNKNOWN", "workflow aborted", false),
289            compensation: outcome,
290        },
291    }
292}
293
294/// Compute the next events. Returns an empty vector at a fixpoint.
295pub fn decide(state: &ExecutionState, def: &WorkflowIr, now: u64) -> Vec<Event> {
296    if state.status.is_terminal() {
297        return vec![];
298    }
299    let mut out = vec![];
300
301    // 1. Workflow deadline.
302    if state.status == WorkflowStatus::Running && state.abort.is_none() {
303        if let Some(t) = def.timeout_ms {
304            if now >= state.created_at_ms + t {
305                return vec![Event::WorkflowTimeoutReached {}];
306            }
307        }
308    }
309
310    // 2. Per-unit transitions: deadlines, timers, retries, redelivery.
311    // Signals delivered in this pass (so two waiting units never take the same one).
312    let mut taken: Vec<u64> = vec![];
313    for (key, u) in &state.units {
314        let inv = invocation(def, key);
315        match u.status() {
316            UnitStatus::Dispatched => {
317                if u.deadline_ms.is_some_and(|d| now >= d) {
318                    out.push(Event::StepAttemptFailed {
319                        step: key.clone(),
320                        attempt: u.attempt,
321                        error: TaskError::new(
322                            codes::TIMEOUT,
323                            format!("attempt {} exceeded its timeout", u.attempt),
324                            true,
325                        ),
326                    });
327                }
328            }
329            UnitStatus::Scheduled => {
330                let qt = inv.and_then(|i| i.queue_timeout_ms);
331                if let (Some(qt), Some(at)) = (qt, u.scheduled_at_ms) {
332                    if !u.redelivery && now >= at + qt {
333                        out.push(Event::StepAttemptFailed {
334                            step: key.clone(),
335                            attempt: u.attempt,
336                            error: TaskError::new(
337                                codes::QUEUE_TIMEOUT,
338                                "no worker picked up the task before queue_timeout",
339                                false,
340                            ),
341                        });
342                    }
343                }
344            }
345            UnitStatus::Waiting => {
346                if let Some(signal) = awaited_signal(def, key) {
347                    // `await`: take the oldest undelivered signal of this name.
348                    let next = state.signals.iter().find(|r| {
349                        r.name == signal && r.delivered_to.is_none() && !taken.contains(&r.seq)
350                    });
351                    if let Some(r) = next {
352                        taken.push(r.seq);
353                        out.push(Event::SignalDelivered {
354                            step: key.clone(),
355                            signal_seq: r.seq,
356                        });
357                    } else if u.wake_at_ms.is_some_and(|w| now >= w) {
358                        out.push(Event::StepFailed {
359                            step: key.clone(),
360                            error: TaskError::new(
361                                codes::SIGNAL_TIMEOUT,
362                                format!("no `{signal}` signal arrived before the step's timeout"),
363                                false,
364                            ),
365                        });
366                    }
367                } else if u.wake_at_ms.is_some_and(|w| now >= w) {
368                    out.push(Event::TimerFired { step: key.clone() });
369                }
370            }
371            UnitStatus::RetryWait => {
372                if u.wake_at_ms.is_some_and(|w| now >= w) {
373                    if state.abort.is_some() && !is_cleanup_key(key) {
374                        out.push(Event::StepFailed {
375                            step: key.clone(),
376                            error: u.last_error.clone().unwrap_or_else(|| {
377                                TaskError::new(codes::CANCELLED, "aborted", false)
378                            }),
379                        });
380                    } else {
381                        out.push(Event::StepScheduled {
382                            step: key.clone(),
383                            attempt: u.attempt,
384                            input: u.input.clone().unwrap_or(Value::Null),
385                            idempotency_key: u.idempotency_key.clone().unwrap_or_default(),
386                            redelivery: false,
387                        });
388                    }
389                }
390            }
391            UnitStatus::AttemptFailed => out.push(after_failure(state, key, u, inv, now)),
392            UnitStatus::Uncertain => out.push(Event::StepScheduled {
393                step: key.clone(),
394                attempt: u.attempt,
395                input: u.input.clone().unwrap_or(Value::Null),
396                idempotency_key: u.idempotency_key.clone().unwrap_or_default(),
397                redelivery: true,
398            }),
399            _ => {}
400        }
401    }
402    if !out.is_empty() {
403        return out;
404    }
405
406    if let Some(phase) = &state.handlers {
407        return decide_handlers(state, def, phase, now);
408    }
409    match state.status {
410        WorkflowStatus::Running => decide_running(state, def, now),
411        WorkflowStatus::Compensating => decide_compensating(state, def),
412        _ => vec![],
413    }
414}
415
416/// Started `on event` instances: `(instance key, signal)`, in start order.
417fn event_instances<'a>(state: &'a ExecutionState, def: &'a WorkflowIr) -> Vec<(String, &'a str)> {
418    state
419        .signals
420        .iter()
421        .filter_map(|r| {
422            let key = r.delivered_to.as_ref()?;
423            let signal = key.strip_prefix("on_event:")?.split_once('@')?.0;
424            def.events
425                .get_key_value(signal)
426                .map(|(k, _)| (key.clone(), k.as_str()))
427        })
428        .collect()
429}
430
431/// The signal an `await` unit waits for.
432fn awaited_signal<'a>(def: &'a WorkflowIr, key: &str) -> Option<&'a str> {
433    let k = parse_key(key);
434    if k.compensation {
435        return None;
436    }
437    match &def.step(k.step)?.action {
438        Action::Await { signal, .. } => Some(signal),
439        _ => None,
440    }
441}
442
443/// Evaluate a `compute` step (or one element of it) in the runtime.
444fn compute(state: &ExecutionState, def: &WorkflowIr, key: &str, expr: &orbit_ir::Expr) -> Event {
445    let mut scope = StateScope::new(state);
446    scope.local = element(state, def, key);
447    if parse_handler_key(key).is_some() {
448        scope.failure = Some(failure_value(state));
449    }
450    scope.event = event_scope(state, key);
451    match eval(expr, &scope) {
452        Ok(output) => Event::StepSucceeded {
453            step: key.into(),
454            attempt: 1,
455            output,
456        },
457        Err(e) => Event::StepFailed {
458            step: key.into(),
459            error: TaskError::new(
460                codes::EXPRESSION_ERROR,
461                format!("evaluating `compute`: {e}"),
462                false,
463            ),
464        },
465    }
466}
467
468fn deps_done(state: &ExecutionState, step: &StepIr) -> bool {
469    step.dependencies.iter().all(|d| {
470        matches!(
471            unit(state, d).status(),
472            UnitStatus::Succeeded | UnitStatus::Skipped
473        )
474    })
475}
476
477/// Element unit keys of a fanned-out step, in order.
478fn element_keys(state: &ExecutionState, step: &str) -> Vec<String> {
479    let n = state
480        .units
481        .get(step)
482        .and_then(|u| u.input.as_ref())
483        .and_then(Value::as_array)
484        .map(Vec::len)
485        .unwrap_or(0);
486    (0..n).map(|i| element_key(step, i)).collect()
487}
488
489fn in_flight(status: UnitStatus) -> bool {
490    matches!(
491        status,
492        UnitStatus::Scheduled
493            | UnitStatus::Dispatched
494            | UnitStatus::AttemptFailed
495            | UnitStatus::Uncertain
496            | UnitStatus::RetryWait
497    )
498}
499
500/// Progress a running `for` step: schedule elements within `max_parallel`,
501/// and resolve the step once every element is done (or one failed).
502fn advance_fan_out(state: &ExecutionState, def: &WorkflowIr, step: &StepIr, out: &mut Vec<Event>) {
503    let keys = element_keys(state, &step.id);
504    let units: Vec<UnitState> = keys.iter().map(|k| unit(state, k)).collect();
505    if let Some(failed) = units.iter().find(|u| u.status() == UnitStatus::Failed) {
506        out.push(Event::StepFailed {
507            step: step.id.clone(),
508            error: failed
509                .error
510                .clone()
511                .unwrap_or_else(|| TaskError::new("UNKNOWN", "element failed", false)),
512        });
513        return;
514    }
515    if units.iter().all(|u| u.status() == UnitStatus::Succeeded) {
516        let output = Value::Array(
517            units
518                .iter()
519                .map(|u| u.output.clone().unwrap_or(Value::Null))
520                .collect(),
521        );
522        out.push(Event::StepSucceeded {
523            step: step.id.clone(),
524            attempt: 1,
525            output,
526        });
527        return;
528    }
529    if let Action::Compute { expr } = &step.action {
530        for (key, u) in keys.iter().zip(&units) {
531            if u.status() == UnitStatus::Pending {
532                out.push(compute(state, def, key, expr));
533            }
534        }
535        return;
536    }
537    let Some(inv) = step.invocation() else { return };
538    let limit = step
539        .for_each
540        .as_ref()
541        .and_then(|f| f.max_parallel)
542        .map(|n| n as usize)
543        .unwrap_or(usize::MAX);
544    let mut running = units.iter().filter(|u| in_flight(u.status())).count();
545    for (key, u) in keys.iter().zip(&units) {
546        if running >= limit {
547            break;
548        }
549        if u.status() == UnitStatus::Pending {
550            out.push(schedule(state, def, key, inv));
551            running += 1;
552        }
553    }
554}
555
556fn decide_running(state: &ExecutionState, def: &WorkflowIr, now: u64) -> Vec<Event> {
557    let mut out = vec![];
558    if state.abort.is_none() {
559        for step in &def.steps {
560            let u = unit(state, &step.id);
561            if u.status() == UnitStatus::Running {
562                advance_fan_out(state, def, step, &mut out);
563                continue;
564            }
565            if u.status() != UnitStatus::Pending || !deps_done(state, step) {
566                continue;
567            }
568            if let Some(cond) = &step.condition {
569                match eval(cond, &StateScope::new(state)) {
570                    Ok(Value::Bool(true)) => {}
571                    Ok(Value::Bool(false)) | Ok(Value::Null) => {
572                        out.push(Event::StepSkipped {
573                            step: step.id.clone(),
574                            reason: "`when` condition was false".into(),
575                        });
576                        continue;
577                    }
578                    Ok(other) => {
579                        out.push(Event::StepFailed {
580                            step: step.id.clone(),
581                            error: TaskError::new(
582                                codes::EXPRESSION_ERROR,
583                                format!("`when` evaluated to {other}, expected Bool"),
584                                false,
585                            ),
586                        });
587                        continue;
588                    }
589                    Err(e) => {
590                        out.push(Event::StepFailed {
591                            step: step.id.clone(),
592                            error: TaskError::new(
593                                codes::EXPRESSION_ERROR,
594                                format!("evaluating `when`: {e}"),
595                                false,
596                            ),
597                        });
598                        continue;
599                    }
600                }
601            }
602            if let Some(fe) = &step.for_each {
603                match eval(&fe.items, &StateScope::new(state)) {
604                    Ok(Value::Array(items)) => out.push(Event::StepFanOut {
605                        step: step.id.clone(),
606                        items,
607                    }),
608                    Ok(Value::Null) => out.push(Event::StepFanOut {
609                        step: step.id.clone(),
610                        items: vec![],
611                    }),
612                    Ok(other) => out.push(Event::StepFailed {
613                        step: step.id.clone(),
614                        error: TaskError::new(
615                            codes::EXPRESSION_ERROR,
616                            format!("`for` expected a list, got {other}"),
617                            false,
618                        ),
619                    }),
620                    Err(e) => out.push(Event::StepFailed {
621                        step: step.id.clone(),
622                        error: TaskError::new(
623                            codes::EXPRESSION_ERROR,
624                            format!("evaluating `for`: {e}"),
625                            false,
626                        ),
627                    }),
628                }
629                continue;
630            }
631            match &step.action {
632                Action::Sleep { duration_ms } => out.push(Event::TimerStarted {
633                    step: step.id.clone(),
634                    fire_at_ms: now + duration_ms,
635                }),
636                Action::Task(inv) | Action::Workflow(inv) => {
637                    out.push(schedule(state, def, &step.id, inv))
638                }
639                Action::Await { signal, timeout_ms } => out.push(Event::SignalAwaitStarted {
640                    step: step.id.clone(),
641                    signal: signal.clone(),
642                    deadline_ms: timeout_ms.map(|t| now + t),
643                }),
644                Action::Compute { expr } => out.push(compute(state, def, &step.id, expr)),
645            }
646        }
647        // `on event`: start an instance for every new signal of a handled name,
648        // then run the steps of every instance.
649        for r in &state.signals {
650            if r.delivered_to.is_none() && def.events.contains_key(&r.name) {
651                out.push(Event::SignalDelivered {
652                    step: event_instance_key(&r.name, r.seq),
653                    signal_seq: r.seq,
654                });
655            }
656        }
657        let mut events_done = true;
658        for (instance, signal) in event_instances(state, def) {
659            let steps = &def.events[signal];
660            for step in steps {
661                let key = format!("{instance}:{}", step.id);
662                let u = unit(state, &key);
663                if u.status().is_terminal() {
664                    continue;
665                }
666                events_done = false;
667                if u.status() != UnitStatus::Pending {
668                    continue;
669                }
670                let deps: Vec<UnitStatus> = step
671                    .dependencies
672                    .iter()
673                    .map(|d| unit(state, &format!("{instance}:{d}")).status())
674                    .collect();
675                if !deps
676                    .iter()
677                    .all(|s| matches!(s, UnitStatus::Succeeded | UnitStatus::Skipped))
678                {
679                    continue;
680                }
681                let mut scope = StateScope::new(state);
682                scope.event = event_scope(state, &key);
683                if let Some(cond) = &step.condition {
684                    match eval(cond, &scope) {
685                        Ok(Value::Bool(true)) => {}
686                        Ok(Value::Bool(false)) | Ok(Value::Null) => {
687                            out.push(Event::StepSkipped {
688                                step: key,
689                                reason: "`when` condition was false".into(),
690                            });
691                            continue;
692                        }
693                        Ok(other) => {
694                            out.push(Event::StepFailed {
695                                step: key,
696                                error: TaskError::new(
697                                    codes::EXPRESSION_ERROR,
698                                    format!("`when` evaluated to {other}, expected Bool"),
699                                    false,
700                                ),
701                            });
702                            continue;
703                        }
704                        Err(e) => {
705                            out.push(Event::StepFailed {
706                                step: key,
707                                error: TaskError::new(
708                                    codes::EXPRESSION_ERROR,
709                                    format!("evaluating `when`: {e}"),
710                                    false,
711                                ),
712                            });
713                            continue;
714                        }
715                    }
716                }
717                match &step.action {
718                    Action::Task(inv) | Action::Workflow(inv) => {
719                        out.push(schedule(state, def, &key, inv))
720                    }
721                    Action::Sleep { duration_ms } => out.push(Event::TimerStarted {
722                        step: key,
723                        fire_at_ms: now + duration_ms,
724                    }),
725                    Action::Compute { expr } => out.push(compute(state, def, &key, expr)),
726                    Action::Await { .. } => out.push(Event::StepFailed {
727                        step: key,
728                        error: TaskError::new(
729                            "UNKNOWN",
730                            "`await` is not allowed in an `on event` step",
731                            false,
732                        ),
733                    }),
734                }
735            }
736        }
737        if !out.is_empty() {
738            return out;
739        }
740        let all_done = events_done
741            && def
742                .steps
743                .iter()
744                .all(|s| unit(state, &s.id).status().is_terminal());
745        if all_done {
746            let output = match &def.result {
747                Some(expr) => match eval(expr, &StateScope::new(state)) {
748                    Ok(v) => v,
749                    Err(e) => {
750                        return vec![Event::WorkflowFailed {
751                            error: TaskError::new(
752                                codes::EXPRESSION_ERROR,
753                                format!("evaluating `return`: {e}"),
754                                false,
755                            ),
756                            compensation: CompensationOutcome::None,
757                        }]
758                    }
759                },
760                None => Value::Object(Default::default()),
761            };
762            if let Err(e) = def.output.validate(&output, false) {
763                return vec![Event::WorkflowFailed {
764                    error: TaskError::new(
765                        codes::OUTPUT_CONTRACT_VIOLATION,
766                        format!("workflow output is invalid at {e}"),
767                        false,
768                    ),
769                    compensation: CompensationOutcome::None,
770                }];
771            }
772            return vec![Event::WorkflowCompleted { output }];
773        }
774        return out;
775    }
776
777    // Aborting: withdraw work that has not started, wait for in-flight work,
778    // then compensate completed units in reverse completion order.
779    let mut waiting = false;
780    let withdraw =
781        |key: &str, u: &UnitState, out: &mut Vec<Event>, waiting: &mut bool| match u.status() {
782            UnitStatus::Pending | UnitStatus::Waiting | UnitStatus::RetryWait => {
783                out.push(Event::StepCancelled { step: key.into() })
784            }
785            UnitStatus::Scheduled if !u.redelivery => {
786                out.push(Event::StepCancelled { step: key.into() })
787            }
788            UnitStatus::Scheduled
789            | UnitStatus::Dispatched
790            | UnitStatus::AttemptFailed
791            | UnitStatus::Uncertain => *waiting = true,
792            _ => {}
793        };
794    for step in &def.steps {
795        let u = unit(state, &step.id);
796        if u.status() == UnitStatus::Running {
797            let mut element_waiting = false;
798            let mut failed = None;
799            for key in element_keys(state, &step.id) {
800                let eu = unit(state, &key);
801                if eu.status() == UnitStatus::Failed && failed.is_none() {
802                    failed = eu.error.clone();
803                }
804                withdraw(&key, &eu, &mut out, &mut element_waiting);
805            }
806            if element_waiting {
807                waiting = true;
808            } else {
809                // Every element is resolved: close the step itself.
810                out.push(match failed {
811                    Some(error) => Event::StepFailed {
812                        step: step.id.clone(),
813                        error,
814                    },
815                    None => Event::StepCancelled {
816                        step: step.id.clone(),
817                    },
818                });
819            }
820        } else {
821            withdraw(&step.id, &u, &mut out, &mut waiting);
822        }
823    }
824    for (instance, signal) in event_instances(state, def) {
825        for step in &def.events[signal] {
826            let key = format!("{instance}:{}", step.id);
827            let u = unit(state, &key);
828            withdraw(&key, &u, &mut out, &mut waiting);
829        }
830    }
831    if !out.is_empty() || waiting {
832        return out;
833    }
834    // Units that succeeded and have a compensation: whole steps, or each
835    // succeeded element of a `for` step.
836    let mut done: Vec<(String, u64)> = vec![];
837    for s in def.steps.iter().filter(|s| s.compensation.is_some()) {
838        let keys = if s.for_each.is_some() {
839            element_keys(state, &s.id)
840        } else {
841            vec![s.id.clone()]
842        };
843        for key in keys {
844            if let Some(u) = state.units.get(&key) {
845                if u.status() == UnitStatus::Succeeded {
846                    done.push((key, u.succeeded_seq.unwrap_or(0)));
847                }
848            }
849        }
850    }
851    done.sort_by_key(|d| std::cmp::Reverse(d.1));
852    if done.is_empty() {
853        return conclude(state, def, CompensationOutcome::None);
854    }
855    let reason = match &state.abort {
856        Some(AbortReason::Failed { error }) => error.message.clone(),
857        Some(AbortReason::Cancelled { reason }) => format!("cancelled: {reason}"),
858        Some(AbortReason::TimedOut) => "workflow timed out".into(),
859        None => String::new(),
860    };
861    vec![Event::CompensationStarted {
862        reason,
863        steps: done.into_iter().map(|(s, _)| s).collect(),
864    }]
865}
866
867fn decide_compensating(state: &ExecutionState, def: &WorkflowIr) -> Vec<Event> {
868    // Compensations run one at a time, newest completed unit first (Saga). A
869    // compensation that fails does not stop the others: every remaining effect
870    // is still undone, and each failure is reported at the end.
871    let mut failures = vec![];
872    for step in &state.compensation_order {
873        let key = compensation_key(step);
874        let u = unit(state, &key);
875        match u.status() {
876            UnitStatus::Succeeded => continue,
877            UnitStatus::Failed | UnitStatus::Cancelled => {
878                let error = u
879                    .error
880                    .unwrap_or_else(|| TaskError::new("UNKNOWN", "compensation failed", false));
881                failures.push(Event::CompensationFailed {
882                    step: step.clone(),
883                    error,
884                });
885                continue;
886            }
887            UnitStatus::Pending => {
888                return match invocation(def, &key) {
889                    Some(inv) => vec![schedule(state, def, &key, inv)],
890                    None => vec![Event::StepFailed {
891                        step: key,
892                        error: TaskError::new("UNKNOWN", "missing compensation", false),
893                    }],
894                };
895            }
896            _ => return vec![],
897        }
898    }
899    if !failures.is_empty() {
900        failures.extend(conclude(state, def, CompensationOutcome::Failed));
901        return failures;
902    }
903    let mut out = vec![Event::CompensationCompleted {}];
904    out.extend(conclude(state, def, CompensationOutcome::Completed));
905    out
906}
907
908/// The handler kind for the abort reason.
909fn handler_kind(state: &ExecutionState) -> &'static str {
910    match &state.abort {
911        Some(AbortReason::Cancelled { .. }) => "cancel",
912        Some(AbortReason::TimedOut) => "timeout",
913        _ => "failure",
914    }
915}
916
917/// After compensation: start the matching `on` handler, or finish.
918fn conclude(state: &ExecutionState, def: &WorkflowIr, outcome: CompensationOutcome) -> Vec<Event> {
919    let kind = handler_kind(state);
920    match def.handlers.get(kind) {
921        Some(steps) if !steps.is_empty() => vec![Event::HandlersStarted {
922            kind: kind.to_string(),
923            compensation: outcome,
924        }],
925        _ => vec![finish(state, outcome)],
926    }
927}
928
929/// Run the handler steps of the current phase; finish when they are all done.
930/// A failed handler step is recorded but does not change the outcome; steps
931/// that depend on it are cancelled.
932fn decide_handlers(
933    state: &ExecutionState,
934    def: &WorkflowIr,
935    phase: &HandlerPhase,
936    now: u64,
937) -> Vec<Event> {
938    let steps = def
939        .handlers
940        .get(&phase.kind)
941        .map(Vec::as_slice)
942        .unwrap_or(&[]);
943    let mut out = vec![];
944    let mut all_done = true;
945    for step in steps {
946        let key = handler_key(&phase.kind, &step.id);
947        let u = unit(state, &key);
948        if u.status().is_terminal() {
949            continue;
950        }
951        all_done = false;
952        if u.status() != UnitStatus::Pending {
953            continue;
954        }
955        let dep_status: Vec<UnitStatus> = step
956            .dependencies
957            .iter()
958            .map(|d| unit(state, &handler_key(&phase.kind, d)).status())
959            .collect();
960        if dep_status
961            .iter()
962            .any(|s| matches!(s, UnitStatus::Failed | UnitStatus::Cancelled))
963        {
964            out.push(Event::StepCancelled { step: key });
965            continue;
966        }
967        if !dep_status
968            .iter()
969            .all(|s| matches!(s, UnitStatus::Succeeded | UnitStatus::Skipped))
970        {
971            continue;
972        }
973        let mut scope = StateScope::new(state);
974        scope.failure = Some(failure_value(state));
975        if let Some(cond) = &step.condition {
976            match eval(cond, &scope) {
977                Ok(Value::Bool(true)) => {}
978                Ok(Value::Bool(false)) | Ok(Value::Null) => {
979                    out.push(Event::StepSkipped {
980                        step: key,
981                        reason: "`when` condition was false".into(),
982                    });
983                    continue;
984                }
985                Ok(other) => {
986                    out.push(Event::StepFailed {
987                        step: key,
988                        error: TaskError::new(
989                            codes::EXPRESSION_ERROR,
990                            format!("`when` evaluated to {other}, expected Bool"),
991                            false,
992                        ),
993                    });
994                    continue;
995                }
996                Err(e) => {
997                    out.push(Event::StepFailed {
998                        step: key,
999                        error: TaskError::new(
1000                            codes::EXPRESSION_ERROR,
1001                            format!("evaluating `when`: {e}"),
1002                            false,
1003                        ),
1004                    });
1005                    continue;
1006                }
1007            }
1008        }
1009        match &step.action {
1010            Action::Task(inv) | Action::Workflow(inv) => out.push(schedule(state, def, &key, inv)),
1011            Action::Sleep { duration_ms } => out.push(Event::TimerStarted {
1012                step: key,
1013                fire_at_ms: now + duration_ms,
1014            }),
1015            Action::Compute { expr } => out.push(compute(state, def, &key, expr)),
1016            Action::Await { .. } => out.push(Event::StepFailed {
1017                step: key,
1018                error: TaskError::new("UNKNOWN", "`await` is not allowed in a handler", false),
1019            }),
1020        }
1021    }
1022    if all_done && out.is_empty() {
1023        return vec![finish(state, phase.compensation)];
1024    }
1025    out
1026}