Quick start
You’re going to give your agent a rule it cannot break, then watch it try. In about a minute you’ll see the agent reach for an action, get refused, and watch the right move appear only once it’s earned. No prompt-wrangling — the rule is in the structure.
1. Create a config file
Section titled “1. Create a config file”Save this as ship-guard.yaml:
version: "1.0.0"workflows: ship_guard: initialState: unchecked states: unchecked: transitions: run_check: { target: checked } # the only move offered here checked: transitions: ship: { target: shipped } # `ship` exists ONLY in this state shipped: {} # terminal — doneA three-state machine. The rule lives in its shape: ship is declared only inside checked, and the only way into checked is run_check. So from unchecked, ship is not a move that exists. Right now run_check just advances the state — we make it run your real test suite below.
2. Start the gateway
Section titled “2. Start the gateway”mcp-flowgate serve --config ship-guard.yamlflowgate is now running as an MCP server over stdio, waiting for a host to connect.
3. Wire it into your coding agent
Section titled “3. Wire it into your coding agent”Declare mcp-flowgate serve as an MCP server in your host’s config. The editor guide covers each host (Claude Code, Cursor, Zed, VS Code, Claude Desktop) in depth.
For Claude Desktop on macOS, the config lives at ~/Library/Application Support/Claude/claude_desktop_config.json:
{ "mcpServers": { "flowgate": { "command": "/absolute/path/to/mcp-flowgate", "args": ["serve", "--config", "/absolute/path/to/ship-guard.yaml"] } }}Absolute paths — relative paths won’t resolve when the host launches the process.
4. Restart your host, then ask it to ship
Section titled “4. Restart your host, then ask it to ship”Restart your agent (Claude Code, Cursor, etc.). It now has exactly two tools — flowgate.query (reads) and flowgate.command (writes) — regardless of how much you wire in later.
Now ask it: “Start the ship_guard workflow and ship the change.” Here’s what crosses the wire:
# It starts the workflow:→ flowgate.command { "definitionId": "ship_guard" }← { "workflow": { "id": "wf_01H…", "state": "unchecked", "version": 1 }, "result": { "status": "started" }, "links": [ { "rel": "run_check", "method": "flowgate.command", "args": { "workflowId": "wf_01H…", "expectedVersion": 1, "transition": "run_check" } } ] }ship is not in links. It isn’t blocked — it isn’t on the menu. If the agent reaches for it anyway:
→ flowgate.command { "workflowId": "wf_01H…", "expectedVersion": 1, "transition": "ship" }← { "result": { "status": "rejected" }, "error": { "code": "INVALID_TRANSITION", "message": "Transition 'ship' is not valid from state 'unchecked'.", "attemptedTransition": "ship" }, "links": [ { "rel": "run_check", … } ] } # the refusal hands back the legal moveThe agent follows the only legal move, and ship appears:
→ flowgate.command { "workflowId": "wf_01H…", "expectedVersion": 1, "transition": "run_check" }← { "workflow": { "state": "checked", "version": 2 }, "result": { "status": "executed" }, "links": [ { "rel": "ship", … } ] } # now it's a moveThat’s the whole idea: the wrong action wasn’t discouraged, it was unreachable — and the right one became available only once it was earned. You didn’t write a prompt. You wrote nine lines of YAML.
That’s TDD —
redhas no write-implementation move until a test fails. That’s deploy-gating —deployisn’t a move until tests pass. Same machine, your labels.
5. Make the check real
Section titled “5. Make the check real”Right now run_check is a placeholder that just advances. Point it at your actual test command so the gate has teeth — add a cli connection and give the transition an executor:
version: "1.0.0"connections: shell: kind: cli command: npm # or: cargo, pytest, /bin/bash …workflows: ship_guard: initialState: unchecked states: unchecked: transitions: run_check: target: checked executor: { kind: cli, connection: shell, args: ["test"] } checked: transitions: ship: { target: shipped } shipped: {}Now run_check runs npm test. If it fails, the transition fails and you stay in unchecked — so ship never becomes reachable. The agent cannot ship past a red suite, because the move to do so only exists on the other side of a green one.
What just happened
Section titled “What just happened”The headline: the agent could only ever do what the workflow’s current state allowed. The illegal move wasn’t a thing it could call, and every response — including the refusal — handed back the moves that were legal, so it could recover on its own. That’s the guardrail, and it holds without a single line in the system prompt.
You also got, for free:
- Audit — every step (start, the rejected
ship, therun_check) emitted a structured JSON event. You have a complete, replayable trace of what the agent did. - Schema validation — malformed input is rejected before it reaches your executor.
- A fixed tool surface — the model always sees two tools, whether you have one workflow or five hundred. (That’s also where the token savings come from at scale — see the cost analysis.)
Next: add an approval gate so some moves wait for a human (actor: human — the agent literally can’t submit them), see how discovery and search keep the surface flat, or compose a small cognitive architecture with skills and multi-step workflows.