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-refund2. 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 }):
- Materialized State: The latest executed node's output overrides the earlier value (
data: { ...state.data, ...output }). - TypeScript Compilation: The type signature cleanly overrides the matching property key while preserving all unaffected accumulated state properties.
- Audit History: Both distinct values are permanently recorded in their respective
STEP_COMPLETEDorCAPABILITY_COMPLETEDevents 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:
| Feature | Session Context (session.context) | Execution State (state) |
|---|---|---|
| Scope | Across related activity & runs | One workflow run (runId) |
| Defined by | Application type, validated application data, and hydrate() | Workflow node outputs |
| Purpose | Durable continuity | Durable 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):
WORKFLOW_STARTEDSTEP_COMPLETEDorSTEP_FAILEDREASON_COMPLETEDorREASON_FAILEDCAPABILITY_COMPLETEDorCAPABILITY_FAILEDWAIT_ENTEREDINPUT_RECEIVEDWORKFLOW_CANCEL_REQUESTEDWORKFLOW_CANCELLATION_STARTEDWORKFLOW_CANCELLEDorWORKFLOW_CANCELLATION_FAILEDWORKFLOW_COMPLETEDorWORKFLOW_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 UUIDrunId` is created by the Host/SDK, outside the pure Kernel. - Revision Parity:
revisionadvances 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 Systemtext
At-Least-Once Dispatch
│
▼
Stable Idempotency Key (e.g. `refund:${execution.id}`)
│
▼
External System Honors Key?
┌────┴────┐
yes no
│ │
▼ ▼
Effectively At-Least-Once
Once InvocationCapabilities 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):
- Failure Facts (
CAPABILITY_FAILED): If a capability throws during the in-process Session command drain, the Session commits aCAPABILITY_FAILEDevent. The Beta does not schedule automatic retries. - Fallback Edges: A capability may declare a deterministic
fallbacknode for application-owned recovery or escalation logic. - 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 attachmentLoading 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
- Database Schema & Data Lifecycle — See how materialized state, immutable events, commands, and revisions are stored and updated.
- Durability Guarantees — Explore the 8 current runtime guarantees.
- Runtime Execution — Inspect the canonical Beta execution model and recovery limitations.
- Sessions & Hydration — Learn how long-lived application context is hydrated.