Core Concepts
States, transitions, guards, and how enforcement actually works
Core Concepts
Statewright is a state machine for an agent's work. Its managed integrations enforce tool policy on the paths they control. An unmanaged host can still expose native tools outside that boundary, so inspect your host's integration guide before relying on a workflow as a tool sandbox.
How Enforcement Works
The gateway checks calls made through Statewright. Managed host hooks also check native tool calls they can intercept. If a controlled tool is not listed in the current state's allowed_tools, the integration rejects it before execution.
This is not prompt engineering. The agent receives a structured error explaining what's available and how to transition.
On an enforced path, calls to unlisted tools are rejected before execution. After max_iterations controlled tool calls, the agent must transition or get blocked. Transition guards evaluate conditions programmatically against context data, not against the agent's stated intent.
Managed shell hooks screen common write patterns when Bash is allowed but Write/Edit are not: redirects (>, >>), in-place edits (sed -i), and destructive commands (rm, shred). allowed_commands can add prefix-matched command restrictions. Do not assume this covers every shell construct or a host tool outside Statewright's control.
States
A state defines what the agent can do right now.
{
"planning": {
"allowed_tools": ["Read", "Grep", "Glob"],
"instructions": "Understand the codebase. Do not modify files.",
"max_iterations": 10,
"on": {
"READY": "implementing",
"FAIL": "failed"
}
}
}allowed_tools controls which tools the agent can call. The instructions field gets injected into the agent's context each turn (use it for phase-specific guidance). Set max_iterations to force a decision point after N tool calls, and on maps event names to their target states.
If allowed_tools is omitted, Statewright applies no tool restrictions for that state. The host may still enforce its own permissions.
Transitions
The agent triggers transitions by calling the statewright_transition MCP tool with an event name and optional context data:
// MCP tool call: statewright_transition
{ "event": "READY", "data": { "rationale": "Bug found in line 42" } }If the event exists in the current state's on map, the machine moves. If not, the call is rejected and the agent stays put. The data.rationale field is stored in run history for the audit trail.
Interrupts
The agent edits a database migration file mid-implementation and keeps going without validating it. Three states later, the migration is broken and everything built on top of it is wrong. Interrupts prevent this.
When the agent changes a file matching a glob pattern, the workflow automatically detours to a handler state. After validation, the agent returns to where it was.
{
"interrupts": {
"pb_check": {
"trigger": { "file_pattern": "site/pb/**/*.js" },
"target": "pb_validating"
}
}
}The handler state uses $return as its success target to go back to the interrupted state:
{
"pb_validating": {
"allowed_tools": ["Bash"],
"allowed_commands": ["task test:hooks"],
"on": { "VALIDATED": "$return", "FAIL": "failed" }
}
}The gateway auto-transitions the agent and changes its available tools. The tool result includes a notice explaining the state change. No additional MCP tools are registered.
Fork/Join
Some tasks have independent steps that can run in parallel: lint, type-check, security scan. Fork transitions spawn multiple branches, each with its own state and tools. When all branches complete, the workflow joins and advances.
{
"on": {
"BUILD_DONE": {
"fork": {
"branches": {
"lint": { "initial": "lint_run", "terminal": "lint_done" },
"types": { "initial": "types_run", "terminal": "types_done" }
},
"join": "all",
"on_complete": "deploying",
"on_fail": "failed"
}
}
}
}Branch states are defined inline — same workflow, no sub-machines. The agent works each branch sequentially by default, or spawns parallel sub-agents with Claude Code's Agent tool. If any branch fails and the join strategy is all, the fork transitions to on_fail.
How the Agent Knows When to Transition
A common question: where does the agent learn which events to emit and when?
Two things work together. First, each state's instructions field tells the agent what to do in this phase and what outcomes to look for. Second, the statewright hook injects the full state context into every prompt turn, including the available transition event names and their targets.
What the agent sees on every turn (injected automatically):
Phase: testing. Tools: Read, Bash.
Transitions: DEPLOY -> deploying, FAIL -> failed.
Instructions: Run the test suite. If all tests pass, transition DEPLOY
with test_result: "pass". If any test fails, transition FAIL.The agent reads this, does its work, and when it decides the conditions are met, calls statewright_transition(event='DEPLOY', data={...}). The engine then evaluates any guards on that transition deterministically.
The key distinction: the agent requests a transition based on the instructions. The engine decides whether the transition is valid based on guards and the current state. The agent can try to skip ahead — the engine won't let it.
Guards
Guards add conditions to transitions. A guarded transition only fires if the condition passes.
{
"on": {
"DEPLOY": {
"target": "deploying",
"guard": "tests_passed"
}
}
}{
"guards": {
"tests_passed": {
"field": "test_result",
"op": "eq",
"value": "pass"
}
}
}The agent passes context when transitioning: { "event": "DEPLOY", "data": { "test_result": "pass" } }. The guard evaluates against the machine's context. If test_result doesn't equal "pass", the transition is blocked.
Available operators: eq, neq, gt, gte, lt, lte, in, contains, exists, not_exists.
Guards evaluate against the context that existed before the current transition — not the data being passed with it. If you need a guard to check a value, set that value in a previous transition's data field.
Context
The state machine carries a context object from state to state. It starts with whatever context is defined at the top level of your workflow JSON (defaults to {}). Each transition's data merges into context after the transition succeeds.
// Workflow defines initial context
{ "context": { "test_result": "pending", "attempts": 0 } }
// Agent transitions with data
{ "event": "TEST_DONE", "data": { "test_result": "pass", "attempts": 1 } }
// Context is now: { "test_result": "pass", "attempts": 1 }
// Guards in the next state can check these valuesContext is how programmatic state (test results, coverage numbers, approval flags) flows through the workflow. Guards read from it. Agents write to it via transition data.
Final States
A state with "type": "final" ends the workflow. Enforcement deactivates. All tools become available.
{
"completed": { "type": "final" },
"failed": { "type": "final" }
}Two final states is a common pattern — one for success, one for failure. Both end enforcement, but the distinction shows up in run history. Why two? Because "the agent gave up" is different from "the agent finished." Your run history should reflect that.
Schema reference has every field. Create Your Own walks through building one from scratch.