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:

  1. 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.
  2. 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.
  3. 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 per task-runner.md. Not the same thing. Shell commands, not agent recipes. Name stays.
  • Mastra Workflow — the substrate's control-flow primitive with suspend/resume per 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

  1. Invoker (operator or guest) selects a workflow.
  2. UI renders an input form generated from the inputs schema.
  3. Invoker submits. Daemon validates inputs against the schema.
  4. Daemon creates a workflow run — a special session type with kind: "workflow", workflow_name, frozen inputs, and a reference to the invoking principal.
  5. 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 workflow tools.allow / tools.deny.
    • Inputs are injected as a structured first message: <workflow_inputs>...</workflow_inputs>.
  6. Primary agent runs. Streaming output goes to the invoker's UI in real-time per ADR-0016.
  7. Completion. Run ends with succeeded or failed. Result is persisted to the session record.
  8. 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_required for 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, abortSignal wired 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 the Agent path 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 in docs/specs/project-dsl.md and JSON Schema at kaged.dev/schema/v1.json.
  • A new spec docs/specs/workflows.md covering: 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.workflow field. 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; the permission_set is 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

  1. 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 existing tools: namespace.
  2. Default tool allowlist. If a workflow omits tools.allow, does it inherit the project's enabled set? Lean no — require explicit allow. Conservative default forces operators to think.
  3. Input ergonomics for arrays / nested objects. v1 schema is flat (no type: array, no type: object). Most workflows don't need them; deferred to v1.x.
  4. 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.
  5. 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.
  6. Cage interaction. v1 ships without cage_overrides. Workflows run with the project's existing cage policy. Per-workflow tightening is plausible v1.x.
  7. 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

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:

  1. 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 new workflow_invocations storage 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.

  2. 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).

  3. Open question 2 (default tool allowlist) — resolved: explicit tools.allow required. Already enforced by WorkflowDefinitionSchema (tools.allow is a required field). No inheritance of the root agent's full set.

  4. Open question 3 (input ergonomics) — confirmed: flat v1. No array/object input types in v1. Unchanged.

  5. 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.

  6. 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_exceeded taxonomy, surfaced to the invoker as a generic workflow failure (guests never see provider/spend detail).

  7. Open question 6 (cage interaction) — confirmed: project cage policy as-is in v1. cage_overrides stays 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.

  8. 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_overrides may become in v1.x).

  9. kaged.workflow tool semantics (new decision). The root-agent default kaged.workflow.* surface (referenced by ADR-0022 rule 5 and the synthesized-DSL spec) is a single action-dispatched tool kaged.workflow with actions list, describe, and run. Recursion rule: an agent run that was itself started by a workflow invocation has the run action refused at dispatch time (workflow_recursion_denied) — workflow → workflow invocation depth is capped at 1 in v1. list/describe remain available so a workflow's prompt can reference the catalog. Composition stays prompt-mediated per this ADR's foreclosures: run exists for operator-session agents to start workflows, not for workflows to chain.

  10. Confirm gate (new decision). confirm_required: true is enforced server-side, not just rendered as a UI step: the invocation is created in state awaiting_confirm and a second API call (.../confirm) transitions it to dispatch. Operators may skip confirm (the operator invoke endpoint accepts confirm: true inline); guests may not. Unconfirmed invocations expire after 10 minutes. Rationale: a guest-facing safety affordance that only exists client-side is not an affordance.

  11. 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.

  12. Timeout enforcement (new decision). timeout_seconds is enforced by the daemon via the existing per-run AbortController registry: a walltime timer started at dispatch aborts the run and marks the invocation failed with workflow_timeout. The session itself returns to idle (operator sessions) or stays terminal for the invocation record — the timer kills the run, not the session.

  13. 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_session cookie, authorized by matching the invocation's invoker to the guest identity. No other session is visible. This extends the WS auth gate, which is currently operator-only; the change is specced in docs/specs/workflows.md §Streaming and must land with guest-WS tests before guest invocation is enabled.

  14. 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:

  1. steps is an optional ordered array, not a named-object map. Each entry carries a required unique id. 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 — a project.local.yaml override 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.

  2. Absent steps = the degenerate single-run case. A workflow without steps behaves exactly as specced in amendment (a): one constrained agent run framed by system_prompt. Everything already ratified (envelope, confirm gate, tool intersection, timeout, streaming) is unchanged. A workflow with steps executes them sequentially as multiple runs within the same kind: "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.

  3. 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), and task (run a named entry from the project's tasks: block per task-runner.md and gate on its exit code). No branch, no loop, no parallel in v1 — sequential only. Conditionals and parallelism are v1.x questions, and the schema shape (array of tagged objects) does not foreclose them.

  4. 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. task steps 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 its tasks: entry exactly as the operator declared it.

  5. Agent steps declare typed outputs; emission is tool-mediated. An agent step may declare an outputs: schema (same primitive types as workflow inputs, minus file). The step's agent receives an auto-injected internal tool kaged.step.complete (same injection pattern as kaged.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 valid kaged.step.complete call 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.

  6. 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.

  7. Per-step tool narrowing, same direction-of-travel rule. An agent step's optional tools: 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.complete is injected outside this math (always present in step runs, never in allowlists); kaged.workflow run remains refused inside workflow runs regardless of step allowlists (amendment (a) item 9).

  8. Failure policy is per-step and boring: on_fail: abort (default) or continue. abort fails the invocation with the step's error; continue records 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 optional timeout_seconds; the workflow-level timeout_seconds remains the global walltime cap covering the whole recipe.

  9. Mid-recipe confirm gates reuse the envelope machinery. A confirm step parks the invocation in a new envelope state awaiting_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 containing confirm steps are refused at invocation time in v1, same rule as confirm_required (amendment (a) item 9) — an unattended caller cannot satisfy an attended gate.

  10. Still not Mastra Workflow. Steps compile to sequential agent runs orchestrated by the daemon envelope, not to the Mastra Workflow primitive. 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 Mastra Workflow for 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:

  1. steps is required and non-empty. The stepless workflow concept from amendment (b) item 2 ("absent steps = 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.
  2. system_prompt removed from WorkflowDefinition. The workflow-level prompt field from this ADR's §DSL shape is deleted. Each step carries its own prompt (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.
  3. model added as optional override at workflow root and on agent steps. Inheritance chain: project (root agent) → workflow root → step. Omitted means inherit. Invalid on confirm and task steps (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.