State
The State is the single source of truth for a running workflow. Every node reads from it, writes to it, and the engine persists it after each step for crash recovery.
import { state } from '@cycgraph/orchestrator';
const initialState = state({ workflowId: graph.id, goal: 'Research and summarize quantum computing', constraints: ['Under 500 words'], maxExecutionTimeMs: 120_000,});Schema reference
Section titled “Schema reference”State is held in a single flat object, grouped below by concern. The first six groups hold the run’s own data: identity and input, control flow, retry and resilience, waiting state, cost, and memory.
The last two hold engine-owned fields that the runner manages and agents never see. The runner populates and maintains everything else.
Identity and input
Section titled “Identity and input”Set when the run is created and fixed for its lifetime.
| Field | Type | Default | Description |
|---|---|---|---|
workflow_id |
string (UUID) |
required | Graph definition this run belongs to. |
run_id |
string (UUID) |
auto-generated | Unique identifier for this execution. |
goal |
string |
required | High-level objective for the workflow. |
constraints |
string[] |
[] |
Rules the workflow must respect. |
Control flow
Section titled “Control flow”Drive the run’s lifecycle and which node executes next.
| Field | Type | Default | Description |
|---|---|---|---|
status |
WorkflowStatus |
'pending' |
Current lifecycle status. |
current_node |
string |
— | Node currently being executed. |
iteration_count |
number |
0 |
Total reducer dispatches so far. The loop guard. |
max_iterations |
number |
50 |
Hard cap: the run fails if exceeded. |
started_at |
Date |
— | When the run was first invoked. |
max_execution_time_ms |
number |
3600000 (1h) |
Wall-clock timeout for the entire run. |
Retry and resilience
Section titled “Retry and resilience”Track node-level retries and the compensating actions for saga rollback.
| Field | Type | Default | Description |
|---|---|---|---|
retry_count |
number |
0 |
Retries on the current node so far. |
max_retries |
number |
3 |
Maximum retries before the node fails permanently. |
last_error |
string |
— | Error message from the most recent failure. |
compensation_stack |
CompensationEntry[] |
[] |
Stack of typed compensating actions for saga rollback. Each entry has action_id and compensation_action: { type, payload }. |
Waiting (human-in-the-loop)
Section titled “Waiting (human-in-the-loop)”Populated while the run is paused in the waiting status.
| Field | Type | Default | Description |
|---|---|---|---|
waiting_for |
WaitingReason |
— | Why the workflow is paused (e.g. human_approval). |
waiting_since |
Date |
— | When the workflow entered the waiting state. |
waiting_timeout_at |
Date |
— | Deadline after which the wait times out. |
Cost and token tracking
Section titled “Cost and token tracking”Running totals, and the ceilings that fail the run when breached.
| Field | Type | Default | Description |
|---|---|---|---|
total_tokens_used |
number |
0 |
Cumulative tokens consumed across all LLM calls. |
max_token_budget |
number |
— | If set, the run fails when token usage exceeds this. |
total_input_tokens |
number |
0 |
Cumulative prompt tokens. |
total_output_tokens |
number |
0 |
Cumulative completion tokens. |
total_cost_usd |
number |
0 |
Cumulative estimated cost in USD. |
budget_usd |
number |
— | Per-run cost budget. Run fails when exceeded. |
model_breakdown |
Record<string, Spend> |
{} |
The same totals split by model id. |
node_breakdown |
Record<string, Spend> |
{} |
The same totals split by the node that incurred them. |
Spend is { input_tokens, output_tokens, cost_usd, calls }. node_breakdown answers which step is expensive, which the run totals cannot: a retrying node’s failed attempts are attributed too, so the cost of the retries is visible rather than folded into the whole.
Memory and tracking
Section titled “Memory and tracking”The shared blackboard, plus the execution-history bookkeeping the engine keeps.
| Field | Type | Default | Description |
|---|---|---|---|
memory |
Record<string, unknown> |
{} |
Shared key-value store. See Memory below. |
visited_nodes |
string[] |
[] |
Node IDs visited in execution order. |
supervisor_history |
object[] |
[] |
Routing decisions made by supervisor nodes. Used for debugging. |
created_at |
Date |
now | When this run was created. |
updated_at |
Date |
now | Last state mutation timestamp. |
Engine-owned registries
Section titled “Engine-owned registries”These fields hold data the engine trusts and agents never see. They live beside memory rather than inside it, so state slicing excludes them structurally. No node’s state view ever contains them, and no memory write can touch them. Reducers keep the two registries append-only.
| Field | Type | Default | Description |
|---|---|---|---|
taint_registry |
TaintRegistry |
{} |
Provenance of untrusted external data, keyed by memory key. See Taint Tracking. |
lesson_provenance |
LessonProvenanceRegistry |
{} |
Which retrieved facts entered which node’s prompt (eval-gated learning evidence). Read with getInjectedFactIds(finalState). |
pending_approval |
unknown |
— | Review payload for the active human-in-the-loop pause. |
policy_approvals |
Record<string, boolean> |
{} |
Security-policy approvals granted by a human, keyed by node id. |
subgraph_checkpoints |
Record<string, unknown> |
{} |
Paused child-run checkpoints for subgraph nodes awaiting a nested approval. |
subgraph_stack |
string[] |
[] |
Ancestor graph ids in a child run (subgraph cycle/depth detection). |
swarm_handoff_count |
number |
0 |
Swarm peer-handoff counter (bounds maxHandoffs). |
Persistence bookkeeping
Section titled “Persistence bookkeeping”| Field | Type | Default | Description |
|---|---|---|---|
state_schema_version |
number |
1 |
Schema version stamped on a freshly created state. Loaded snapshots pass through hydrateWorkflowState(), which migrates older versions forward to CURRENT_STATE_SCHEMA_VERSION (v1 snapshots have their memory._* system keys lifted into the fields above) and refuses snapshots from a newer engine. |
_last_event_sequence_id |
number |
— | Event-log high-water mark at the moment the snapshot was persisted. Resume logic uses it to decide whether a logged action’s effects are already inside the snapshot (crash-window idempotency). |
These fields are managed by the runner, so don’t set them by hand.
Refs:
- state: Build state from input.
- hydrateWorkflowState: Parse and migrate a persisted snapshot at a load boundary.
- Lesson provenance readers:
getInjectedFactIds,getLessonProvenance. - WorkflowState: The full runtime state shape.
Status lifecycle
Section titled “Status lifecycle”stateDiagram-v2
direction LR
pending --> scheduled
scheduled --> running
running --> completed
running --> waiting
running --> retrying
waiting --> running
retrying --> running
retrying --> failed
running --> cancelled
running --> timeout
failed --> cancelled: saga rollback
timeout --> cancelled: saga rollback
Refs:
- Status guards:
canTransitionStatus,isTerminalStatus,TERMINAL_STATUSES. - WorkflowStatus / WaitingReason: The status and waiting enums.
Memory
Section titled “Memory”The memory object is the primary data exchange between nodes. It’s an arbitrary key-value store, so you define the keys based on your workflow’s needs. Agents write to it via their text output, which the orchestrator automatically routes to the node’s write key. Agents read from memory via their filtered state view, controlled by read keys on the node.
- Use descriptive keys.
research_notesis better thandataorresult. - Reference, don’t store. Avoid large blobs in memory; store them externally and keep a reference.
- Keep it flat. Deeply nested objects are harder to debug.
Memory layers
Section titled “Memory layers”| Layer | Scope | Persistence | Purpose |
|---|---|---|---|
| Graph State | Shared across all nodes | Persisted after every step | Source of truth: goal, results, artifacts |
| Thread Context | Local to a single agent | Ephemeral | Raw LLM conversation for the current agent |
Graph State is the memory object. It’s persisted after every node execution, enabling crash recovery and time-travel debugging.
Thread Context is the raw LLM conversation history within a single agent execution. Each agent has its own thread, so agents don’t see each other’s raw messages. The orchestrator automatically captures the agent’s text output and routes it to the appropriate write key, and the thread is discarded.
Action types
Section titled “Action types”Actions dispatched to the reducer use a discriminated union type ActionTypeSchema. Valid action types are:
| Action Type | Purpose |
|---|---|
update_memory |
Write key-value pairs to the memory object |
set_status |
Transition the workflow status |
goto_node |
Override the next node in the graph |
handoff |
Transfer control to another agent/workflow |
request_human_input |
Pause for human-in-the-loop approval |
resume_from_human |
Inject human response and resume |
merge_parallel_results |
Combine results from parallel node execution |
Refs:
- Action: The reducer action shape and its type union.
Taint tracking
Section titled “Taint tracking”Data entering the system from external tools, such as web search and file reads, is flagged as tainted. Taint propagates automatically. If a node reads tainted data and writes to state, the output key inherits the taint flag. This lets downstream nodes make trust decisions about their inputs.
Refs:
- TaintMetadata: Provenance of the untrusted data behind a memory key.
Build a valid WorkflowState from authoring input.
state(input: WorkflowStateConfig): WorkflowStatecreateWorkflowState(input: WorkflowStateConfig): WorkflowStateOptions
Section titled “Options”The input is a WorkflowStateConfig. The common fields:
| Parameter | Type | Default | Description |
|---|---|---|---|
workflowId |
string (UUID) |
required | Graph definition this run belongs to. |
goal |
string |
required | High-level objective for the run. |
constraints |
string[] |
[] |
Rules the run must respect. |
maxExecutionTimeMs |
number |
3600000 |
Wall-clock timeout for the whole run. |
maxTokenBudget |
number |
— | Token ceiling. The run fails when usage exceeds it. |
budgetUsd |
number |
— | Cost ceiling in USD. |
memory |
Record<string, unknown> |
{} |
Seed values for the blackboard. |
hydrateWorkflowState
Section titled “hydrateWorkflowState”Parse a persisted state at a load boundary. Persisted snapshots round-trip through JSON/jsonb. Every path that loads state from storage (checkpoints, snapshots, recovery) must pass it through this function. It runs any pending schema migrations, then parses with WorkflowStateSchema.
hydrateWorkflowState(raw: unknown): WorkflowStateStatus guards
Section titled “Status guards”Check status-transition legality without running the reducer. canTransitionStatus returns false for any move out of a terminal status, with one exception: the saga-rollback move from failed or timeout to cancelled.
canTransitionStatus(from: WorkflowStatus, to: WorkflowStatus): booleanisTerminalStatus(status: WorkflowStatus): booleanTERMINAL_STATUSES: ReadonlySet<WorkflowStatus>Lesson provenance readers
Section titled “Lesson provenance readers”Read the append-only lesson_provenance registry after a run, for eval-gated learning.
getInjectedFactIds(state: WorkflowState): string[]getLessonProvenance(state: WorkflowState): LessonProvenanceEntry[]getInjectedFactIds returns the deduplicated fact IDs injected into prompts during the run, in deterministic order. This is the value to pass as fact_ids when recording the run’s outcome. getLessonProvenance returns the individual LessonProvenanceEntry records.
CURRENT_STATE_SCHEMA_VERSION
Section titled “CURRENT_STATE_SCHEMA_VERSION”The schema version this engine build writes, currently 2. hydrateWorkflowState migrates older snapshots up to it and refuses snapshots from a newer engine.
CURRENT_STATE_SCHEMA_VERSION: numberInterfaces
Section titled “Interfaces”WorkflowState
Section titled “WorkflowState”The complete runtime state, persisted after every reducer dispatch. Each field is documented in the Schema reference above.
WorkflowStateConfig
Section titled “WorkflowStateConfig”The authoring shape accepted by state.
WorkflowStatus
Section titled “WorkflowStatus”The lifecycle status enum. See Status lifecycle for the legal transitions.
| Group | Values |
|---|---|
| Initial | 'pending', 'scheduled' |
| Active | 'running', 'waiting', 'retrying' |
| Terminal | 'completed', 'failed', 'cancelled', 'timeout' |
WaitingReason
Section titled “WaitingReason”Why a workflow sits in the waiting status.
| Value | Meaning |
|---|---|
'human_approval' |
Human-in-the-loop review. |
'external_event' |
Waiting for a webhook or callback. |
'scheduled_time' |
Cron or scheduled execution. |
'rate_limit' |
API rate limiting. |
'resource_limit' |
System resource constraints. |
Action
Section titled “Action”A reducer action: a discriminated payload keyed by action type, plus identity and observability metadata.
| Field | Type | Description |
|---|---|---|
id |
string (UUID) |
Unique action identifier. |
type |
ActionType |
Which action this is. |
payload |
type-specific | Validated against the schema for type. |
idempotencyKey |
string |
Deduplication key that prevents re-execution on retry or resume. |
compensation |
{ type, payload } |
Optional compensating action for saga rollback. |
metadata |
object |
Observability metadata: node_id, timestamp, attempt, and optional duration_ms, model, token_usage. |
ActionType is the string union of the seven public action types listed under Action types. Internal engine actions (prefixed _) form a separate InternalActionType union reserved for the engine.
CompensationEntry
Section titled “CompensationEntry”One frame of the compensation_stack, pushed by nodes with requires_compensation and drained LIFO on saga rollback.
| Field | Type | Description |
|---|---|---|
action_id |
string |
The forward action this compensates. |
compensation_action |
{ type: string, payload: Record<string, unknown> } |
The compensating action to dispatch on rollback. |
TaintMetadata
Section titled “TaintMetadata”Provenance of the untrusted data behind one memory key. Keyed by memory key inside TaintRegistry. See Taint Tracking for the propagation model.
| Field | Type | Description |
|---|---|---|
source |
'mcp_tool' | 'custom_tool' | 'tool_node' | 'agent_response' | 'derived' | 'retrieval' | 'a2a' |
Origin of the data. |
tool_name |
string? |
Tool that produced it, for tool sources. |
server_id |
string? |
MCP server, or registered A2A server, that provided it. |
agent_id |
string? |
Agent that produced it, for 'agent_response'. |
node_id |
string? |
Node that introduced the data. |
derived_from |
string[]? |
Tainted keys this value was derived from. |
bytes |
number? |
Serialized size of the value. |
created_at |
string |
ISO 8601 timestamp. |
TaintRegistry is Record<string, TaintMetadata>, keyed by memory key.
LessonProvenanceEntry
Section titled “LessonProvenanceEntry”One record of which retrieved facts entered a node’s prompt. Accumulated append-only into lesson_provenance and read with getInjectedFactIds.
| Field | Type | Description |
|---|---|---|
node_id |
string |
Node whose prompt received the facts. |
agent_id |
string? |
Agent that ran the node. |
fact_ids |
string[] |
Retrieved fact IDs injected into the prompt. |
retrieved_at |
string |
ISO 8601 timestamp. |
LessonProvenanceRegistry is Record<string, LessonProvenanceEntry>, keyed by a per-entry UUID.