concepts
How the kernel works
One inversion drives everything: the workflow chooses capabilities, and the kernel enforces the transitions. This page walks the mechanism — legal moves, bounded contexts, typed contracts, validated state.
01 · the inversion
Why prompts don't govern
You told the agent to run the tests first. It ran the deploy. You told it not to approve its own change. It approved its own change. So you added a sentence to the prompt, and it held — until the next time pressure or a full context window let the model rationalize around it. A rule that lives in prose is a rule the model can talk itself out of.
The usual reflex is more tools: a tool to check CI, a tool to gate the deploy, a tool to request review. But every tool you register lands in the model's context, and the model has to reason across all of them to pick one. More tools, more wrong choices — and the "governance" is still just prose sitting next to the list.
Praxec inverts the arrangement. The model doesn't choose from a catalog; the workflow declares which capability each step uses. The kernel grants access scoped to that one step, runs the capability, and validates the output before the state moves. The model contributes judgment inside the steps that want it — it never owns the process.
02 · legal moves
The wrong move isn't blocked. It doesn't exist.
A workflow is a state machine, and a state machine is a map of legal moves. An action that's illegal in the current state isn't intercepted or discouraged — it simply isn't a move that exists from here. Take a nine-line workflow:
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 here
shipped: {} # terminal — doneThe rule lives in the shape: ship is declared only insidechecked, and the only way into checked isrun_check. When the workflow starts, here's what the model gets back:
→ praxec.command { "definitionId": "ship_guard" }
← { "workflow": { "state": "unchecked", "version": 1 },
"result": { "status": "started" },
"links": [ { "rel": "run_check",
"method": "praxec.command",
"args": { /* prefilled */ } } ] }One link: run_check, arguments prefilled. shipisn't in the list — from unchecked, it isn't a thing that can be called. If the model reaches for it anyway:
→ praxec.command { "transition": "ship" } # tries to skip the check
← { "result": { "status": "rejected" },
"error": { "code": "INVALID_TRANSITION",
"message": "Transition 'ship' is not valid from state 'unchecked'." },
"links": [ { "rel": "run_check" } ] } # the refusal hands back the legal moveThe refusal hands back the legal move, so the model recovers on its own — no human untangling required. That "each response carries the valid next moves" pattern is HATEOAS, the same discipline that makes the web navigable without a sitemap.
03 · bounded contexts
When a step needs a model, the model gets a box
Some steps genuinely want judgment — triage this issue, pick a remediation, draft the summary. For those, a step can host a governed LLM call with the llm executor. The key constraint: the transitions legal at the current state are the model's tool list. It picks exactly one per turn, and the kernel advances the workflow.
executor:
kind: llm
model: anthropic:claude-sonnet
prompt_template: |
Triage this issue. Pick exactly one transition.
max_iterations: 3 # retry budget for malformed turns
max_seconds: 60 # wall-clock cap per turn
max_tokens: 2000 # token cap per turn
max_cost_usd: 0.25 # cumulative cost cap per workflowEvery dimension of the context is capped: iterations, wall-clock time, tokens, cumulative cost. And there is no tools: field — the config schema rejects it, because injecting an arbitrary tool list would be a way to route around governance. The boundary isn't a convention. It's the shape of the config.
04 · typed contracts
Contracts on the way in, contracts on the way out
Every capability declares what it accepts and what it produces. Malformed input is rejected before it ever reaches the executor. And the output is checked against the declared schema before the state transition commits — a capability that returns garbage doesn't advance the workflow, it fails loudly with the violation on record.
On top of schemas, transitions can carry guards and actor rules:
transitions:
approve:
actor: human # only a human can submit this move
target: approved
guards:
- { kind: permission, permission: workflow.approve }
- { kind: evidence, evidence: ci_green } # no green check, no move- Actor rules say who may fire a move.
actor: humanmeans the link is never offered to the model, and if it submits the move anyway, the kernel rejects it. An agent cannot approve its own work — not as policy, as mechanics. - Guards are preconditions — evidence, permissions, roles, expressions — composable with
allOf/anyOf/not. No green CI check on record, nodeploymove.
05 · validated transitions
State only moves when everything agrees
Every write carries an expectedVersion. If two actors — a model and a human, or two models — race on the same workflow, the stale one is rejected with the current state attached, so it can retry from reality instead of from memory. No lost updates, no heavyweight locks.
And every attempt — the starts, the executions, the refusals — emits a structured audit event. You get a complete, replayable trace of what actually happened, including the moves that were denied. Debugging an agent stops being archaeology.
06 · deterministic chaining
Skip what the model shouldn't decide
Lint, test, build — these are computable. Mark themactor: deterministic and the kernel chains through them automatically: three real commands run, zero LLM round trips. The model is only consulted where a decision actually lives.
states:
lint:
transitions:
run_lint: { target: test, actor: deterministic }
test:
transitions:
run_tests: { target: build, actor: deterministic }
build:
transitions:
package: { target: ready, actor: deterministic }
ready:
transitions:
deploy: { target: live, actor: agent } # the chain stops hereA useful side effect: because the kernel carries the discipline, the model doesn't have to. Bounded, well-scoped steps run safely on a cheap model — you spend frontier-model money only where judgment is the actual work.
07 · the honest math
What the indirection costs
Fair question: if capabilities are discovered rather than listed, what does that indirection cost?
1 hop
…but every tool's schema sits in the prompt on every call, and grows with each tool you add.
2 hops
Search, then call. Paid once — the first time the model reaches for a capability in a session.
1 hop
The discovered link is already in context and gets reused. No second search.
So the tax is one extra round trip on first use, and the surface never grows with your capability count. Inside workflows it barely shows up at all — the legal moves arrive with every response.