Capabilities and orchestrators

Praxec's two-tier composition model: typed capability sub-workflows you compose into lifecycle orchestrators. Write a capability once. Use it everywhere.

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 praxec’s capability.

The two tiers

There are two workflow shapes, and the cleanest way in is the function analogy:

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

# capabilities/cap.plan.vet.yaml
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 praxec has three closed verb sets and it’s easy to conflate them — one set per kind of thing:

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:

The body is an ordinary state machine — the same primitive as any other praxec workflow. How the work actually runs depends on the kind of capability:

An orchestrator invoking it

# orchestrators/flow.add-feature.yaml
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 → done

An 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:

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

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          failed

It’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

Most of what would otherwise surface as a hard-to-debug runtime error is caught at load time instead:

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

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 praxec 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 praxec’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

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 praxec-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