Statewright

Workflow Schema Reference

Every field in the Statewright workflow definition schema

Workflow Schema Reference

Workflow definitions are JSON documents conforming to the schema at https://statewright.ai/workflow-schema.json.

Validate your definitions with $schema:

{
  "$schema": "https://statewright.ai/workflow-schema.json",
  "id": "my-workflow",
  "initial": "planning",
  "states": { ... }
}

Top-Level Fields

FieldTypeRequiredDescription
idstringYesUnique workflow identifier
initialstringYesName of the starting state
statesobjectYesState definitions keyed by state name
contextobjectNoInitial context values for guard evaluation
guardsobjectNoNamed guard predicates for conditional transitions
interruptsobjectNoFile-pattern triggers that auto-transition to handler states
metaobjectNoMachine metadata for the agent layer

Minimal Example

{
  "id": "review",
  "initial": "reading",
  "states": {
    "reading": {
      "allowed_tools": ["Read", "Grep"],
      "on": { "DONE": "complete" }
    },
    "complete": { "type": "final" }
  }
}

Full Example with Context and Guards

{
  "id": "deploy-pipeline",
  "initial": "testing",
  "context": {
    "test_result": null,
    "coverage": 0
  },
  "states": {
    "testing": {
      "allowed_tools": ["Read", "Bash"],
      "allowed_commands": ["pytest", "npm test"],
      "on": {
        "DEPLOY": {
          "target": "deploying",
          "guard": "tests_passed"
        },
        "FAIL": "failed"
      }
    },
    "deploying": {
      "allowed_tools": ["Bash"],
      "on": { "DONE": "complete" }
    },
    "complete": { "type": "final" },
    "failed": { "type": "final" }
  },
  "guards": {
    "tests_passed": {
      "field": "test_result",
      "op": "eq",
      "value": "pass"
    }
  }
}

State Definition Fields

Each key in states maps to a state definition object.

FieldTypeDefaultDescription
type"final"—Set to "final" for terminal states. Once reached, enforcement deactivates.
allowed_toolsstring[]omitted = no enforcementTools the agent can use in this state. If omitted, all tools pass through.
instructionsstring—Natural language instructions injected into agent context each turn
max_iterationsinteger (>= 1)unlimitedMaximum tool calls before forcing a transition decision
safe_nextstring—Fallback state for unrecognized transition events
max_edit_linesinteger (>= 1)unlimitedMaximum lines per edit operation
max_files_per_stateinteger (>= 1)unlimitedMaximum files that can be edited in this state
allowed_commandsstring[]—Allowed shell command prefixes for Bash tool
blocked_envstring[]—Environment variables denied in Bash commands. Alias: deny_env
env_overridesobject—Environment variable overrides injected as context. Alias: env
modelstringclient defaultModel route requested for this state
thinking_level"high" | "medium" | "low" | "off"client defaultReasoning effort requested for this state
model_ladder(string | object)[]—Ordered managed-client recovery candidates
context_budget_bytesintegerunlimitedMaximum accumulated tool result bytes in this state
onobject{}Transition events keyed by event name

allowed_tools

Controls which tools Statewright's integration permits in this state. Names such as Read, Edit, Bash, Grep, Glob, and Write are capabilities; a host can map them to its own tool names. See the host guide for its enforcement boundary.

{
  "planning": {
    "allowed_tools": ["Read", "Grep", "Glob"],
    "on": { "READY": "implementing" }
  }
}

If allowed_tools is omitted, Statewright applies no tool restriction for that state. Statewright's own MCP tools (transition, get_state, etc.) remain available. This setting does not change the host's separate permissions.

instructions

Injected into the agent's context on every turn while in this state. Use for phase-specific guidance the agent should follow.

{
  "implementing": {
    "allowed_tools": ["Read", "Edit", "Write"],
    "instructions": "Apply the minimal fix. Do not refactor unrelated code. Keep edits under 20 lines.",
    "on": { "DONE": "testing" }
  }
}

max_iterations

Counts tool calls in the current state. When the limit is reached, the agent is forced to make a transition decision rather than continuing to loop.

{
  "planning": {
    "allowed_tools": ["Read", "Grep"],
    "max_iterations": 10,
    "on": { "READY": "implementing" }
  }
}

safe_next

Fallback when the agent emits an event that has no matching transition. Only fires on truly unknown events — explicitly defined transitions (including FAIL) are unaffected.

{
  "planning": {
    "allowed_tools": ["Read"],
    "safe_next": "implementing",
    "on": {
      "READY": "implementing",
      "FAIL": "failed"
    }
  }
}

If the agent calls statewright_transition(event='GO') and GO has no definition, the machine transitions to implementing. Without safe_next, the transition is rejected.

allowed_commands

Restricts which shell commands the agent can run via the Bash tool. Values are prefix-matched.

{
  "testing": {
    "allowed_tools": ["Read", "Bash"],
    "allowed_commands": ["pytest", "npm test", "cargo test"],
    "on": { "PASS": "complete", "FAIL": "debugging" }
  }
}

The agent can run pytest -v tests/ but not rm -rf / or git push. A trailing namespace wildcard is supported for Taskfile tasks: "task test:*" permits a single task test:unit or task test:e2e invocation while rejecting other task namespaces, shell chaining, redirection, and command substitution.

Instructions guide, allowed_commands enforce. Instructions tell the agent what to do — but the agent can choose to take shortcuts. allowed_commands removes the choice. If your interrupt handler says "run task test:hooks" in instructions but allows node -e in allowed_commands, the agent will use the shortcut. Narrow allowed_commands to only the commands whose output you'd trust as proof the check passed.

model_ladder

Managed clients can recover a turn that completed without advancing the workflow by moving through an ordered model ladder. Candidate objects may set model, thinking_level, health_url, requires_provider_switch, requires_approval, and max_active_seconds.

max_active_seconds is a safe-boundary escalation budget, not a hard wall-clock timeout. The persistent Codex supervisor accumulates monotonic time from turn/started through turn/completed for the same model and state epoch, subtracting time spent inside a task test:* command. When a completed turn leaves the workflow in the same state and the cumulative budget is exhausted, recovery starts with the next eligible candidate instead of retrying the same model. Approval waits do not run inside an active turn and therefore do not consume this budget. Clients without this capability treat the field as advisory.

blocked_env and env_overrides

Control environment variable access within Bash commands.

{
  "staging": {
    "allowed_tools": ["Bash"],
    "blocked_env": ["PROD_DB_URL", "AWS_SECRET_ACCESS_KEY"],
    "env_overrides": {
      "NODE_ENV": "staging",
      "DATABASE_URL": "postgres://localhost/staging"
    },
    "on": { "DONE": "complete" }
  }
}

blocked_env (alias deny_env) prevents the agent from reading specified variables. env_overrides (alias env) injects overrides into the agent's context.

max_edit_lines and max_files_per_state

Scope constraints on file modifications.

{
  "implementing": {
    "allowed_tools": ["Read", "Edit", "Write"],
    "max_edit_lines": 20,
    "max_files_per_state": 3,
    "on": { "DONE": "testing" }
  }
}

The agent can edit at most 3 files, with each edit operation limited to 20 lines. This prevents sprawling changes.

context_budget_bytes

Caps the total bytes of tool results the agent can accumulate in a state. Prevents runaway context from large file reads.

{
  "analysis": {
    "allowed_tools": ["Read", "Grep"],
    "context_budget_bytes": 50000,
    "on": { "READY": "implementing" }
  }
}

Transition Patterns

Transitions are defined in a state's on object. Each key is an event name, and the value is one of four patterns.

Pattern 1: Simple

Target state as a plain string. No conditions, no approval.

{
  "on": {
    "READY": "implementing",
    "FAIL": "failed"
  }
}

Pattern 2: Guarded

Object with target and one or more guards. The transition only fires if all guards pass.

{
  "on": {
    "DEPLOY": {
      "target": "deploying",
      "guard": "tests_passed"
    }
  }
}

With multiple guards (all must pass):

{
  "on": {
    "DEPLOY": {
      "target": "deploying",
      "guards": ["tests_passed", "coverage_adequate"],
      "requires_approval": true,
      "approval_message": "Validate the release checklist before deployment.",
      "approval_reviewer": { "email": "release-manager@example.com" }
    }
  }
}
FieldTypeRequiredDescription
targetstringYesDestination state
guardstringNoSingle guard name to evaluate
guardsstring[]NoMultiple guard names (all must pass)
requires_approvalbooleanNoPause for human approval before transitioning
approval_messagestringNoMessage shown to the human reviewer
approval_reviewerobjectNoAn email or user_id identifying a verified Statewright user; omitted or {} defaults to the workflow creator

Reviewers are assigned to individual transitions. A release gate can name a release manager while an engineering gate names a team lead. When the field is omitted or empty, the reviewer is the workflow creator. There is no workflow-level override or event-data override. At publication, email resolves to a user ID retained by that approval; later email changes do not transfer authority.

Approval resolution requires an active Pro, Team, or Enterprise subscription for the workflow owner. The reviewer may be a different user. Both website decisions and native terminal decisions enforce the same identity and entitlement checks. A terminal identifies the required reviewer and only offers Approve/Deny when its owning API key identifies that reviewer. Otherwise its workflow remains parked without inference while the assigned reviewer resolves it from the website's Workflow Runs page. Runtime receipts represent this as approval_requirement: {mode, required_approvals, reviewers}. Version 0.3 supports only mode: "single"; first-response and threshold policies are reserved for a future schema revision.

Non-empty invalid or unverified reviewers leave a gate unresolved; they do not fall back to the creator. Existing pending approvals without a reviewer are assigned to their workflow creator during migration. Do not deploy the new authorization contract until approval-bearing workflow definitions name their reviewers and existing unresolved gates have an explicit operator recovery plan. The gateway, website migration/hooks, and managed native client must be updated together; older receipts without decision capability never enable a native dialog.

Approval evidence packets

An approval transition can carry a small, versioned review packet in its transition data. This gives the reviewer the decision context without exposing the agent's turn history:

{
  "event": "FACTS_CONFIRMED",
  "data": {
    "evidence_packet": {
      "schema": "statewright/evidence-packet/v1",
      "title": "Client brief fact check",
      "summary": "The final brief passed source, link, and rendering checks.",
      "checklist": [
        "Confirm the named contact and role.",
        "Confirm the recommendation matches the cited evidence."
      ],
      "artifacts": [
        {
          "local_path": "docs/brief-runs/acme/final-brief.pdf",
          "label": "Final brief",
          "description": "Candidate reviewed by this gate.",
          "classification": "confidential"
        },
        {
          "local_path": "docs/brief-runs/acme/final-qa.json",
          "label": "Validation receipt",
          "classification": "internal"
        }
      ]
    }
  }
}

Managed Codex and Claude clients resolve artifact paths inside the active project root, calculate a SHA-256 digest, and upload the bytes before presenting the approval. Paths outside the project, missing files, unsupported file types, and files larger than 20 MiB fail closed. A packet supports up to eight PDF, JSON, Markdown, text, PNG, JPEG, or WebP artifacts. The host performs the upload; the model never receives an upload credential or arbitrary file-upload capability.

The Codex approval dialog links to the authenticated review page; Claude's managed supervisor opens the same page when the gate is parked. That page shows the summary, checklist, reviewer policy, immutable artifact digests, inline PDF, image, and bounded text previews, plus the structured approval context. It does not include the conversation transcript. Evidence is protected and visible only to the workflow owner and designated reviewer. Current default retention is 7 days for Free, 30 days for Pro, 90 days for Team, and long-term for Enterprise accounts; expired files are removed independently of the approval decision record.

Pattern 3: Branched (XState Pattern)

Array of guarded transitions. The engine evaluates guards in order and takes the first match.

{
  "on": {
    "EVALUATE": [
      { "target": "deploying", "guard": "coverage_high" },
      { "target": "improving", "guard": "coverage_low" },
      { "target": "failed" }
    ]
  }
}

The last entry with no guard acts as the default branch. If no guard matches and there is no default, the transition is rejected.

Each branch is an object:

FieldTypeRequiredDescription
targetstringYesDestination state
guardstringNoGuard name to evaluate
guardsstring[]NoMultiple guard names (all must pass)

Pattern 4: Invoke

Delegate to a sub-machine, then resume at on_complete.

{
  "on": {
    "RUN_TESTS": {
      "invoke": "test-suite",
      "on_complete": "deploying",
      "on_fail": "debugging",
      "input": { "suite": "integration" }
    }
  }
}
FieldTypeRequiredDescription
invokestringYesSub-machine definition to invoke
on_completestringYesState to transition to on success
on_failstringNoState to transition to on failure
inputobjectNoInput data for the sub-machine's initial context

Pattern 5: Fork

Some phases have independent steps that don't depend on each other's output. Fork transitions spawn multiple branches, each with its own state and tool restrictions. The parent advances when the join condition is met.

{
  "on": {
    "BUILD_DONE": {
      "fork": {
        "branches": {
          "lint": { "initial": "lint_run", "terminal": "lint_done" },
          "types": { "initial": "types_run", "terminal": "types_done" },
          "security": { "initial": "security_run", "terminal": "security_done" }
        },
        "join": "all",
        "on_complete": "deploying",
        "on_fail": "failed"
      }
    }
  }
}

Branch states are defined inline in the same workflow. Each branch has an initial state (where execution begins) and a terminal state (a final state that signals completion).

FieldTypeRequiredDescription
fork.branchesobjectYesNamed branches, each with initial and terminal state names
fork.joinstringNoJoin strategy: all (default, AND gate) or any (race)
fork.on_completestringYesState to transition to when join condition is met
fork.on_failstringNoState to transition to if any branch fails

Fork supports two execution modes with the same schema:

Sequential (any client): The agent works each branch to completion, then calls statewright_transition(event='BRANCH_DONE:<branch_name>') to advance. After each completion, get_state returns the next branch's state and tools.

Parallel (Claude Code): The parent agent spawns sub-agents via the Agent tool, each calling statewright_load_workflow(name='ci-pipeline', branch='lint') to connect to their branch session. Each sub-agent gets independent tool enforcement. Sub-agents call BRANCH_DONE when finished; the gateway joins automatically.


Interrupts

Interrupts live at the top level of the workflow under interrupts. Each interrupt maps a file glob pattern to a handler state.

{
  "interrupts": {
    "pb_check": {
      "trigger": { "file_pattern": "site/pb/**/*.js" },
      "target": "pb_validating"
    }
  },
  "states": {
    "implementing": {
      "allowed_tools": ["Read", "Edit", "Write", "Bash"],
      "on": { "DONE": "testing", "FAIL": "failed" }
    },
    "pb_validating": {
      "instructions": "A PocketBase hook was edited. Run 'task test:hooks' to validate.",
      "allowed_tools": ["Bash", "Read", "Edit"],
      "allowed_commands": ["task test:hooks"],
      "on": {
        "VALIDATED": "$return",
        "FAIL": "failed"
      }
    }
  }
}

When the agent changes site/pb/hooks/auth.pb.js, the gateway auto-transitions to pb_validating. The agent's tools change immediately. After validation, the agent transitions VALIDATED and the $return target sends it back to whichever state was interrupted.

Interrupt Fields

FieldTypeRequiredDescription
trigger.file_patternstringYesGlob pattern matched against changed file paths
targetstringYesState to transition to when the pattern matches

Behavior

RuleDetail
Fires fromAny non-final state when a matching file is changed via Edit or Write tools. Bash file writes (>, sed -i) do not trigger interrupts.
Re-entrancySuppressed until the agent returns via $return. Editing another matching file while in the handler does not fire a second interrupt.
Agent noticeThe tool result includes a [STATEWRIGHT INTERRUPT] message explaining the state change

The $return Target

Use $return as a transition target in interrupt handler states. It resolves to whichever state the agent was in when the interrupt fired. Only valid while an interrupt is active — using $return without a prior interrupt trigger returns an error.

In the example above, VALIDATED uses $return to resume at the interrupted state, while FAIL targets failed directly. You can mix $return with regular targets in the same state.

Glob Patterns

PatternMatches
*.jsJS files with no directory separators (app.js yes, src/app.js no)
**/*.jsJS files at any depth
site/pb/**/*.jsJS files anywhere under site/pb/
**/*.env*.env, .env.local, .env.production at any depth
src/?.rsSingle-character Rust files in src/

Guard Definitions

Guards live at the top level of the workflow under guards. Each guard checks a value in the state machine's context.

{
  "guards": {
    "tests_passed": {
      "field": "test_result",
      "op": "eq",
      "value": "pass"
    },
    "coverage_adequate": {
      "field": "coverage",
      "op": "gte",
      "value": 80
    }
  }
}
FieldTypeRequiredDescription
fieldstringYesContext field to check
opstringYesComparison operator
valueanyNoValue to compare against (type depends on operator)

Guard Operators

OperatorDescriptionExample
eqEqual{"field": "status", "op": "eq", "value": "pass"}
neqNot equal{"field": "status", "op": "neq", "value": "fail"}
gtGreater than{"field": "coverage", "op": "gt", "value": 80}
gteGreater than or equal{"field": "coverage", "op": "gte", "value": 80}
ltLess than{"field": "errors", "op": "lt", "value": 5}
lteLess than or equal{"field": "errors", "op": "lte", "value": 0}
inValue is in a list{"field": "env", "op": "in", "value": ["staging", "prod"]}
containsField contains value{"field": "tags", "op": "contains", "value": "approved"}
existsField exists and is not null{"field": "review_id", "op": "exists"}
not_existsField is missing or null{"field": "error", "op": "not_exists"}

Guards read from context. The agent writes to context via the data parameter of statewright_transition:

statewright_transition(event='DEPLOY', data={test_result: 'pass', coverage: 92})

Guard Timing

Guards evaluate against the context that existed before the current transition, not the data being passed with it. If you put a guard on TESTS_GREEN that checks test_result == "pass" and the agent passes {test_result: "pass"} in the same call... the guard fails, because the data hasn't merged yet.

Data from statewright_transition(event, data) merges into context after the transition completes (regardless of whether that transition has guards). That merged data is then available for guards on subsequent transitions.

To use guards effectively, set the guard field in one transition and check it in the next:

{
  "states": {
    "implementing": {
      "on": {
        "TESTS_GREEN": "refactoring"
      }
    },
    "refactoring": {
      "on": {
        "CLEAN": {
          "target": "pre_deploy",
          "guard": "tests_still_pass"
        }
      }
    }
  },
  "guards": {
    "tests_still_pass": {
      "field": "test_result",
      "op": "eq",
      "value": "pass"
    }
  }
}

Step by step:

  1. Agent is in implementing. Tests pass. Agent calls statewright_transition(event="TESTS_GREEN", data={test_result: "pass"}).
  2. TESTS_GREEN has no guard, so it fires. Data merges: context.test_result is now "pass".
  3. Agent is now in refactoring. Does cleanup work. Calls statewright_transition(event="CLEAN").
  4. CLEAN has guard tests_still_pass. Guard checks context.test_result == "pass" (set in step 2). Passes.

If the agent skipped step 1 and tried to jump straight to CLEAN with data={test_result: "pass"}, the guard would fail because it checks pre-transition context where test_result is still null.


Workflow Operations

Pause and Resume

statewright_pause saves the current state and context to the run record and deactivates enforcement. The run status changes to paused.

To resume in a new session:

statewright_load_workflow(name="tdd-feature", resume=true)

This finds the latest paused run for that workflow, restores the saved state and context, and sets the run back to running. Iteration count resets to 0, but transition count and context are preserved.

If no paused run exists, resume=true falls back to starting fresh from the initial state.

Debug Mode

Set "debug": true in the workflow meta to enable statewright_force_state. This jumps to any state and optionally merges context values (not a full replace, it patches on top of existing context).

statewright_force_state(state="refactoring", context={test_result: "pass", build_ok: true})

Guards, transitions, and iteration counts are all bypassed. Use this for development recovery when you need to fix forward after a crash, or to skip past states you've already completed in a prior session. Not for production workflows.


Meta Fields

Optional metadata at the top level under meta. Used by the agent layer and observability tooling, not by the state machine engine itself.

{
  "id": "deploy-pipeline",
  "initial": "testing",
  "meta": {
    "enabled": true,
    "task_type": "deployment",
    "estimated_steps": 15,
    "danger_level": "dangerous",
    "requires_human_approval": true,
    "capture_output": true
  },
  "states": { ... }
}
FieldTypeDescription
enabledbooleanControls whether the workflow can be listed, loaded, or resumed. Defaults to true when omitted. Set false to retire a workflow without deleting its definition or run history.
task_typestringCategorization hint (e.g., "bugfix", "deployment", "refactor")
estimated_stepsintegerRough estimate of total tool calls for the workflow
danger_level"safe" | "moderate" | "dangerous"Risk classification for the workflow
requires_human_approvalbooleanWhether the workflow requires human sign-off at some point
capture_outputbooleanEnable output capture to the run history log
approval_mode"ui" | "none"When "ui", transitions with requires_approval park for review in supported native clients and the web dashboard. Default: "none" (advisory only)
debugbooleanEnable statewright_force_state tool for jumping to arbitrary states. For development and recovery.

Additional arbitrary fields are allowed under meta for forward compatibility.

Retiring a workflow

Set meta.enabled to false when a workflow should remain stored for audit or later recovery but must not be selectable by agents:

{
  "meta": {
    "enabled": false
  }
}

Disabled workflows are omitted from statewright_list_workflows and rejected by statewright_load_workflow, including resume attempts. A workflow already running when it is disabled is not terminated. Use statewright_get_workflow to retrieve the complete stored definition, then preserve every existing field when calling statewright_create_workflow(overwrite=true) to disable or re-enable it. The database record's Default setting selects the default among enabled workflows; it does not replace meta.enabled.


Complete Reference Example

A full workflow definition using every feature:

{
  "$schema": "https://statewright.ai/workflow-schema.json",
  "id": "deploy-pipeline",
  "initial": "planning",
  "context": {
    "test_result": null,
    "coverage": 0,
    "approved": false
  },
  "meta": {
    "enabled": true,
    "task_type": "deployment",
    "estimated_steps": 25,
    "danger_level": "dangerous",
    "requires_human_approval": true,
    "capture_output": true
  },
  "states": {
    "planning": {
      "allowed_tools": ["Read", "Grep", "Glob"],
      "instructions": "Understand the change. Read tests and deployment config.",
      "max_iterations": 10,
      "context_budget_bytes": 50000,
      "safe_next": "testing",
      "on": {
        "READY": "testing",
        "FAIL": "failed"
      }
    },
    "testing": {
      "allowed_tools": ["Read", "Bash"],
      "allowed_commands": ["pytest", "npm test", "cargo test"],
      "blocked_env": ["PROD_DB_URL"],
      "max_iterations": 15,
      "on": {
        "EVALUATE": [
          { "target": "deploying", "guard": "deploy_ready" },
          { "target": "fixing", "guard": "tests_failed" },
          { "target": "failed" }
        ]
      }
    },
    "fixing": {
      "allowed_tools": ["Read", "Edit"],
      "max_edit_lines": 20,
      "max_files_per_state": 3,
      "instructions": "Fix only the failing tests. Minimal changes.",
      "on": {
        "DONE": "testing",
        "FAIL": "failed"
      }
    },
    "deploying": {
      "allowed_tools": ["Bash"],
      "allowed_commands": ["kubectl", "helm"],
      "env_overrides": { "KUBECONFIG": "/etc/kube/staging.yaml" },
      "on": {
        "DONE": {
          "target": "complete",
          "requires_approval": true,
          "approval_message": "Deployment finished. Approve to mark complete?"
        },
        "FAIL": "failed"
      }
    },
    "complete": { "type": "final" },
    "failed": { "type": "final" }
  },
  "guards": {
    "deploy_ready": {
      "field": "test_result",
      "op": "eq",
      "value": "pass"
    },
    "tests_failed": {
      "field": "test_result",
      "op": "eq",
      "value": "fail"
    }
  }
}

On this page