Open source · Apache-2.0

Make the wrong move unreachable.

Your coding agent shipped before the tests passed. Again. flowgate makes that move not exist: ship only appears after the check runs, so the agent literally can't reach it from unchecked. Guardrails you declare in YAML — and, because the whole thing rides on two fixed tools, a smaller prompt as a bonus.

ship-guard.yaml
version: "1.0.0"
workflows:
  ship_guard:
    initialState: unchecked
    states:
      unchecked:
        transitions:
          run_check: { target: checked }  # only move here
      checked:
        transitions:
          ship: { target: shipped }     # ship exists ONLY here
      shipped: {}                          # from unchecked, ship is not a move

The problem you already have

You told the agent to run the tests first. It ran the deploy. You told it not to approve its own PR. It approved its own PR. The system prompt is a suggestion, and Claude Code or Cursor is free to ignore it the moment the context fills up. A rule that lives in prose is a rule the model can talk itself out of.

The usual reflex is to hand the agent more tools — a tool to check CI, a tool to gate the deploy, a tool to request review. But every MCP tool you register lands in the model's system prompt. Ten tools? Fine. Fifty? You're spending 5,000+ tokens per call just to describe what's available, and the model still has to reason about all of them to pick one — more tools, more wrong choices, more wasted round trips.

So you get the worst of both: a guardrail the model can ignore, bolted onto a tool list that keeps growing. A flat list and a prayer.

The fix: two tools, and a state machine that decides what's reachable

flowgate exposes exactly two MCP tools no matter how many actions — capabilities, the things your agent can actually do — you wire in behind them. The split is reads vs. writes, so your host can auto-approve reads and gate writes. The model's tool list never grows, and what it's allowed to do next is set by the workflow's current state, not by the prompt. (For the curious: the read/write split is the CQRS pattern.)

flowgate.query all reads

Browse the catalog and search for an action by what it does.

Describe an action's full schema on demand.

Check workflow state and look up shared terms (the lexicon).

flowgate.command all writes

Start a guarded workflow.

Fire a transition to move the workflow's state forward.

Define shared terms — every write is schema-validated and audited.

Your 50 actions surface through search results and the links each response hands back — loaded one at a time, only when they're a legal next move. (For the curious: that "the response tells you what you can do next" pattern is HATEOAS.)

The guardrails, in your terms

Every one of these is YAML. No glue code, no per-tool wrappers, no host-specific routing.

Your agent can't approve its own PR

Tag the approve transition actor: "human" and the link is never offered to the model. If it tries anyway, the runtime rejects it with ACTOR_MISMATCH. Not a hint — a hard gate.

Block deploy unless CI passed

Gate a transition on evidence, permission, role, or an expr — compose them with allOf, anyOf, not. No green check, no deploy move.

Bad input never reaches the action

Define inputSchema on any action. Malformed arguments are rejected before they touch the thing that actually runs (the executor).

Skip what the model shouldn't decide

Tag steps actor: "deterministic" — lint, test, build. The runtime chains through them automatically. Zero LLM round trips for computable work.

A replayable trace of what it did

Every action emits a structured JSON event — including the moves that got refused. Route to file, stdout, or your observability stack.

Any backend

MCP servers, shell commands, REST APIs. Import existing tools with proxy.import — no rewriting.

Reliability

Timeout, retry with exponential backoff, fallback executors. Declare it in YAML, not code.

Hot reload

Send SIGHUP to reload config without restarting. In-flight workflows continue uninterrupted.

What the model actually sees

Claims deserve evidence. Here's the actual wire format when the agent tries to ship before the check has run — the ship_guard workflow.

1. The agent searches — not scans

 flowgate.query { "query": "ship the change" }

 { "items": [{
    "id": "workflow:ship_guard",
    "title": "Ship guard — check before ship"
  }] }

One hit. Not 50 tool definitions.

2. Start the workflow — it lands in unchecked

 flowgate.command {
    "definitionId": "ship_guard"
  }

 { "workflow": { "state": "unchecked", "version": 1 },
    "result": { "status": "started" },
    "links": [{
      "rel": "run_check",
      "method": "flowgate.command",
      "args": { /* prefilled */ }
    }] }

The only link offered is run_check. ship isn't in the list — from this state, it isn't a move that exists.

3. It reaches for ship anyway — and can't

 flowgate.command { "transition": "ship" }  # tries to skip the check

 { "result": { "status": "rejected" },
    "error": { "code": "INVALID_TRANSITION" },
    "links": [{
      "rel": "run_check"     # refusal hands back the legal move
    }] }

INVALID_TRANSITION — not blocked, unreachable. And the refusal hands back the one legal move, so the agent recovers on its own: run the check, and only then does ship appear.

So what's the indirection tax?

You just watched the model search before it acted. Fair question to ask next: if every capability starts with a search, what does that indirection cost? Here's the honest math — and why it stays small.

Direct tool list

1 hop

…but every tool's schema sits in the prompt, always — growing with each tool you add.

Flowgate, first use

2 hops

Search, then call. Paid once — the first time the model reaches for a capability this session.

Flowgate, after that

1 hop

The discovered link is already in context. The model reuses it — no second search.

So the tax is one search, the first time the model uses a capability in a session. After that it's free — the link lives in context and gets reused. It's a model round trip, not gateway latency; it shows up on one-shot capability calls, not workflows; and unlike a flat tool list, it never grows with your capability count.

The floor is that one first-use search — and we won't go under it by pushing a tool list back into your prompt, because that's the whole thing Flowgate removes. See the full accounting →

Skip what the LLM doesn't need to decide

Linting, testing, building artifacts — these are computable. Tag them actor: "deterministic" and the runtime chains through them automatically.

deploy-pipeline.yaml
states:
  lint:
    transitions:
      run_lint:
        target: test
        actor: deterministic
        executor: { kind: cli, command: lint-check }
  test:
    transitions:
      run_tests:
        target: build
        actor: deterministic
  build:
    transitions:
      build_artifact:
        target: ready_to_deploy
        actor: deterministic
  ready_to_deploy:
    goal: Confirm deployment
    transitions:
      deploy:
        target: deployed
        actor: agent          # chain stops here

The model calls flowgate.command. The runtime chains lint, test, build automatically. Three executor calls, zero LLM round trips. The response arrives at ready_to_deploy with a chain trace and guidance for the decision ahead.

Ready to try it?

One YAML file. Two tools. Moves your agent can't make. Free and open source.