Durable execution
A crash mid-run does not lose work, re-spend tokens, or fire a side effect twice. The shared tier (inline executor) recovers through an event-sourced journal_events table and a CompletedStep replay gate, with external deliveries de-duplicated by idempotency key. MicroVM runs are re-scheduled by the same recovery sweep (see below).
1. POST /v1/runs
The SDK sends { agentName, input }. The route requires the runs:execute scope.
Event-sourced journal
Every step transition is appended to journal_events before the next step begins. The journal — not in-memory state — is authoritative. On the shared tier the inline executor writes these events directly. On the microVM tier the control plane accepts step_event reports on /v1/runtime/report and writes them to the same table; the harness does not emit step events yet.
run step journal events
────────────────────────────────────────────────
run_1 step_a step_started → step_completed
run_1 step_b step_started → step_completed
run_1 step_c step_started ◀── crash here
(no step_completed written)Other event kinds you will see in the waterfall: step_retrying (between retry attempts), step_waiting (run parked at an approval node), anomaly_detected (runaway_loop, excessive_steps, retry_storm, cost_spike or token_spike), confidence_evaluated (confidence gate decision, when enabled), and confidence_gate_bypassed (gating is on but no handler is wired — step auto-approves).
CompletedStep replay
The workflow interpreter receives a CompletedStep hook wired to journalCompletedStep. Before invoking any side-effecting node (ai-step, tool, connector, subagent, approval) the interpreter queries journal_events for an existing step_completed row. If one is found, the cached output is returned and the underlying side-effect is never re-invoked.
The plain-LLM path has the equivalent checkCachedLLMStep cache: before calling the model router, the executor checks the journal for a previous successful LLM step for this run. If found, it replays the cached response — the provider is never contacted again.
Resume from the last completed step
On re-drive, the interpreter walks the graph from the trigger node again. Nodes that already have a step_completed row are skipped via the CompletedStep hook. Execution continues from the first incomplete node. This is idempotent: a re-drive that re-walks an already-finished graph is a no-op.
Side-effect dedup via idempotency keys
Every external side effect carries a key derived from a one-way hash of the three identifiers, pipe-delimited:
idempotency_key = sha256("runID|stepID|attempt")LLM provider calls receive an Idempotency-Key HTTP header on every request to OpenAI and Anthropic (derived in internal/handlers/llm_idempotency.govia a one-way hash — the key never carries secret material). A crash then replay to the same provider dedups at the provider instead of double-billing.
External deliveries (email, connector auth pages, browser and cross-app actions) claim a side_effect_receipts row before dispatching. A re-drive that reaches the same delivery finds the receipt and short-circuits. Workflow connector and tool nodes rely on the CompletedStep skip instead.
Shared-tier recovery sweep
The shared tier uses run_locks to detect orphaned runs. Each in-flight run holds a lock row (run_id, worker_id, expires_at). A background loop — RunRecoveryLoop — runs every 30 s (override: LANTERN_RECOVERY_INTERVAL, a Go duration; invalid or ≤0 values fall back to 30 s, so the sweep cannot be disabled). Each pass:
resumeMicroVMRun — see MicroVM-tier crash-resume below — rather than re-driving it via the shared-tier inline executor. The two paths are distinct to avoid racing with the scheduler's own state machine.Scheduler HA (microVM tier)
Leader election keeps one active scheduler, and placement state is durable. VMs lost with a node are reconciled to terminated; microVM runs are retried by the control-plane recovery sweep (resumeMicroVMRun, max 3 attempts).
Per-step retry policy
Workflow nodes can declare a retry policy in node.Data["retry"]:
{
"retry": {
"maxAttempts": 3,
"backoffMs": 500,
"retryableClasses": ["llm", "connector"]
}
}maxAttempts is the total number of attempts including the first (default 1 = no retry). Between attempts the interpreter journals a step_retrying event with {attempt, of, error} — visible in the run waterfall. Retryable classes: any, timeout, llm, connector, tool.
Retry wraps executeNode outside the CompletedStep check. A crash-replay skips a node only when it has a step_completed row — it does not re-retry a node that was mid-retry when the crash happened.
MicroVM-tier crash-resume
The recovery sweep handles microVM runs separately from shared-tier runs. When the sweep finds an orphaned microvm run (no live VM, lock expired), it calls resumeMicroVMRun:
- If a live (non-terminal) VM exists for this run — skip. The VM is self-managing.
- Count all
runtime_vmsrows for this run (total VMs ever spawned = resume attempts). - If
attempts ≥ 3— mark the runfailedwith codemicrovm_resume_exhausted. No further retries. - Otherwise — re-schedule a new VM via the dispatcher, injecting
LANTERN_RESUME=1in the environment.
# environment injected into the re-scheduled VM
LANTERN_RESUME=1LANTERN_RESUME=1 signals a re-drive; skipping completed work is the workload's responsibility (no SDK helper reads it yet). The 3-attempt cap is a hard constant (maxMicroVMResumeAttempts = 3). On exhaustion the run is marked failed and its error field carries {code: microvm_resume_exhausted, attempts}.
Why it matters
- No double-spend. LLM steps are replayed from the journal, never re-billed at the provider.
- No double-send. Idempotency keys and
side_effect_receiptsde-dup LLM calls and external deliveries across retries. - No babysitting. The 30 s recovery sweep re-drives crashed shared-tier runs and re-schedules microVM runs (up to 3 attempts) without intervention.
- Retry visibility.
step_retryingevents appear in the waterfall so you see exactly how many attempts a step took.
Each resume is still one trace per span — see Observability — and the completed run's journal is what the verifiable receipt is signed over.