Skip to content

Concepts: State & Durability

Invariant treats execution state as durable infrastructure, not ephemeral model memory.

"The model may forget. The runtime must not."


1. Execution State

Execution State (state) represents the accumulated progress of a single Workflow run.

Invariant persists that progress durably so later nodes in the same live execution continue from committed state rather than attempting to reconstruct context from model memory. After process loss, a new process can read those facts but cannot continue the run through the current public API:

ts
const workflow = app.workflow("refund", {
  inputSchema: z.object({
    orderId: z.string(),
  }),
})
  .capability("load-order", loadOrder)
  .step("check-policy", ({ state }) => ({
    eligible: state.order.ageDays <= 30,
  }))
  .capability("issue-refund", ({ state }) => {
    // state.order and state.eligible came from previous node outputs
  });
text
input (orderId)


load-order

   ├── state.order

check-policy

   ├── state.eligible

issue-refund

2. Execution State Accumulates Through Node Outputs

Execution state is built incrementally as each graph node returns data.

Explicit Return Contracts ("What You Return Is What You Get")

Invariant does not perform magical name inference from node ID strings, nor does it rely on hidden mutations. Handlers explicitly return structured objects whose keys merge into execution state:

ts
// 1. Explicit return shape from capability
async function loadOrder({ input }: { input: { orderId: string } }) {
  const order = await db.orders.find(input.orderId);
  return { order }; // <-- Explicitly adds 'order' to state
}

// 2. Explicit return shape from step
function checkPolicy({ state }: { state: { order: Order } }) {
  const eligible = state.order.totalAmount < 500;
  return { eligible, checkedAt: new Date().toISOString() }; // <-- Adds 'eligible' and 'checkedAt'
}

Type-Level Accumulation in TypeScript

The WorkflowBuilder<TInput, TState> uses compile-time generic state accumulation. At each step, capability, wait point, or branch, TypeScript strictly calculates the merged state shape:

ts
export type Merge<A, B> = string extends keyof B
  ? A & B
  : { [K in keyof A as K extends keyof B ? never : K]: A[K] } & B;
ts
const workflow = app.workflow<BookingInput>("sample", {
  description: "Salon appointment booking",
})
  .capability("load-order", loadOrder)      // TState = { locationId?: string, cart: Cart }
  .wait<{ selectedDate: string }>("select-date-ui", { schema: SelectDateSchema })
  // TState = { locationId?: string, cart: Cart, selectedDate: string }
  .step("check-policy", ({ state }) => {
    // IDE provides 100% autocompletion on state.cart and state.selectedDate!
    return { eligible: true };              
    // TState = { locationId?: string, cart: Cart, selectedDate: string, eligible: boolean }
  });

Property Collision & Overwrite Semantics

If two sequential nodes return a property with the same key (e.g. both return { status: string }):

  1. Materialized State: The latest executed node's output overrides the earlier value (data: { ...state.data, ...output }).
  2. TypeScript Compilation: The type signature cleanly overrides the matching property key while preserving all unaffected accumulated state properties.
  3. Audit History: Both distinct values are permanently recorded in their respective STEP_COMPLETED or CAPABILITY_COMPLETED events in the append-only event log. Nothing is lost.

3. Session Context vs. Execution State

It is essential to keep long-lived session context separate from workflow execution state:

FeatureSession Context (session.context)Execution State (state)
ScopeAcross related activity & runsOne workflow run (runId)
Defined byApplication type, validated application data, and hydrate()Workflow node outputs
PurposeDurable continuityDurable progress

Canonical Home: For hydration, rehydration, and the reserved validation boundary, see Session Context Schema Status.

Workflow input is a third, explicit boundary. Starting a run passes only the object supplied to session.startWorkflow(workflow, input) or start_workflow({ workflowId, input }). Invariant does not merge session.context into that input. If a workflow needs a context value, application code must deliberately project or copy the authorized field into the input payload so the run contract remains reviewable and schema-validatable.


4. How State Becomes Durable (Event Reduction)

Execution State is not stored merely as a mutable database row. Invariant records the immutable facts (events) that produced it:

text
WORKFLOW_STARTED


CAPABILITY_COMPLETED
{ order: { ageDays: 14 } }


STEP_COMPLETED
{ eligible: true }


CAPABILITY_COMPLETED
{ refundId: "rf_99" }


WORKFLOW_COMPLETED

        │ reduce (pure reducer)


Materialized Execution State
{
  order: { ageDays: 14 },
  eligible: true,
  refundId: "rf_99"
}

State is derived deterministically by passing historical execution events through pure reducers.


5. Execution Event History

Every execution-relevant fact is represented durably in the execution history with a strictly monotonic sequence number (seq = 1..n):

  1. WORKFLOW_STARTED
  2. STEP_COMPLETED or STEP_FAILED
  3. REASON_COMPLETED or REASON_FAILED
  4. CAPABILITY_COMPLETED or CAPABILITY_FAILED
  5. WAIT_ENTERED
  6. INPUT_RECEIVED
  7. WORKFLOW_CANCEL_REQUESTED
  8. WORKFLOW_CANCELLATION_STARTED
  9. WORKFLOW_CANCELLED or WORKFLOW_CANCELLATION_FAILED
  10. WORKFLOW_COMPLETED or WORKFLOW_FAILED

Failure facts advance revision exactly like completion facts. In particular, a throwing deterministic step appends STEP_FAILED; the Kernel then appends WORKFLOW_FAILED in the same atomic transition when no fallback applies. Neither event merges synthetic step output into application state.

Event History Properties

  • Append-Only Execution History: Committed execution events are not rewritten as part of normal runtime execution. Payload retention or redaction may be governed separately by storage policy.
  • Monotonic Ordering: Events within an execution have strictly increasing sequence numbers (seq).
  • Run-Scoped Identity: Newly written events use ``id = ${runId}:${seq}```; a UUID runId` is created by the Host/SDK, outside the pure Kernel.
  • Revision Parity: revision advances once for every durable event. After a commit, the materialized revision equals the last committed event sequence, even when one transaction appends multiple events.
  • Data Provenance: Events preserve the execution facts and metadata required for auditing and replay, subject to configured payload retention or redaction policy.
  • Legacy Compatibility: Persisted histories retain their original IDs and remain readable and replayable; adopting the current write format does not backfill history.

6. Materialization & Storage Decoupling

Invariant decouples execution fact persistence from state materialization:

  • Execution Event History — The immutable source of execution facts.
  • Materialized Execution State — Current derived state persisted for fast runtime access.

7. State & External Effects (Idempotency)

The state transition and the intent to perform an external effect are committed atomically. The current Session host invokes the handler in process after that commit:

text
Atomic Transaction (BEGIN...COMMIT)
├── Execution State
├── Execution Event
└── Capability Intent


 In-Process Session Dispatcher


 External System
text
At-Least-Once Dispatch


Stable Idempotency Key (e.g. `refund:${execution.id}`)


External System Honors Key?
   ┌────┴────┐
  yes        no
   │         │
   ▼         ▼
Effectively  At-Least-Once
  Once       Invocation

Capabilities supply stable durable command identities. The current public handler contract does not expose the resolved command key, so provider-side deduplication requires a separate stable application request key. Invariant guarantees durable intent, but effectively-once external effects require the target API to honor that provider key. See Effect Identity.

Capability Failure Modes

When a capability touches an external network resource (database, payment gateway, third-party API):

  1. Failure Facts (CAPABILITY_FAILED): If a capability throws during the in-process Session command drain, the Session commits a CAPABILITY_FAILED event. The Beta does not schedule automatic retries.
  2. Fallback Edges: A capability may declare a deterministic fallback node for application-owned recovery or escalation logic.
  3. Branching & Compensation: Workflows can inspect error states or branch to compensation steps (e.g. notifying human operators or creating customer support tickets) without losing any previously committed state.

"Invariant guarantees repeatable intent, not exactly-once effects in systems it does not control."


8. Crash Recovery Without AI Memory Reconstruction

When a process crashes, committed facts do not need to be reconstructed by an LLM. A durable store can expose the materialized state and event history to a new process:

text
Worker Crash / Process Restart


   Load committed materialized state


   Read ordered event history for audit


Public SDK stops before command claim and runnable-work attachment

Loading facts is not the same as resuming interrupted work. Session restoration can reattach a registered .wait() boundary; it cannot claim and redispatch a pending capability command. The exact boundary is defined once in the Recovery Contract Matrix.


Go Deeper

Invariant Durable Execution Engine.