Concepts · Workflow primitives

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

kindWhat it doesKey fields
llmOne model call: render system + promptTemplate against the context, call the bound text model, store { text, json }.system, promptTemplate, expectJson, jsonSchema, maxTokens
imageImage generation from an upstream prompt.promptPath, aspectRatio, model
toolA registered deterministic tool (live keyword data, URL fetch, transcript extraction, aggregation).tool, inputPath
subWorkflowRuns 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
agentA bounded LLM loop whose tools are sub-workflows.tools, maxIterations, maxCostUsd
code / qaRecorded seam steps — no provider call, no metering.

The step contract

an llm step, annotatedjson
{
  "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.

reference syntaxtext
{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 intake

Structured 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:

DeliverableRealistic outputmaxTokens
A seed phrase or short classification≤ 100 tokens300
A structured research object (keywords, questions, spec)3–5k tokens8000
A design brief (typography, palette, motion, layout)~2.5k tokens6000
A complete single-file HTML page (inline CSS/JS + JSON-LD)13–16k tokens32000
A full-page rewrite that re-emits the corrected document14–18k tokens36000
Thinking models spend maxTokens before they write.

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.

Truncation fails loudly — never silently.

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

FieldDefaultBehavior
timeoutSecondsper kind: llm 300s · image 300s · tool 240s · agent 900sWall-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.
estimatedTokensinput 4000 · output 2000Feeds 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.

generate-research-report · runtimeStepsjson
"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:

execution walkthroughtext
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.

generate-research-infographic · step-1json
{
  "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 estimatedTokens and a cap that reflects the pipeline's real cost.
Templates execute from the platform, not from your editor.

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.