Capabilities and orchestrators
You have a workflow that runs your bugfix loop end-to-end. Parse the error log, reproduce, localize, plan the fix, edit, verify, review, open the PR. It works.
Now you start writing the feature-delivery workflow. Different first step — read the brief, draft a plan, vet it — but the back half is the same. Edit, verify, review, open the PR. Same again for safe refactor. Same again for any other lifecycle that ends in “ship a diff to a base ref.”
You can copy-paste the four shared states across three workflows. Then you fix a bug in one copy and forget the other two. Or you extract them into a subroutine you write once and call from everywhere — the same instinct you’d reach for in any codebase.
That subroutine is flowgate’s capability.
The two tiers
Section titled “The two tiers”There are two workflow shapes, and the cleanest way in is the function analogy:
- A capability is a function. It has a typed signature — declared inputs and outputs — and a body. It can be called, but it can’t call other workflows. It’s the unit of reuse.
- An orchestrator is a program that calls those functions. It wires capabilities together into a lifecycle — read the brief, draft, vet, edit, verify, review, open the PR — and declares its own entry signature.
Both run on the same workflow runtime you already know; they’re just two role conventions on top of it. In YAML, a capability is named cap.<verb>.<name> and an orchestrator is named flow.<name>.
A capability
Section titled “A capability”workflows: cap.plan.vet: verb: plan lifecycle: experimental snippet: inputs: plan: { type: object, required: true } max_iterations: { type: integer, default: 3 } outputs: verdict: { type: string, enum: [pass, fail, needs-revision] } findings: { type: array } skills: [compose.plan.vet] initialState: ready states: ready: goal: Vet the plan adversarially; cite each finding. transitions: submit_vet: target: done actor: agent inputSchema: type: object required: [verdict] properties: verdict: { type: string, enum: [pass, fail, needs-revision] } findings: { type: array, default: [] } executor: { kind: noop } output: verdict: "$.arguments.verdict" findings: "$.arguments.findings" done: { terminal: true }The id follows cap.<verb>.<name>, and the loader enforces that shape. The verb: (plan, here) names what kind of work the capability does, drawn from a fixed list.
A quick orientation, because flowgate has three closed verb sets and it’s easy to conflate them — one set per kind of thing:
- Skills draw from the cognitive verbs (10).
- Scripts draw from the script verbs (12).
- Capabilities draw from the cap verbs (24).
Each set is closed: the verb you pick must come from its set, and the loader rejects anything else. The verb: on a capability is a cap verb.
The snippet: block is the capability’s typed signature — its inputs and outputs, like a function declaration:
inputs:— the shape of the data the capability accepts. Required fields, defaults, JSON-schema constraints.outputs:— the shape of the data it produces. The runtime validates every output value against this schema before returning. A bad output aborts withcap.output.schema_violation; nothing partial leaks to the caller.
The body is an ordinary state machine — the same primitive as any other flowgate workflow. How the work actually runs depends on the kind of capability:
- Cognitive caps use
kind: noopwithactor: agentandskills: [<subject>]: the agent driving the gateway reads the skill guidance and submits the typed output as arguments. - Deterministic caps use
kind: script, bound to your scripts library. - Coordination caps (
gate,coordinate) bind tokind: humanfor a human-in-the-loop step, orkind: mcpfor an external side effect.
An orchestrator invoking it
Section titled “An orchestrator invoking it”workflows: flow.add-feature: inputs: feature_brief: { type: string, required: true } base_ref: { type: string, default: main } initialState: drafting_plan states: drafting_plan: transitions: draft: target: vetting_plan actor: deterministic executor: kind: workflow definitionId: cap.plan.draft use: inputs: brief: "$.context.feature_brief" outputs: "$.context.draft_plan": plan
vetting_plan: transitions: vet: target: vetted_plan actor: deterministic executor: kind: workflow definitionId: cap.plan.vet use: inputs: plan: "$.context.draft_plan" max_iterations: 3 outputs: "$.context.vet_verdict": verdict
vetted_plan: transitions: advance: target: implementing actor: deterministic guards: - kind: expr expr: "$.context.vet_verdict == 'pass'" abort: target: failed actor: deterministic guards: - kind: expr expr: "$.context.vet_verdict == 'fail'"
# ... implementing → verifying → reviewing → opening_pr → doneAn orchestrator (flow.<name>) declares no snippet: and no verb: — it’s an entry point, not a callable function. Its inputs: block is the orchestrator’s own entry signature, and those inputs seed the workflow’s shared state (its context) that downstream type checks resolve paths against.
Every capability invocation goes through a kind: workflow executor with a use: binding:
use.inputs— for each capability input, the path into the orchestrator’scontext(or a literal) that supplies the value.use.outputs— for each declared capability output, the path incontextwhere the returned value lands.
Each capability runs against its own private context. Whatever it writes internally — intermediate state, partial work, sensitive scratch — stays scoped to that run. Only the outputs you bind in use.outputs propagate back to the caller. That isolation is also what makes parallel calls safe: two concurrent invocations of the same capability run against independent context, so neither can clobber the other’s state.
The split-state branching pattern
Section titled “The split-state branching pattern”Notice the orchestrator above uses two states where you might expect one: vetting_plan runs the capability, then vetted_plan branches on its output. That’s because guards evaluate before a transition’s executor fires — so to branch on a capability’s output, you have to land in a state that runs after the capability completed.
vetting_plan ← state that fires the cap ↓ (cap writes vet_verdict)vetted_plan ← state whose guards read vet_verdict ↓ ↓advance abort ↓ ↓implementing failedIt’s a tiny bit verbose; it works. The cognitive-architectures library ships every lifecycle orchestrator in this shape, so it’s the pattern you’ll see everywhere.
What the runtime enforces
Section titled “What the runtime enforces”Most of what would otherwise surface as a hard-to-debug runtime error is caught at load time instead:
- Contract shape — a capability must declare a
snippet:block with bothinputs:andoutputs:, each a valid JSON schema. - Executor fits the verb — the executor has to match the verb’s category. Cognitive verbs need
kind: nooporkind: mcp; deterministic verbs needkind: scriptorkind: mcp;gateneeds a human actor;coordinateneedskind: mcp. - No nesting — capabilities can’t invoke other workflows and orchestrators can’t invoke other orchestrators. Leaves stay leaves.
- Bindings are present and resolvable — every
kind: workflowcall into acap.*definition needs ause:binding; a bare call is rejected. Everyuse.inputspath must resolve to something — an orchestrator input or a value some earlier state writes — and when two states write the same path, their schemas must match. - Pinned contracts — a capability marked
stablemust be invoked against a matching contract hash, so a callee can’t drift out from under its callers unnoticed.
At runtime, every output is schema-validated, and an abnormal termination returns a structured error with no partial result projected back.
The point of all this: change a capability’s output type or rename an enum value, and every dependent orchestrator surfaces a load-time error pointing at the exact caller/callee mismatch — not a surprise in staging. See Validation rules for the full catalog.
Why this matters
Section titled “Why this matters”The cap.coordinate.pr-open capability lives in one file. flow.add-feature, flow.bugfix-from-error-log, and flow.safe-refactor all invoke it. Improve it once — better PR body templating, a regression-test attachment, a different label scheme — and every orchestrator that depends on it picks up the improvement. Break its contract and they all refuse to load until you fix the callers.
That’s the same earned-move guarantee flowgate gives a single workflow, now extended across composition: a capability is your subroutine primitive, and the wiring between caller and callee is checked, not hoped for. And it composes with enforcement the same way — an orchestrator you run through Claude Code or Cursor governs every move it routes, and run through flowgate’s own loop the wrong move isn’t merely refused, it’s never offered. How to run it lays out the three modes and what each enforces.
Where the caps live
Section titled “Where the caps live”You can ship caps + orchestrators in your own gateway config, or load them from a sibling repo via the multi-repo loader. The cognitive-architectures library ships a set of reusable caps and lifecycle orchestrators for the most common engineering surfaces (flow.add-feature, flow.bugfix-from-error-log, flow.safe-refactor, flow.triage-issue, flow.evidence-driven-convergence). The flowgate-meta library ships meta-orchestrators that author capabilities and orchestrators themselves.
See Multi-repo loading for how operators wire repos into their gateway.
What’s next
Section titled “What’s next”- Multi-repo loading — how to compose libraries into your gateway.
- Self-authoring with flowgate-meta — the meta-orchestrators that use the framework on itself.
- Cap verbs — the 24 verbs a capability can declare.
- Validation rules — the full load-time and runtime check catalog.