ADR-0019: Workflows are operator-authored, parameterised DSL artifacts agents invoke
- Status: Accepted
- Date: 2026-05-25
- Deciders: @karasu
- Supersedes: —
- Superseded by: —
Context
The motivating scenario for the entire guest tier (ADR-0017, ADR-0018) is the operator giving a non-operator a constrained way to accomplish a specific job — adding a testimonial to a static site, drafting a blog post, kicking off a deploy. The pattern is the same each time: an operator-authored recipe the agent executes against operator-supplied parameters from the invoker.
The naive way to express this is "let the guest chat with the primary agent, with tool access restricted." That fails on three counts:
- The agent has no guard rails for what the guest can ask it to do. A photographer's testimonial request and a deploy-the-site request are very different in risk and shape, but a free-form chat treats them identically.
- The required inputs are not declared. Without an input schema, the agent has to negotiate them in conversation every time. That's fine for an operator pairing with their agent; it's terrible UX for a guest who knows exactly what they want.
- The prompt that frames the work is buried in operator memory. "How does the agent know to deploy after editing?" becomes folklore. The operator has to retype the framing every time, or hope the project's primary system prompt covers all cases.
What we want is a named, parameterised, prompt-bound recipe that:
- Lives in the project DSL (portable, version-controlled, reviewable).
- Has a declared set of required and optional inputs (so the invoker is asked for exactly what's needed, in a structured way).
- Has a system-prompt fragment that frames the agent's task for the duration of this invocation.
- Has a tool allowlist that intersects with the project's overall tool config (so a workflow can be more restrictive than the project, but never broader).
- Is invokable by the primary agent on behalf of the operator or a guest, with the same lifecycle either way.
There is a naming collision to handle. The project already uses two terms that look like candidates:
tasks:— operator-initiated shell commands pertask-runner.md. Not the same thing. Shell commands, not agent recipes. Name stays.- Mastra
Workflow— the substrate's control-flow primitive withsuspend/resumeper ADR-0012. Implementation detail, not operator-facing. Used by the harness; doesn't appear in the DSL.
We use workflow as the operator-facing word. It's the noun a layperson would use ("there's a workflow for adding testimonials"), and it doesn't conflict at the layer the operator sees. The fact that Mastra has its own internal type called Workflow is harness business — operators don't see Mastra types, per ADR-0012 and ADR-0014.
Decision
The project DSL gains a top-level
workflows:block — a named-object map (ADR-0015 shape) where each entry declares a parameterised, prompt-bound recipe with a required-inputs schema and a tool allowlist. Workflows are invokable by the primary agent on behalf of operators (via session UI) or guests (via the guest UI, gated by ADR-0018 grants). The harness compiles each invocation into a constrained agent run with the workflow's prompt appended, inputs bound, and tools intersected.
DSL shape
version: 1
project: photographer-site
# ... primary, subagents, tasks, tools ...
workflows:
testimonial.add:
description: Add a client testimonial to the site
system_prompt: ./workflows/testimonial-add.md
inputs:
name:
type: string
required: true
max_length: 80
quote:
type: string
required: true
max_length: 500
photo:
type: file
required: true
accept: ["image/jpeg", "image/png"]
max_size_kb: 2048
link:
type: string
required: false
pattern: "^https?://"
tools:
allow: ["file.read", "file.write", "image.optimize", "git.commit", "build.run", "deploy.run"]
confirm_required: true
invokable_by: [operator, guest]
blog.draft:
description: Draft a blog post from a topic and an outline
system_prompt: ./workflows/blog-draft.md
inputs:
title:
type: string
required: true
outline:
type: string
required: true
tools:
allow: ["file.read", "file.write", "search.web"]
invokable_by: [operator]
The workflows field is a named-object map per ADR-0015, keyed by workflow name (dot-delimited, mirroring tools: conventions). Values are WorkflowDefinition objects or null (nullification, per ADR-0015 federated semantics).
WorkflowDefinition schema
| Field | Type | Required | Meaning |
|---|---|---|---|
description |
string | yes | One-line human-readable description. Shown to operators in UI and to guests on the invocation form. Max 280 chars. |
system_prompt |
path | yes | Project-root-relative path to a Markdown file containing the workflow's system prompt fragment. Validated per ADR-0011 path rules. |
inputs |
object | yes | Named-object map of input parameters. May be empty ({}) for a workflow that takes no inputs. |
tools.allow |
string[] | yes | Tool names the workflow is permitted to use. Must be a subset of the project's enabled tools (per tools: block, ADR-0015). |
tools.deny |
string[] | no | Additional explicit denials, useful in conjunction with glob allow patterns. |
confirm_required |
bool | no | If true, guests are shown a confirmation step before the workflow runs. Default false. Operators may always skip via UI. |
invokable_by |
enum[] | no | Subset of ["operator", "guest"]. Default ["operator"]. A workflow with guest in this list is eligible for guest grants; the actual grant lives in project_guest_grants. |
timeout_seconds |
int | no | Max wall-clock for the invocation. Default 600 (10 minutes). |
cage_overrides |
object | no | Per-invocation cage tightening. Cannot loosen project cages; can only narrow. Reserved for v1.x; not implemented in v1. |
Input schema
Each inputs.<name> is a small Zod-shaped schema:
| Field | Type | Required | Meaning |
|---|---|---|---|
type |
enum | yes | One of string, integer, number, boolean, file, url. |
required |
bool | no | Default true. |
description |
string | no | Shown in the invocation form's field label. |
max_length / min_length |
int | no | For strings. |
min / max |
number | no | For integer / number. |
pattern |
string | no | Regex (re2-compatible) for strings and urls. |
accept |
string[] | no | MIME types for files. |
max_size_kb |
int | no | For files. |
enum |
string[] | no | Restricted set of allowed values. |
default |
(matches type) | no | Default value when not supplied. |
Inputs are validated at invocation time against this schema. Validation failure returns a structured error to the invoker (operator UI or guest UI) before the agent starts.
File inputs
File-typed inputs are uploaded via a separate endpoint (POST /api/v1/projects/:slug/workflows/:name/upload) which streams the file to a workflow-scoped staging area, returns a token, and that token is what's submitted with the rest of the inputs. The agent reads the file from staging using the standard file.read tool against the staging path. Files are cleaned up after the invocation completes (or after a 1-hour TTL on abandoned uploads). Detailed in specs/workflows.md.
Invocation lifecycle
- Invoker (operator or guest) selects a workflow.
- UI renders an input form generated from the
inputsschema. - Invoker submits. Daemon validates inputs against the schema.
- Daemon creates a workflow run — a special session type with
kind: "workflow",workflow_name, frozen inputs, and a reference to the invoking principal. - Harness composes the agent config:
instructions= project's primary system prompt + workflow's system prompt fragment (concatenated with a clear delimiter).tools= intersection of project tool config and workflowtools.allow/tools.deny.- Inputs are injected as a structured first message:
<workflow_inputs>...</workflow_inputs>.
- Primary agent runs. Streaming output goes to the invoker's UI in real-time per ADR-0016.
- Completion. Run ends with
succeededorfailed. Result is persisted to the session record. - Audit log captures every step with the invoker's
user_id.
Prompt composition
The agent's system prompt at invocation time is:
[project primary prompt content]
---
### Workflow: testimonial.add
[contents of ./workflows/testimonial-add.md]
### Workflow inputs
name: "Cara McGee"
quote: "They captured our day perfectly."
photo: /workflow-staging/<run_id>/photo.jpg
link: (omitted)
The composition is operator-readable: the full prompt is visible in the run's debug view, per the manifesto principle that no system prompt is hidden. The AuditProcessor logs the composed prompt per specs/agent.md.
Federated config composition
project.local.yaml may override or nullify workflows from project.yaml, per ADR-0015:
# project.local.yaml
workflows:
testimonial.add:
tools:
allow: ["file.read", "file.write", "image.optimize"] # narrower than project.yaml; no deploy
blog.draft: null # disable this workflow on this operator's machine
Operator-local overrides are useful for:
- Disabling workflows during development without touching the portable DSL.
- Narrowing tool allowlists for an operator who's experimenting.
- Overriding
confirm_requiredfor an operator who wants extra friction on their own runs.
The merge follows ADR-0015's deep-merge with nullification semantics.
Mastra integration
The harness compiles each workflow invocation into one of:
- Constrained Mastra
Agent— instructions composed as above, tools restricted,abortSignalwired to operator pause / timeout. The default path; covers the testimonial-style "agent does the thing" workflows. - Mastra
Workflow(the substrate primitive) — used for workflows that have explicit suspend/resume points or branching control flow. Reserved for v1.x; v1 ships theAgentpath only.
The choice is harness implementation detail per ADR-0011 and ADR-0012. Operators see a workflow; the harness picks the substrate primitive.
Consequences
What this commits us to
- A new top-level
workflows:field in the DSL, with full schema indocs/specs/project-dsl.mdand JSON Schema atkaged.dev/schema/v1.json. - A new spec
docs/specs/workflows.mdcovering: input schema reference, file upload protocol, invocation lifecycle, prompt composition, run record schema, error taxonomy. - API endpoints under
/api/v1/projects/:slug/workflows/*: list available, invoke, upload file, get run, stream run. - UI: a workflows tab in the operator project view (
/projects/:id/workflows) and a primary surface in the guest project view (/g/:project_id). - Validation timing: schema-level at DSL parse, prompt-file presence at project-load, tool intersection at session-start, input shape at invocation.
- Audit events:
workflow.invoked,workflow.completed,workflow.failed,workflow.upload,workflow.upload_expired. - Harness changes per
docs/specs/agent.md: composed-prompt path, workflow-scoped tool registry, structured-input first message.
What this forecloses
- No guest-authored workflows. Workflows are operator-authored, end of story. Guests invoke; they do not define. A guest who wants to do something not covered files an issue (ADR-0020).
- No dynamic / runtime workflow construction. The DSL is the source. No "if a guest does X, generate a workflow that does Y." If you find yourself wanting this, the workflow you actually want covers a parameterised case — author that.
- No workflow → workflow chaining in the DSL. A workflow's prompt may instruct the agent to call another workflow tool, but there's no declarative
then: another.workflowfield. Composition is via prompt, not via DSL. - No tool-set broadening. A workflow's tool allowlist must be a subset of the project's. Workflows can be narrower; never broader. The operator can't accidentally grant a workflow more than the project allows.
- No external workflow registry. Workflows are local to a project's DSL. No "import shared/workflows/testimonial.add" syntax. If two projects need the same workflow, the operator copies the YAML. Sharing is a future plugin concern, not core.
What becomes easier
- Exposing a constrained surface to a guest: define the workflow, grant the guest, done.
- Operator automation of recurring tasks: the same workflow runs from the operator UI with one click.
- Reviewing what guests can do: the DSL
workflows:block is the catalog; thepermission_setis the assignment. Both are auditable artifacts. - Prompt iteration: edit
./workflows/testimonial-add.md, daemon hot-reloads, next invocation picks it up. - Tool scoping: a workflow can declare exactly the tools it needs, even if the project has more.
What becomes harder
- DSL has a new top-level concept. Operator onboarding gains a section.
- Prompt composition has two sources (project primary + workflow). The composed text is what the audit log captures; the operator has to read both to understand a run.
- File uploads are a real surface with a real attack surface (size limits, MIME validation, staging cleanup). Documented in
specs/workflows.md. - A workflow's tool allowlist can drift behind the project's
tools:config; we validate at session-start, but operators may be surprised by tool removals breaking workflows. Diagnostics in the UI on project load flag this.
Alternatives considered
Alternative A — Free-form agent + per-grant tool allowlists
Why tempting: No new DSL surface. Guests chat with the primary agent, just with fewer tools.
Why rejected: No structured inputs. No declared framing. Every interaction is a fresh negotiation between the guest and the agent. Works for collaboration; fails for "do this specific task." Workflows are the difference between "I can use Claude" and "I have an app."
Alternative B — Mastra Workflow exposed directly in the DSL
Why tempting: Substrate already has the primitive. Suspend/resume, typed schemas, the works.
Why rejected: Leaks Mastra types into operator-facing config — violates ADR-0011 portability and ADR-0012's "operator never sees Mastra types" line. The DSL must compile to a substrate, not embed it. The kaged workflow concept is portable; Mastra is implementation detail.
Alternative C — Plugins for each workflow
Why tempting: Plugins (ADR-0008) already exist. Each workflow becomes a small plugin with declared inputs and a JSON-RPC interface.
Why rejected: Plugins are processes. Spinning up a process per workflow invocation is heavyweight; workflows are usually a prompt and a tool list. Plugins make sense for capabilities (LLM providers, preset sources, language servers); workflows are recipes using existing capabilities. Different layer.
Alternative D — Defer workflows; use only confirm_required system prompts
Why tempting: Smaller v1. Workflows can come in v1.x once the auth tier is proven.
Why rejected: The auth tier (ADR-0017, ADR-0018) has very little reason to exist without something for guests to do. Workflows are the something. Shipping guest auth without workflows is shipping a login screen to nowhere.
Alternative E — Generate workflows from natural-language operator descriptions
Why tempting: Operator types "I want a workflow that adds a testimonial to my site"; the system generates the YAML.
Why rejected: Maybe a useful tool in the operator UI later, but it's a helper — the artifact is still the YAML, reviewed by the operator, committed to the repo. Generation is layered on top, not in place of, the DSL. Out of scope here.
Open questions
- Naming format for workflows. Dot-delimited (
testimonial.add) like tools, or slash-delimited (content/testimonial) like routes, or kebab (testimonial-add)? Lean dot-delimited for consistency with the existingtools:namespace. - Default tool allowlist. If a workflow omits
tools.allow, does it inherit the project's enabled set? Lean no — require explicitallow. Conservative default forces operators to think. - Input ergonomics for arrays / nested objects. v1 schema is flat (no
type: array, notype: object). Most workflows don't need them; deferred to v1.x. - Result schema. Should workflows declare an output shape (e.g., "this workflow produces a deployed URL")? Lean yes for v1.x — useful for chaining and for UI display. Out of v1 to keep the schema small.
- Cost budget per workflow. Should each workflow declare a max token / max cost ceiling per invocation? Probably yes once budget tracking is wired (STATUS.md tech debt); v1 ships without and relies on global project budgets.
- Cage interaction. v1 ships without
cage_overrides. Workflows run with the project's existing cage policy. Per-workflow tightening is plausible v1.x. - Subagent participation. Can a workflow's primary call subagents declared in the project? Lean yes — same supervisor pattern as a normal session. Worth confirming the message-filter implications for guests (subagent output should be visible to the invoker the same way primary output is).
References
- ADR-0006 — YAML DSL the workflows block extends
- ADR-0008 — adjacent capability layer
- ADR-0011 — workflows are portable; grants are not
- ADR-0012 — substrate the harness compiles workflows into
- ADR-0014 — provider routing for workflow runs
- ADR-0015 — federated config the workflows block follows
- ADR-0016 — UI streaming model for workflow output
- ADR-0017 — guests who invoke workflows
- ADR-0018 — grants that gate guest invocation
docs/specs/project-dsl.md— DSL spec the workflows block extendsdocs/specs/agent.md— harness that compiles invocationsdocs/specs/agent-tooling.md— tool config workflows compose withdocs/specs/workflows.md— workflows implementation spec (to be written)- Original discussion: design conversation with colleagues, 2026-05-25
Amendments
2026-05-26 — Tool intersection operates against root agent's tool surface (ADR-0022)
Workflow model is unchanged (still operator-authored, parameterised, prompt-bound recipes). Tool intersection logic now operates against the root agent's tool surface instead of the removed project-level tools: block, per ADR-0022. Since tools are now per-agent on AgentSpec and the project-level tools: block no longer exists, workflows compose against ProjectDsl.primary.tools (the root agent's resolved tool set). The intersection semantics are identical — workflows can narrow, never broaden — but the reference point is the root agent, not a project-level construct. Spec amendment in docs/specs/workflows.md.
2026-06-10 — Execution semantics ratified for the v1 engine
The declaration side of this ADR (DSL block, input schema, tool intersection) is implemented in @kaged/dsl. The execution side — what actually happens when a workflow is invoked — was left implicit in the §Invocation lifecycle sketch. This amendment ratifies the execution decisions so the engine can be implemented against docs/specs/workflows.md, which this amendment is paired with. The decisions:
A workflow run is a session, not a new runtime. Invocation creates a session with
kind: "workflow"driven by the existing session/run state machines (@kaged/session-manager) and dispatched through the existing primary dispatch path. There is no separate workflow-run state machine and no parallel execution engine. The workflow-specific phases that precede dispatch (input validation, upload staging, confirm gate) are an invocation envelope handled at the daemon handler layer and recorded in a newworkflow_invocationsstorage table that references the session. Rationale: the session machinery already carries streaming, abort, checkpoints, compaction, spend gating, persistence, and audit — re-deriving any of it for workflows would guarantee drift.Open question 1 (naming) — resolved: dot-delimited. Already enforced by
WorkflowNameSchema(lowercase letters, digits, underscores, hyphens, dots; 2–64 chars; starts with a letter; reserved names rejected).Open question 2 (default tool allowlist) — resolved: explicit
tools.allowrequired. Already enforced byWorkflowDefinitionSchema(tools.allowis a required field). No inheritance of the root agent's full set.Open question 3 (input ergonomics) — confirmed: flat v1. No
array/objectinput types in v1. Unchanged.Open question 4 (result schema) — deferred to v1.x. A workflow run's "result" in v1 is its final assistant message plus the run outcome (
succeeded/failed). No declared output shape.Open question 5 (cost budget) — resolved: reuse ADR-0026. Workflow dispatches flow through the existing spend-limit gate (ADR-0026) exactly like chat dispatches. No per-workflow budget field in v1; a guest-triggered run that trips a provider spend limit fails with the same
spend_limit_exceededtaxonomy, surfaced to the invoker as a generic workflow failure (guests never see provider/spend detail).Open question 6 (cage interaction) — confirmed: project cage policy as-is in v1.
cage_overridesstays reserved and is rejected-if-present at parse time only in the sense of being schema-allowed but ignored with a project-load diagnostic. The synthesized-DSL view must show that it is inert.Open question 7 (subagent participation) — resolved: yes. The workflow run's agent is the root agent (constrained); its declared subagent tree is available unchanged. Subagent output streams to the invoker the same way primary output does. The tool intersection applies to the root agent's own tool surface only; subagents keep their declared per-agent tools (a workflow cannot reach into the tree and narrow grandchildren in v1 — that is what
cage_overridesmay become in v1.x).kaged.workflowtool semantics (new decision). The root-agent defaultkaged.workflow.*surface (referenced by ADR-0022 rule 5 and the synthesized-DSL spec) is a single action-dispatched toolkaged.workflowwith actionslist,describe, andrun. Recursion rule: an agent run that was itself started by a workflow invocation has therunaction refused at dispatch time (workflow_recursion_denied) — workflow → workflow invocation depth is capped at 1 in v1.list/describeremain available so a workflow's prompt can reference the catalog. Composition stays prompt-mediated per this ADR's foreclosures:runexists for operator-session agents to start workflows, not for workflows to chain.Confirm gate (new decision).
confirm_required: trueis enforced server-side, not just rendered as a UI step: the invocation is created in stateawaiting_confirmand a second API call (.../confirm) transitions it to dispatch. Operators may skip confirm (the operator invoke endpoint acceptsconfirm: trueinline); guests may not. Unconfirmed invocations expire after 10 minutes. Rationale: a guest-facing safety affordance that only exists client-side is not an affordance.Concurrency limits (new decision). Per-project cap of 4 concurrently running workflow invocations; per-guest cap of 1. Excess invocations are rejected (
workflow_concurrency_exceeded), not queued, in v1. Queuing is a v1.x consideration.Timeout enforcement (new decision).
timeout_secondsis enforced by the daemon via the existing per-runAbortControllerregistry: a walltime timer started at dispatch aborts the run and marks the invocationfailedwithworkflow_timeout. The session itself returns toidle(operator sessions) or stays terminal for the invocation record — the timer kills the run, not the session.Guest streaming access (new decision). Guests get read access to the session output WebSocket channel scoped to sessions created by their own invocations — authenticated by the existing
kaged_guest_sessioncookie, authorized by matching the invocation'sinvokerto the guest identity. No other session is visible. This extends the WS auth gate, which is currently operator-only; the change is specced indocs/specs/workflows.md§Streaming and must land with guest-WS tests before guest invocation is enabled.Prompt-injection boundary (new decision). Guest-supplied input values are untrusted. The structured-inputs message is composed with explicit fencing and escaping rules (specced in §Prompt composition of the spec): values are rendered inside a
<workflow_inputs>block, angle-bracket-escaped, length-capped by the input schema, with a fixed preamble instructing the model that the block is data, not instructions. This does not make injection impossible; it makes the cage and tool intersection the actual security boundary — which is why workflow tool allowlists should be minimal. This also resolves an internal inconsistency in the original text of this ADR: §Invocation lifecycle step 5 said inputs are "a structured first message", while the §Prompt composition example rendered them inside the system prompt. Step 5 wins — inputs are a fenced first user-role message, never system-prompt material. The system prompt stays stable per workflow (cacheable), and untrusted invoker data stays on the user side of the role boundary.
Implementation order, storage schema, API contracts, error taxonomy, staging protocol, and test plan are normative in docs/specs/workflows.md (amended 2026-06-10 in lockstep with this amendment). No code ships with this amendment, per ADR-0003: spec first, failing tests next, engine after.
2026-06-10 (b) — Step-based recipes: steps: DSL structure and execution semantics
The same-day amendment above ratified the run model (a workflow run is a session) but left multi-step recipes at "Mastra Workflow reserved for v1.x" — no DSL structure, no validation rules, no execution semantics. That is insufficient to implement against: real recipes ("gather → operator reviews → publish → deploy") need declared steps. This amendment defines the step model for v1. A note on provenance: STATUS.md has claimed a "steps placeholder" in WorkflowDefinitionSchema — no such field exists in @kaged/dsl (the only steps-adjacent field is the unrelated max_steps agent-loop limit on AgentSpec). Steps are greenfield, defined here first, per ADR-0003.
Decisions:
stepsis an optional ordered array, not a named-object map. Each entry carries a required uniqueid. This is a deliberate, called-out deviation from the ADR-0015 named-object-map convention, for two reasons: (a) step order is semantic — encoding sequence in YAML map key order would make meaning depend on parser key-ordering behaviour, which ADR-0015 explicitly treats as non-semantic; (b) ADR-0015's "arrays replace" overlay rule is the safer merge semantics for steps — aproject.local.yamloverride replaces the whole recipe rather than splicing individual steps into a sequence it can't see. Partial step surgery via deep-merge is a foot-gun; whole-array replacement is reviewable.Absent
steps= the degenerate single-run case. A workflow withoutstepsbehaves exactly as specced in amendment (a): one constrained agent run framed bysystem_prompt. Everything already ratified (envelope, confirm gate, tool intersection, timeout, streaming) is unchanged. A workflow withstepsexecutes them sequentially as multiple runs within the samekind: "workflow"session — the run model from amendment (a) item 1 is refined, not replaced: still a session, still the existing session/run machines, one run per agent step.Three step kinds in v1:
agent(an agent run with its own prompt fragment and optionally further-narrowed tools),confirm(a server-enforced mid-recipe pause for the invoker, generalising the pre-dispatch confirm gate), andtask(run a named entry from the project'stasks:block pertask-runner.mdand gate on its exit code). Nobranch, noloop, noparallelin v1 — sequential only. Conditionals and parallelism are v1.x questions, and the schema shape (array of tagged objects) does not foreclose them.Explicit data flow via
{{ … }}bindings, two roots only. Bindable fields may reference{{ inputs.<name> }}and{{ steps.<id>.output.<name> }}— nothing else (no environment, no filesystem, no arbitrary expressions). References are resolved statically at DSL validation time: unknown input names, forward/self step references, and references to outputs a step doesn't declare are all parse/validate-time errors, not runtime surprises.tasksteps accept no bindings at all in v1 — interpolating invoker-influenced strings into shell commands is an injection primitive we decline to build; a task step runs itstasks:entry exactly as the operator declared it.Agent steps declare typed outputs; emission is tool-mediated. An
agentstep may declare anoutputs:schema (same primitive types as workflow inputs, minusfile). The step's agent receives an auto-injected internal toolkaged.step.complete(same injection pattern askaged.checkpoint) whose arguments are validated against the declared schema; calling it ends the step successfully. A step with declared required outputs that ends without a validkaged.step.completecall fails (step_output_missing/step_output_invalid). Steps with no declared outputs succeed on normal run completion. This gives later steps and confirm messages structured values to bind, without trusting prose extraction.Shared-transcript context model. All agent steps run in the workflow session's single conversation: step N sees the transcript of steps 1…N-1 (compaction applies per ADR-0024). Explicit
with:bindings are still rendered into each step's fenced kickoff message — bindings are the contract, the transcript is context. Rationale: isolated-context steps would require re-deriving message-history plumbing for marginal benefit, and operators can already see the entire transcript in the debug view, which keeps the recipe auditable as one conversation.Per-step tool narrowing, same direction-of-travel rule. An
agentstep's optionaltools:intersects against the workflow's effective tool set (which intersected against the root agent's). Narrowing only, at every level: root ⊇ workflow ⊇ step.kaged.step.completeis injected outside this math (always present in step runs, never in allowlists);kaged.workflow runremains refused inside workflow runs regardless of step allowlists (amendment (a) item 9).Failure policy is per-step and boring:
on_fail: abort(default) orcontinue.abortfails the invocation with the step's error;continuerecords the failure and proceeds (later bindings to that step's outputs are statically forbidden — a step you may skip past cannot be a data dependency, enforced at validation time). No retries in v1 (v1.x:retry:). Each step takes an optionaltimeout_seconds; the workflow-leveltimeout_secondsremains the global walltime cap covering the whole recipe.Mid-recipe confirm gates reuse the envelope machinery. A
confirmstep parks the invocation in a new envelope stateawaiting_step_confirm; the same confirm/cancel endpoints apply; expiry fails the invocation (step_confirm_expired, default 600s per step, overridable). Guests confirm in the guest UI; operators in the session view. For agent-initiated invocations (kaged.workflow run), workflows containingconfirmsteps are refused at invocation time in v1, same rule asconfirm_required(amendment (a) item 9) — an unattended caller cannot satisfy an attended gate.Still not Mastra
Workflow. Steps compile to sequential agent runs orchestrated by the daemon envelope, not to the MastraWorkflowprimitive. The substrate's suspend/resume machinery remains a v1.x harness option behind the same DSL (per this ADR's Alternative B reasoning: the DSL must compile to a substrate, not embed one). If the harness later adopts MastraWorkflowfor execution, the DSL, storage, and API shapes defined in the spec are the stable contract it must satisfy.
Full DSL structure, schemas, validation matrix, envelope/state extensions, storage, API, streaming, error taxonomy, and tests are normative in docs/specs/workflows.md §Steps (amended 2026-06-10 (b) in lockstep). No code ships with this amendment.
2026-06-12 — ADR-0038: mandatory steps, no workflow-level prompt, per-level model override
ADR-0038 amends three aspects of this ADR's workflow schema:
stepsis required and non-empty. The stepless workflow concept from amendment (b) item 2 ("absentsteps= the degenerate single-run case") is retired. Every workflow must declare at least one step; the former single-prompt workflow is now a one-step workflow. This collapses the dual execution path into a single step pipeline.system_promptremoved fromWorkflowDefinition. The workflow-level prompt field from this ADR's §DSL shape is deleted. Each step carries its ownprompt(path or path array per ADR-0037). Shared content across steps is reused by listing the same file path in each step's prompt array — explicit composition, consistent with ADR-0037's abolition of implicit prefix concatenation.modeladded as optional override at workflow root and onagentsteps. Inheritance chain: project (root agent) → workflow root → step. Omitted means inherit. Invalid onconfirmandtasksteps (parse error). Fills the gap identified in this ADR's open question 5 (cost budget) and the per-agent config principle from ADR-0022.
Schema, spec, and tests amended in lockstep. See ADR-0038 for full rationale and alternatives considered.