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) => default_idempotency_key(state, key),
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 => default_idempotency_key(state, key),
224    };
225    Event::StepScheduled {
226        step: key.into(),
227        attempt: 1,
228        input,
229        idempotency_key,
230        redelivery: false,
231    }
232}
233
234/// `<execution>/<unit>` for run 1, `<execution>/run<N>/<unit>` for later runs
235/// (so a worker never mistakes a later run's step for an earlier one's).
236fn default_idempotency_key(state: &ExecutionState, key: &str) -> String {
237    if state.run > 1 {
238        format!("{}/run{}/{key}", state.id, state.run)
239    } else {
240        format!("{}/{key}", state.id)
241    }
242}
243
244/// Retry-or-fail decision after a failed attempt.
245fn after_failure(
246    state: &ExecutionState,
247    key: &str,
248    u: &UnitState,
249    inv: Option<&Invocation>,
250    now: u64,
251) -> Event {
252    let error = u
253        .last_error
254        .clone()
255        .unwrap_or_else(|| TaskError::new("UNKNOWN", "attempt failed", true));
256    let aborting = state.abort.is_some() && !is_cleanup_key(key);
257    if let Some(inv) = inv {
258        let policy = &inv.retry;
259        let retryable = error.retryable && !policy.non_retryable.contains(&error.code);
260        if retryable && !aborting && u.attempt < policy.max_attempts {
261            let next = u.attempt + 1;
262            let delay = policy.delay_before(next);
263            return Event::RetryScheduled {
264                step: key.into(),
265                next_attempt: next,
266                delay_ms: delay,
267                fire_at_ms: now + delay,
268            };
269        }
270        if retryable && u.attempt >= policy.max_attempts && policy.max_attempts > 1 {
271            let mut e = error.clone();
272            e.message = format!("{} (gave up after {} attempts)", e.message, u.attempt);
273            return Event::StepFailed {
274                step: key.into(),
275                error: e,
276            };
277        }
278    }
279    Event::StepFailed {
280        step: key.into(),
281        error,
282    }
283}
284
285fn finish(state: &ExecutionState, outcome: CompensationOutcome) -> Event {
286    match &state.abort {
287        Some(AbortReason::Cancelled { .. }) => Event::WorkflowCancelled {
288            compensation: outcome,
289        },
290        Some(AbortReason::TimedOut) => Event::WorkflowTimedOut {
291            compensation: outcome,
292        },
293        Some(AbortReason::Failed { error }) => Event::WorkflowFailed {
294            error: error.clone(),
295            compensation: outcome,
296        },
297        None => Event::WorkflowFailed {
298            error: TaskError::new("UNKNOWN", "workflow aborted", false),
299            compensation: outcome,
300        },
301    }
302}
303
304/// Compute the next events. Returns an empty vector at a fixpoint.
305pub fn decide(state: &ExecutionState, def: &WorkflowIr, now: u64) -> Vec<Event> {
306    if state.status.is_terminal() {
307        return vec![];
308    }
309    let mut out = vec![];
310
311    // 1. Workflow deadline.
312    if state.status == WorkflowStatus::Running && state.abort.is_none() {
313        if let Some(t) = def.timeout_ms {
314            if now >= state.created_at_ms + t {
315                return vec![Event::WorkflowTimeoutReached {}];
316            }
317        }
318    }
319
320    // 2. Per-unit transitions: deadlines, timers, retries, redelivery.
321    // Signals delivered in this pass (so two waiting units never take the same one).
322    let mut taken: Vec<u64> = vec![];
323    for (key, u) in &state.units {
324        let inv = invocation(def, key);
325        match u.status() {
326            UnitStatus::Dispatched => {
327                if u.deadline_ms.is_some_and(|d| now >= d) {
328                    out.push(Event::StepAttemptFailed {
329                        step: key.clone(),
330                        attempt: u.attempt,
331                        error: TaskError::new(
332                            codes::TIMEOUT,
333                            format!("attempt {} exceeded its timeout", u.attempt),
334                            true,
335                        ),
336                    });
337                }
338            }
339            UnitStatus::Scheduled => {
340                let qt = inv.and_then(|i| i.queue_timeout_ms);
341                if let (Some(qt), Some(at)) = (qt, u.scheduled_at_ms) {
342                    if !u.redelivery && now >= at + qt {
343                        out.push(Event::StepAttemptFailed {
344                            step: key.clone(),
345                            attempt: u.attempt,
346                            error: TaskError::new(
347                                codes::QUEUE_TIMEOUT,
348                                "no worker picked up the task before queue_timeout",
349                                false,
350                            ),
351                        });
352                    }
353                }
354            }
355            UnitStatus::Waiting => {
356                if let Some(signal) = awaited_signal(def, key) {
357                    // `await`: take the oldest undelivered signal of this name.
358                    let next = state.signals.iter().find(|r| {
359                        r.name == signal && r.delivered_to.is_none() && !taken.contains(&r.seq)
360                    });
361                    if let Some(r) = next {
362                        taken.push(r.seq);
363                        out.push(Event::SignalDelivered {
364                            step: key.clone(),
365                            signal_seq: r.seq,
366                        });
367                    } else if u.wake_at_ms.is_some_and(|w| now >= w) {
368                        out.push(Event::StepFailed {
369                            step: key.clone(),
370                            error: TaskError::new(
371                                codes::SIGNAL_TIMEOUT,
372                                format!("no `{signal}` signal arrived before the step's timeout"),
373                                false,
374                            ),
375                        });
376                    }
377                } else if u.wake_at_ms.is_some_and(|w| now >= w) {
378                    out.push(Event::TimerFired { step: key.clone() });
379                }
380            }
381            UnitStatus::RetryWait => {
382                if u.wake_at_ms.is_some_and(|w| now >= w) {
383                    if state.abort.is_some() && !is_cleanup_key(key) {
384                        out.push(Event::StepFailed {
385                            step: key.clone(),
386                            error: u.last_error.clone().unwrap_or_else(|| {
387                                TaskError::new(codes::CANCELLED, "aborted", false)
388                            }),
389                        });
390                    } else {
391                        out.push(Event::StepScheduled {
392                            step: key.clone(),
393                            attempt: u.attempt,
394                            input: u.input.clone().unwrap_or(Value::Null),
395                            idempotency_key: u.idempotency_key.clone().unwrap_or_default(),
396                            redelivery: false,
397                        });
398                    }
399                }
400            }
401            UnitStatus::AttemptFailed => out.push(after_failure(state, key, u, inv, now)),
402            UnitStatus::Uncertain => out.push(Event::StepScheduled {
403                step: key.clone(),
404                attempt: u.attempt,
405                input: u.input.clone().unwrap_or(Value::Null),
406                idempotency_key: u.idempotency_key.clone().unwrap_or_default(),
407                redelivery: true,
408            }),
409            _ => {}
410        }
411    }
412    if !out.is_empty() {
413        return out;
414    }
415
416    if let Some(phase) = &state.handlers {
417        return decide_handlers(state, def, phase, now);
418    }
419    match state.status {
420        WorkflowStatus::Running => decide_running(state, def, now),
421        WorkflowStatus::Compensating => decide_compensating(state, def),
422        _ => vec![],
423    }
424}
425
426/// Started `on event` instances: `(instance key, signal)`, in start order.
427fn event_instances<'a>(state: &'a ExecutionState, def: &'a WorkflowIr) -> Vec<(String, &'a str)> {
428    state
429        .signals
430        .iter()
431        .filter_map(|r| {
432            let key = r.delivered_to.as_ref()?;
433            let signal = key.strip_prefix("on_event:")?.split_once('@')?.0;
434            def.events
435                .get_key_value(signal)
436                .map(|(k, _)| (key.clone(), k.as_str()))
437        })
438        .collect()
439}
440
441/// The signal an `await` unit waits for.
442fn awaited_signal<'a>(def: &'a WorkflowIr, key: &str) -> Option<&'a str> {
443    let k = parse_key(key);
444    if k.compensation {
445        return None;
446    }
447    match &def.step(k.step)?.action {
448        Action::Await { signal, .. } => Some(signal),
449        _ => None,
450    }
451}
452
453/// Evaluate a `compute` step (or one element of it) in the runtime.
454fn compute(state: &ExecutionState, def: &WorkflowIr, key: &str, expr: &orbit_ir::Expr) -> Event {
455    let mut scope = StateScope::new(state);
456    scope.local = element(state, def, key);
457    if parse_handler_key(key).is_some() {
458        scope.failure = Some(failure_value(state));
459    }
460    scope.event = event_scope(state, key);
461    match eval(expr, &scope) {
462        Ok(output) => Event::StepSucceeded {
463            step: key.into(),
464            attempt: 1,
465            output,
466        },
467        Err(e) => Event::StepFailed {
468            step: key.into(),
469            error: TaskError::new(
470                codes::EXPRESSION_ERROR,
471                format!("evaluating `compute`: {e}"),
472                false,
473            ),
474        },
475    }
476}
477
478fn deps_done(state: &ExecutionState, step: &StepIr) -> bool {
479    step.dependencies.iter().all(|d| {
480        matches!(
481            unit(state, d).status(),
482            UnitStatus::Succeeded | UnitStatus::Skipped
483        )
484    })
485}
486
487/// Element unit keys of a fanned-out step, in order.
488fn element_keys(state: &ExecutionState, step: &str) -> Vec<String> {
489    let n = state
490        .units
491        .get(step)
492        .and_then(|u| u.input.as_ref())
493        .and_then(Value::as_array)
494        .map(Vec::len)
495        .unwrap_or(0);
496    (0..n).map(|i| element_key(step, i)).collect()
497}
498
499fn in_flight(status: UnitStatus) -> bool {
500    matches!(
501        status,
502        UnitStatus::Scheduled
503            | UnitStatus::Dispatched
504            | UnitStatus::AttemptFailed
505            | UnitStatus::Uncertain
506            | UnitStatus::RetryWait
507    )
508}
509
510/// Progress a running `for` step: schedule elements within `max_parallel`,
511/// and resolve the step once every element is done (or one failed).
512fn advance_fan_out(state: &ExecutionState, def: &WorkflowIr, step: &StepIr, out: &mut Vec<Event>) {
513    let keys = element_keys(state, &step.id);
514    let units: Vec<UnitState> = keys.iter().map(|k| unit(state, k)).collect();
515    if let Some(failed) = units.iter().find(|u| u.status() == UnitStatus::Failed) {
516        out.push(Event::StepFailed {
517            step: step.id.clone(),
518            error: failed
519                .error
520                .clone()
521                .unwrap_or_else(|| TaskError::new("UNKNOWN", "element failed", false)),
522        });
523        return;
524    }
525    if units.iter().all(|u| u.status() == UnitStatus::Succeeded) {
526        let output = Value::Array(
527            units
528                .iter()
529                .map(|u| u.output.clone().unwrap_or(Value::Null))
530                .collect(),
531        );
532        out.push(Event::StepSucceeded {
533            step: step.id.clone(),
534            attempt: 1,
535            output,
536        });
537        return;
538    }
539    if let Action::Compute { expr } = &step.action {
540        for (key, u) in keys.iter().zip(&units) {
541            if u.status() == UnitStatus::Pending {
542                out.push(compute(state, def, key, expr));
543            }
544        }
545        return;
546    }
547    let Some(inv) = step.invocation() else { return };
548    let limit = step
549        .for_each
550        .as_ref()
551        .and_then(|f| f.max_parallel)
552        .map(|n| n as usize)
553        .unwrap_or(usize::MAX);
554    let mut running = units.iter().filter(|u| in_flight(u.status())).count();
555    for (key, u) in keys.iter().zip(&units) {
556        if running >= limit {
557            break;
558        }
559        if u.status() == UnitStatus::Pending {
560            out.push(schedule(state, def, key, inv));
561            running += 1;
562        }
563    }
564}
565
566fn decide_running(state: &ExecutionState, def: &WorkflowIr, now: u64) -> Vec<Event> {
567    let mut out = vec![];
568    if state.abort.is_none() {
569        for step in &def.steps {
570            let u = unit(state, &step.id);
571            if u.status() == UnitStatus::Running {
572                advance_fan_out(state, def, step, &mut out);
573                continue;
574            }
575            if u.status() != UnitStatus::Pending || !deps_done(state, step) {
576                continue;
577            }
578            if let Some(cond) = &step.condition {
579                match eval(cond, &StateScope::new(state)) {
580                    Ok(Value::Bool(true)) => {}
581                    Ok(Value::Bool(false)) | Ok(Value::Null) => {
582                        out.push(Event::StepSkipped {
583                            step: step.id.clone(),
584                            reason: "`when` condition was false".into(),
585                        });
586                        continue;
587                    }
588                    Ok(other) => {
589                        out.push(Event::StepFailed {
590                            step: step.id.clone(),
591                            error: TaskError::new(
592                                codes::EXPRESSION_ERROR,
593                                format!("`when` evaluated to {other}, expected Bool"),
594                                false,
595                            ),
596                        });
597                        continue;
598                    }
599                    Err(e) => {
600                        out.push(Event::StepFailed {
601                            step: step.id.clone(),
602                            error: TaskError::new(
603                                codes::EXPRESSION_ERROR,
604                                format!("evaluating `when`: {e}"),
605                                false,
606                            ),
607                        });
608                        continue;
609                    }
610                }
611            }
612            if let Some(fe) = &step.for_each {
613                match eval(&fe.items, &StateScope::new(state)) {
614                    Ok(Value::Array(items)) => out.push(Event::StepFanOut {
615                        step: step.id.clone(),
616                        items,
617                    }),
618                    Ok(Value::Null) => out.push(Event::StepFanOut {
619                        step: step.id.clone(),
620                        items: vec![],
621                    }),
622                    Ok(other) => out.push(Event::StepFailed {
623                        step: step.id.clone(),
624                        error: TaskError::new(
625                            codes::EXPRESSION_ERROR,
626                            format!("`for` expected a list, got {other}"),
627                            false,
628                        ),
629                    }),
630                    Err(e) => out.push(Event::StepFailed {
631                        step: step.id.clone(),
632                        error: TaskError::new(
633                            codes::EXPRESSION_ERROR,
634                            format!("evaluating `for`: {e}"),
635                            false,
636                        ),
637                    }),
638                }
639                continue;
640            }
641            match &step.action {
642                Action::Sleep { duration_ms } => out.push(Event::TimerStarted {
643                    step: step.id.clone(),
644                    fire_at_ms: now + duration_ms,
645                }),
646                Action::Task(inv) | Action::Workflow(inv) => {
647                    out.push(schedule(state, def, &step.id, inv))
648                }
649                Action::Await { signal, timeout_ms } => out.push(Event::SignalAwaitStarted {
650                    step: step.id.clone(),
651                    signal: signal.clone(),
652                    deadline_ms: timeout_ms.map(|t| now + t),
653                }),
654                Action::Compute { expr } => out.push(compute(state, def, &step.id, expr)),
655            }
656        }
657        // `on event`: start an instance for every new signal of a handled name,
658        // then run the steps of every instance.
659        for r in &state.signals {
660            if r.delivered_to.is_none() && def.events.contains_key(&r.name) {
661                out.push(Event::SignalDelivered {
662                    step: event_instance_key(&r.name, r.seq),
663                    signal_seq: r.seq,
664                });
665            }
666        }
667        let mut events_done = true;
668        for (instance, signal) in event_instances(state, def) {
669            let steps = &def.events[signal];
670            for step in steps {
671                let key = format!("{instance}:{}", step.id);
672                let u = unit(state, &key);
673                if u.status().is_terminal() {
674                    continue;
675                }
676                events_done = false;
677                if u.status() != UnitStatus::Pending {
678                    continue;
679                }
680                let deps: Vec<UnitStatus> = step
681                    .dependencies
682                    .iter()
683                    .map(|d| unit(state, &format!("{instance}:{d}")).status())
684                    .collect();
685                if !deps
686                    .iter()
687                    .all(|s| matches!(s, UnitStatus::Succeeded | UnitStatus::Skipped))
688                {
689                    continue;
690                }
691                let mut scope = StateScope::new(state);
692                scope.event = event_scope(state, &key);
693                if let Some(cond) = &step.condition {
694                    match eval(cond, &scope) {
695                        Ok(Value::Bool(true)) => {}
696                        Ok(Value::Bool(false)) | Ok(Value::Null) => {
697                            out.push(Event::StepSkipped {
698                                step: key,
699                                reason: "`when` condition was false".into(),
700                            });
701                            continue;
702                        }
703                        Ok(other) => {
704                            out.push(Event::StepFailed {
705                                step: key,
706                                error: TaskError::new(
707                                    codes::EXPRESSION_ERROR,
708                                    format!("`when` evaluated to {other}, expected Bool"),
709                                    false,
710                                ),
711                            });
712                            continue;
713                        }
714                        Err(e) => {
715                            out.push(Event::StepFailed {
716                                step: key,
717                                error: TaskError::new(
718                                    codes::EXPRESSION_ERROR,
719                                    format!("evaluating `when`: {e}"),
720                                    false,
721                                ),
722                            });
723                            continue;
724                        }
725                    }
726                }
727                match &step.action {
728                    Action::Task(inv) | Action::Workflow(inv) => {
729                        out.push(schedule(state, def, &key, inv))
730                    }
731                    Action::Sleep { duration_ms } => out.push(Event::TimerStarted {
732                        step: key,
733                        fire_at_ms: now + duration_ms,
734                    }),
735                    Action::Compute { expr } => out.push(compute(state, def, &key, expr)),
736                    Action::Await { .. } => out.push(Event::StepFailed {
737                        step: key,
738                        error: TaskError::new(
739                            "UNKNOWN",
740                            "`await` is not allowed in an `on event` step",
741                            false,
742                        ),
743                    }),
744                }
745            }
746        }
747        if !out.is_empty() {
748            return out;
749        }
750        let all_done = events_done
751            && def
752                .steps
753                .iter()
754                .all(|s| unit(state, &s.id).status().is_terminal());
755        if all_done {
756            if let Some(event) = continue_as_new(state, def) {
757                return vec![event];
758            }
759            let output = match &def.result {
760                Some(expr) => match eval(expr, &StateScope::new(state)) {
761                    Ok(v) => v,
762                    Err(e) => {
763                        return vec![Event::WorkflowFailed {
764                            error: TaskError::new(
765                                codes::EXPRESSION_ERROR,
766                                format!("evaluating `return`: {e}"),
767                                false,
768                            ),
769                            compensation: CompensationOutcome::None,
770                        }]
771                    }
772                },
773                None => Value::Object(Default::default()),
774            };
775            if let Err(e) = def.output.validate(&output, false) {
776                return vec![Event::WorkflowFailed {
777                    error: TaskError::new(
778                        codes::OUTPUT_CONTRACT_VIOLATION,
779                        format!("workflow output is invalid at {e}"),
780                        false,
781                    ),
782                    compensation: CompensationOutcome::None,
783                }];
784            }
785            return vec![Event::WorkflowCompleted { output }];
786        }
787        return out;
788    }
789
790    // Aborting: withdraw work that has not started, wait for in-flight work,
791    // then compensate completed units in reverse completion order.
792    let mut waiting = false;
793    let withdraw =
794        |key: &str, u: &UnitState, out: &mut Vec<Event>, waiting: &mut bool| match u.status() {
795            UnitStatus::Pending | UnitStatus::Waiting | UnitStatus::RetryWait => {
796                out.push(Event::StepCancelled { step: key.into() })
797            }
798            UnitStatus::Scheduled if !u.redelivery => {
799                out.push(Event::StepCancelled { step: key.into() })
800            }
801            UnitStatus::Scheduled
802            | UnitStatus::Dispatched
803            | UnitStatus::AttemptFailed
804            | UnitStatus::Uncertain => *waiting = true,
805            _ => {}
806        };
807    for step in &def.steps {
808        let u = unit(state, &step.id);
809        if u.status() == UnitStatus::Running {
810            let mut element_waiting = false;
811            let mut failed = None;
812            for key in element_keys(state, &step.id) {
813                let eu = unit(state, &key);
814                if eu.status() == UnitStatus::Failed && failed.is_none() {
815                    failed = eu.error.clone();
816                }
817                withdraw(&key, &eu, &mut out, &mut element_waiting);
818            }
819            if element_waiting {
820                waiting = true;
821            } else {
822                // Every element is resolved: close the step itself.
823                out.push(match failed {
824                    Some(error) => Event::StepFailed {
825                        step: step.id.clone(),
826                        error,
827                    },
828                    None => Event::StepCancelled {
829                        step: step.id.clone(),
830                    },
831                });
832            }
833        } else {
834            withdraw(&step.id, &u, &mut out, &mut waiting);
835        }
836    }
837    for (instance, signal) in event_instances(state, def) {
838        for step in &def.events[signal] {
839            let key = format!("{instance}:{}", step.id);
840            let u = unit(state, &key);
841            withdraw(&key, &u, &mut out, &mut waiting);
842        }
843    }
844    if !out.is_empty() || waiting {
845        return out;
846    }
847    // Units that succeeded and have a compensation: whole steps, or each
848    // succeeded element of a `for` step.
849    let mut done: Vec<(String, u64)> = vec![];
850    for s in def.steps.iter().filter(|s| s.compensation.is_some()) {
851        let keys = if s.for_each.is_some() {
852            element_keys(state, &s.id)
853        } else {
854            vec![s.id.clone()]
855        };
856        for key in keys {
857            if let Some(u) = state.units.get(&key) {
858                if u.status() == UnitStatus::Succeeded {
859                    done.push((key, u.succeeded_seq.unwrap_or(0)));
860                }
861            }
862        }
863    }
864    done.sort_by_key(|d| std::cmp::Reverse(d.1));
865    if done.is_empty() {
866        return conclude(state, def, CompensationOutcome::None);
867    }
868    let reason = match &state.abort {
869        Some(AbortReason::Failed { error }) => error.message.clone(),
870        Some(AbortReason::Cancelled { reason }) => format!("cancelled: {reason}"),
871        Some(AbortReason::TimedOut) => "workflow timed out".into(),
872        None => String::new(),
873    };
874    vec![Event::CompensationStarted {
875        reason,
876        steps: done.into_iter().map(|(s, _)| s).collect(),
877    }]
878}
879
880/// `continue_as_new`: `None` when the workflow has none or its condition is
881/// false (the run then completes with `return`), else the event that ends
882/// the run (or fails it, if the condition or the input cannot be evaluated).
883fn continue_as_new(state: &ExecutionState, def: &WorkflowIr) -> Option<Event> {
884    let can = def.continue_as_new.as_ref()?;
885    let scope = StateScope::new(state);
886    let fail = |code: &str, message: String| Event::WorkflowFailed {
887        error: TaskError::new(code, message, false),
888        compensation: CompensationOutcome::None,
889    };
890    if let Some(cond) = &can.condition {
891        match eval(cond, &scope) {
892            Ok(Value::Bool(true)) => {}
893            Ok(Value::Bool(false)) | Ok(Value::Null) => return None,
894            Ok(other) => {
895                return Some(fail(
896                    codes::EXPRESSION_ERROR,
897                    format!("the `continue_as_new` condition evaluated to {other}, expected Bool"),
898                ))
899            }
900            Err(e) => {
901                return Some(fail(
902                    codes::EXPRESSION_ERROR,
903                    format!("evaluating the `continue_as_new` condition: {e}"),
904                ))
905            }
906        }
907    }
908    let mut input = match eval(&can.input, &scope) {
909        Ok(v) => v,
910        Err(e) => {
911            return Some(fail(
912                codes::EXPRESSION_ERROR,
913                format!("evaluating the `continue_as_new` input: {e}"),
914            ))
915        }
916    };
917    def.input.apply_defaults(&mut input);
918    if let Err(e) = def.input.validate(&input, false) {
919        return Some(fail(
920            codes::INPUT_CONTRACT_VIOLATION,
921            format!("the `continue_as_new` input is invalid at {e}"),
922        ));
923    }
924    Some(Event::WorkflowContinuedAsNew {
925        next_run: state.run + 1,
926        input,
927    })
928}
929
930fn decide_compensating(state: &ExecutionState, def: &WorkflowIr) -> Vec<Event> {
931    // Compensations run one at a time, newest completed unit first (Saga). A
932    // compensation that fails does not stop the others: every remaining effect
933    // is still undone, and each failure is reported at the end.
934    let mut failures = vec![];
935    for step in &state.compensation_order {
936        let key = compensation_key(step);
937        let u = unit(state, &key);
938        match u.status() {
939            UnitStatus::Succeeded => continue,
940            UnitStatus::Failed | UnitStatus::Cancelled => {
941                let error = u
942                    .error
943                    .unwrap_or_else(|| TaskError::new("UNKNOWN", "compensation failed", false));
944                failures.push(Event::CompensationFailed {
945                    step: step.clone(),
946                    error,
947                });
948                continue;
949            }
950            UnitStatus::Pending => {
951                return match invocation(def, &key) {
952                    Some(inv) => vec![schedule(state, def, &key, inv)],
953                    None => vec![Event::StepFailed {
954                        step: key,
955                        error: TaskError::new("UNKNOWN", "missing compensation", false),
956                    }],
957                };
958            }
959            _ => return vec![],
960        }
961    }
962    if !failures.is_empty() {
963        failures.extend(conclude(state, def, CompensationOutcome::Failed));
964        return failures;
965    }
966    let mut out = vec![Event::CompensationCompleted {}];
967    out.extend(conclude(state, def, CompensationOutcome::Completed));
968    out
969}
970
971/// The handler kind for the abort reason.
972fn handler_kind(state: &ExecutionState) -> &'static str {
973    match &state.abort {
974        Some(AbortReason::Cancelled { .. }) => "cancel",
975        Some(AbortReason::TimedOut) => "timeout",
976        _ => "failure",
977    }
978}
979
980/// After compensation: start the matching `on` handler, or finish.
981fn conclude(state: &ExecutionState, def: &WorkflowIr, outcome: CompensationOutcome) -> Vec<Event> {
982    let kind = handler_kind(state);
983    match def.handlers.get(kind) {
984        Some(steps) if !steps.is_empty() => vec![Event::HandlersStarted {
985            kind: kind.to_string(),
986            compensation: outcome,
987        }],
988        _ => vec![finish(state, outcome)],
989    }
990}
991
992/// Run the handler steps of the current phase; finish when they are all done.
993/// A failed handler step is recorded but does not change the outcome; steps
994/// that depend on it are cancelled.
995fn decide_handlers(
996    state: &ExecutionState,
997    def: &WorkflowIr,
998    phase: &HandlerPhase,
999    now: u64,
1000) -> Vec<Event> {
1001    let steps = def
1002        .handlers
1003        .get(&phase.kind)
1004        .map(Vec::as_slice)
1005        .unwrap_or(&[]);
1006    let mut out = vec![];
1007    let mut all_done = true;
1008    for step in steps {
1009        let key = handler_key(&phase.kind, &step.id);
1010        let u = unit(state, &key);
1011        if u.status().is_terminal() {
1012            continue;
1013        }
1014        all_done = false;
1015        if u.status() != UnitStatus::Pending {
1016            continue;
1017        }
1018        let dep_status: Vec<UnitStatus> = step
1019            .dependencies
1020            .iter()
1021            .map(|d| unit(state, &handler_key(&phase.kind, d)).status())
1022            .collect();
1023        if dep_status
1024            .iter()
1025            .any(|s| matches!(s, UnitStatus::Failed | UnitStatus::Cancelled))
1026        {
1027            out.push(Event::StepCancelled { step: key });
1028            continue;
1029        }
1030        if !dep_status
1031            .iter()
1032            .all(|s| matches!(s, UnitStatus::Succeeded | UnitStatus::Skipped))
1033        {
1034            continue;
1035        }
1036        let mut scope = StateScope::new(state);
1037        scope.failure = Some(failure_value(state));
1038        if let Some(cond) = &step.condition {
1039            match eval(cond, &scope) {
1040                Ok(Value::Bool(true)) => {}
1041                Ok(Value::Bool(false)) | Ok(Value::Null) => {
1042                    out.push(Event::StepSkipped {
1043                        step: key,
1044                        reason: "`when` condition was false".into(),
1045                    });
1046                    continue;
1047                }
1048                Ok(other) => {
1049                    out.push(Event::StepFailed {
1050                        step: key,
1051                        error: TaskError::new(
1052                            codes::EXPRESSION_ERROR,
1053                            format!("`when` evaluated to {other}, expected Bool"),
1054                            false,
1055                        ),
1056                    });
1057                    continue;
1058                }
1059                Err(e) => {
1060                    out.push(Event::StepFailed {
1061                        step: key,
1062                        error: TaskError::new(
1063                            codes::EXPRESSION_ERROR,
1064                            format!("evaluating `when`: {e}"),
1065                            false,
1066                        ),
1067                    });
1068                    continue;
1069                }
1070            }
1071        }
1072        match &step.action {
1073            Action::Task(inv) | Action::Workflow(inv) => out.push(schedule(state, def, &key, inv)),
1074            Action::Sleep { duration_ms } => out.push(Event::TimerStarted {
1075                step: key,
1076                fire_at_ms: now + duration_ms,
1077            }),
1078            Action::Compute { expr } => out.push(compute(state, def, &key, expr)),
1079            Action::Await { .. } => out.push(Event::StepFailed {
1080                step: key,
1081                error: TaskError::new("UNKNOWN", "`await` is not allowed in a handler", false),
1082            }),
1083        }
1084    }
1085    if all_done && out.is_empty() {
1086        return vec![finish(state, phase.compensation)];
1087    }
1088    out
1089}