In-runtime LLM execution
The seam this closes
Section titled “The seam this closes”Wire flowgate into Claude Code or Cursor and it governs everything you route through it — but there’s a seam. The client’s model can ignore the guidance you hand back, call its host’s own built-in shell, or skip the workflow entirely. The guardrails are real, but the agent lives in someone else’s loop, and that loop has an escape hatch. You’re trusting the client to behave.
The kind: llm executor removes the seam. Instead of handing the workflow to an external client, the runtime hosts the model call itself, in-process. The model the runtime spins up sees exactly one thing as its tool list: the transitions available at the current state. Nothing else exists to call. The wrong move isn’t refused — it’s absent.
This is the runtime mode behind Wispr and Mission Control (modes 2 and 3): the no-seam topology where flowgate owns the loop. This page is the depth on how that hosting actually works.
The tool list is the transitions
Section titled “The tool list is the transitions”This is the whole idea. When a state’s transition uses kind: llm, the runtime presents the state’s transitions to the model as its tools. The model picks exactly one per turn, and the runtime advances to the matching target. There is no general tool list, no escape hatch, no way to call something the state doesn’t allow.
states: triaging: goal: Decide whether the issue is a bug, feature request, or noise. transitions: mark_as_bug: target: investigating executor: kind: llm model: anthropic:claude-sonnet-4-6 prompt_template: | Triage this GitHub issue. The body is in the workflow's shared state (its `context`). Pick exactly one of mark_as_bug, mark_as_feature, or close_as_noise.
Issue body: {{ $.context.issue_body }} max_iterations: 3 mark_as_feature: { target: backlog } close_as_noise: { target: closed }The model is shown mark_as_bug, mark_as_feature, and close_as_noise — and picks one. {{ $.context.issue_body }} reads from the workflow’s shared state (its context) — the values that carry between states as the workflow runs, here populated with the issue body before triage. A full worked example lives at examples/issue_triager.yaml.
You cannot add a tools: field. The per-turn tool list is the workflow’s transitions, full stop — letting an author inject an arbitrary tool list would reopen the escape hatch, so the field is rejected at parse time.
Bounded by construction
Section titled “Bounded by construction”Every in-runtime call runs under caps you declare. They’re not hints to the model — they’re enforced by the runtime.
| Field | Required | What it does |
|---|---|---|
model | yes (or affinity) | The provider:model string, e.g. anthropic:claude-sonnet-4-6. The cost catalog is keyed by this exact name. Use affinity: instead (e.g. affinity: coding-frontier) to pick the model per state through models.yaml — a cheap model for the bounded steps, a frontier model for the one hard reasoning step. |
prompt_template | yes | Rendered against the workflow’s shared state ($.context.*) and its inputs ($.input.*) via the template engine. ($.blackboard.* works too — it’s an alias for $.context.*.) |
max_iterations | no (default 3) | Per-turn retry budget for malformed responses. |
max_seconds | no | Per-turn wall-clock cap. |
max_tokens | no | Per-turn token cap. |
max_cost_usd | no | Per-workflow cumulative cost ceiling (requires the model to be in the cost catalog). |
reasoning_effort | no | Provider hint: low / medium / high / xhigh. |
capture_reasoning | no (default true) | Set false to record reasoning as "<elided>" in the audit — a per-workflow privacy/compliance opt-out. |
deny_unknown_fields is set, so a typo or a forbidden field fails at config load, not silently at runtime.
Every turn is on the record
Section titled “Every turn is on the record”Each invocation emits one llm.invocation audit event — on success and on failure alike. The payload is a stable public contract; sinks can rely on the field names. It includes the resolved model, tokens_in / tokens_out / tokens_reasoning, latency_ms, cost_usd (from the cost catalog), stop_reason, the tool_call_emitted (the transition the model chose), the captured reasoning (or "<elided>"), and an error_code (LLM_*) when the turn failed. That’s your cost attribution, your latency graph, and your decision trail — for free, because the call ran inside the runtime.
When to reach for it
Section titled “When to reach for it”Use kind: llm when a step genuinely needs judgment but must stay inside hard guardrails — classify, route, decide-which-branch — and you don’t want to stand up a separate model service or trust an external client to follow the rules. Keep the external flowgate.command path (Claude Code, Cursor, your own harness) for the interactive, human-in-the-loop driving an editor does best. Both surfaces share the same workflows, guards, shared context, and audit log; you pick per state which to use.
See Executors for the full field reference and the llm.invocation payload, and Audit events for routing the event stream.