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.
instructionsstringNatural language instructions injected into agent context each turn
max_iterationsinteger (>= 1)unlimitedMaximum tool calls before forcing a transition decision
safe_nextstringFallback 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_overridesobjectEnvironment variable overrides injected as context. Alias: env
context_budget_bytesintegerunlimitedMaximum accumulated tool result bytes in this state
onobject{}Transition events keyed by event name

allowed_tools

Controls which MCP tools the agent can call. Tool names must match exactly as the agent knows them (e.g., Read, Edit, Bash, Grep, Glob, Write).

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

If allowed_tools is omitted, all tools pass through — no enforcement for that state. Statewright's own MCP tools (transition, get_state, etc.) are always available regardless.

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.

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.

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": "Deploy to production?"
    }
  }
}
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

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": {
    "task_type": "deployment",
    "estimated_steps": 15,
    "danger_level": "dangerous",
    "requires_human_approval": true,
    "capture_output": true
  },
  "states": { ... }
}
FieldTypeDescription
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 in the web dashboard for human review. 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.


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": {
    "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