Statewright

Core Concepts

States, transitions, guards, and how enforcement actually works

Core Concepts

Statewright is a state machine that sits between your agent and its tools. Every tool call passes through the machine. If the current state doesn't allow the tool, the call is blocked.

How Enforcement Works

The MCP gateway intercepts every tool call. Before the tool executes, the gateway checks the current state's allowed_tools list. If the tool isn't listed, the gateway returns an error to the agent... the tool never runs.

This is not prompt engineering. The agent receives a structured error explaining what's available and how to transition.

Calls to unlisted tools are rejected before execution. After max_iterations tool calls, the agent must transition or get blocked. And transition guards evaluate conditions programmatically against context data, not against the agent's stated intent.

When Bash is allowed but Write/Edit are not, statewright still blocks bash commands that look like writes: redirects (>, >>), in-place edits (sed -i), destructive ops (rm, shred). It also supports allowed_commands for prefix-matched command whitelisting. This catches the common patterns agents use to bypass tool restrictions through shell.

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, all tools pass through. There are no tool restrictions for that state.

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.

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.

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 values

Context 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.

On this page