Runtime steps
A Workflow Template's runtimeSteps are not documentation of a process — they are the process. One generic engine reads the ordered list and executes each step by its kind: an LLM call, an image generation, a tool invocation, a sub-workflow, an agent loop. This page is the step contract — every field, the prompt-reference syntax, and the sizing limits (maxTokens, timeoutSeconds, estimatedTokens) that decide whether a step succeeds, fails loudly, or never launches.
How steps execute
Steps run in order. Each step's output is written to a run-context blackboard under the step's id, and later steps (and the terminal artifact assembler) address earlier outputs by dotted path. There is no per-template execution code — the stored step list is the program, and every run freezes the exact list it executed into its Specification for reproducibility.
Step kinds
| kind | What it does | Key fields |
|---|---|---|
llm | One model call: render system + promptTemplate against the context, call the bound text model, store { text, json }. | system, promptTemplate, expectJson, jsonSchema, maxTokens |
image | Image generation from an upstream prompt. | promptPath, aspectRatio, model |
tool | A registered deterministic tool (live keyword data, URL fetch, transcript extraction, aggregation). | tool, inputPath |
subWorkflow | Runs another Template as a linked child run; its artifact content lands at {step-N.report...}. Costs roll up to the parent. | subWorkflow { templateId, templateVersion, intakeMapping }, satisfiedBy |
agent | A bounded LLM loop whose tools are sub-workflows. | tools, maxIterations, maxCostUsd |
code / qa | Recorded seam steps — no provider call, no metering. | — |
The step contract
{
"id": "step-2", // stable id; later steps reference outputs by it
"name": "Synthesize report", // human label shown in run telemetry
"kind": "llm", // llm | image | tool | subWorkflow | agent | code | qa
"capability": "text", // resolves a model from providers when no role/model is set
"role": "writer", // optional: binds providers[role] instead of capability
"system": "You are an expert research analyst...",
"promptTemplate": "Using this outline:\n{step-1.text}\n\nWrite a report on \"{intake.topic}\"...",
"expectJson": true, // parse the response as JSON into the step's .json slot
"jsonSchema": { "...": "..." }, // native structured output — the response IS valid JSON
"maxTokens": 3000, // hard output ceiling for THIS step (see sizing below)
"timeoutSeconds": 720, // optional wall-clock override (clamped; see limits)
"estimatedTokens": { "input": 4000, "output": 3000 } // feeds the pre-run budget gate
}Model binding
A step resolves its model in a fixed order: an explicit model pin wins; otherwise providers[role]; otherwise providers[capability]. Binding roles ("role": "designResearcher") rather than pinning lets a run override the model without editing the Template — and lets sibling Templates (e.g. a model-comparison twin) swap one binding while sharing every step definition.
Prompt references
system and promptTemplate interpolate {dotted.path} references against the blackboard. Publishing validates every reference — a prompt that mentions a step that doesn't exist (or exists only later) is rejected before the Template can go live.
{intake.topic} the run's intake field
{step-1.text} an earlier step's raw text output
{step-1.json.title} a field of an earlier step's parsed JSON
{step-1.report.summary} a sub-workflow step's child artifact content
{inputs.sourceReport} a supplied artifactReference intake (content object)
{lookup.styleDescriptor} a declarative lookup resolved from intakeStructured output
jsonSchema engages the provider's native structured output — the response is schema-constrained server-side, never prompt-and-scrape. Keep schemas provider-portable: array bounds like minItems/maxItems beyond 0/1 are rejected by some providers — enforce ranges in prompt text instead ("5–8 colors").
Sizing maxTokens — the field that decides completeness
maxTokens is a per-step output ceiling. Size it to the deliverable that step produces, not to a habit:
| Deliverable | Realistic output | maxTokens |
|---|---|---|
| A seed phrase or short classification | ≤ 100 tokens | 300 |
| A structured research object (keywords, questions, spec) | 3–5k tokens | 8000 |
| A design brief (typography, palette, motion, layout) | ~2.5k tokens | 6000 |
| A complete single-file HTML page (inline CSS/JS + JSON-LD) | 13–16k tokens | 32000 |
| A full-page rewrite that re-emits the corrected document | 14–18k tokens | 36000 |
On models with always-on reasoning, internal thinking tokens count against the same budget. A 14,000-token cap that comfortably fits a page on a non-thinking model can yield ~2,000 tokens of actual text after ~12,000 tokens of reasoning — a truncated document. If a Template can bind a thinking model (directly or via a twin's provider swap), size caps for output plus reasoning.
A response stopped by max_tokens is never shippable: mid-document JSON fails parsing, and a mid-document page is a broken artifact. The engine fails the step with the real cause — output truncated at max_tokens=N — the step's maxTokens is too small for its output — instead of letting a truncated result ride to the next step. If you see this error, raise the step's cap; nothing was published.
The other two limits
| Field | Default | Behavior |
|---|---|---|
timeoutSeconds | per kind: llm 300s · image 300s · tool 240s · agent 900s | Wall-clock budget for the whole step (provider call + processing). A step may override upward for legitimately long generations; overrides are clamped by the platform so no step is unbounded. subWorkflow steps carry no budget of their own — they are bounded by their children’s. |
estimatedTokens | input 4000 · output 2000 | Feeds the pre-run cost estimate that budget caps enforce. A step that really emits 16k tokens but omits estimatedTokens is priced at 2k — the budget gate under-reads real spend. Declare honest estimates on every metered step. |
Worked example — a two-step workflow, step by step
generate-research-report is the platform's canonical minimal pipeline: outline, then synthesize. Two llm steps, one reference between them, one structured terminal output.
"runtimeSteps": [
{
"id": "step-1",
"name": "Outline report",
"kind": "llm",
"capability": "text",
"system": "You are an expert research analyst...",
"promptTemplate": "Create a tight section outline for a research report on: {intake.topic}. Audience: {intake.audience}. List 3-5 sections with one-line descriptions.",
"maxTokens": 800
},
{
"id": "step-2",
"name": "Synthesize report",
"kind": "llm",
"capability": "text",
"expectJson": true,
"system": "You are an expert research analyst...",
"promptTemplate": "Using this outline:\n{step-1.text}\n\nWrite a research report on \"{intake.topic}\". Return ONLY JSON with keys: title, summary, sections (array of {heading, body}), sources (array of strings).",
"maxTokens": 3000
}
]What actually happens when a run executes it:
POST /v1/runs { templateId: "generate-research-report",
intake: { topic: "urban heat islands", audience: "city planners" } }
── step-1 · Outline report ────────────────────────────────────────────
engine renders promptTemplate against the context blackboard:
"Create a tight section outline for a research report on: urban heat
islands. Audience: city planners. List 3-5 sections..."
→ provider call with max_tokens = 800
→ context["step-1"] = { text: "1. What heat islands are...", json: null }
── step-2 · Synthesize report ─────────────────────────────────────────
engine renders promptTemplate — {step-1.text} interpolates the outline:
"Using this outline:\n1. What heat islands are...\n\nWrite a research
report on \"urban heat islands\". Return ONLY JSON with keys: ..."
→ provider call with max_tokens = 3000, expectJson
→ context["step-2"] = { text: "{\"title\": ...}", json: { title, summary,
sections, sources } }
── assembly ───────────────────────────────────────────────────────────
the artifactType's assembler shapes the terminal step's output into the
artifact content + QA envelope → the Run completes with an Artifact.Note the sizing: the outline step gets 800 (a list of five lines), the synthesis step 3000 (a full report as JSON). Neither number is arbitrary — each is the deliverable's realistic ceiling with headroom, and each would fail loudly rather than truncate.
Composition: a step that runs another workflow
generate-research-infographic starts by running the report Template above as a sub-workflow — unless the operator supplies an existing report, in which case satisfiedBy binds the supplied artifact into the same slot and the child run never happens. Downstream steps read the same reference either way.
{
"id": "step-1",
"name": "Research report",
"kind": "subWorkflow",
"subWorkflow": {
"templateId": "generate-research-report",
"intakeMapping": { "intake.topic": "topic", "intake.audience": "audience" }
},
"satisfiedBy": "inputs.sourceReport" // a supplied report skips the child run
}
// downstream, the child's artifact content is addressable:
// "promptTemplate": "...visualizes the key findings below.\n\nFindings:\n{step-1.report.summary}"When limits hit
- Truncation — the step fails naming the cap. Fix: raise that step's
maxTokens. - Timeout — the step fails with its wall-clock budget. Fix: raise
timeoutSeconds(bounded), or split the step. - Budget gate — the run never launches: its estimate exceeds an applicable budget's per-run cap. Fix: honest
estimatedTokensand a cap that reflects the pipeline's real cost.
Step definitions live in the published Template. Changing maxTokens in an authoring file does nothing until the Template is republished — a new immutable version is minted and subsequent runs freeze that version. If a run's error shows an old cap, the fix is published, not deployed.
Related concepts
- Workflow templates — the declared workflow these steps live inside.
- Sub-workflows — composition via child runs.
- Costs — how metered steps become estimates, ledgers, and reconciled spend.
- Runs — per-step telemetry: the exact rendered prompt, tokens, duration, and cost of every step described here.