Writing Workflows (Explained Simply)
Think of a workflow as a checklist. The runtime reads the list top to bottom, runs each step, and saves the results into state.
If you are new, remember one sentence: A workflow is a YAML file that describes inputs and steps.
What A Workflow Does
- Declare inputs (what data you expect).
- Run steps in order.
- Store each step output in state.
- Let later steps read earlier outputs.
Basic Shape
workflow: # workflow metadata
id: issue_triage # unique workflow id
version: v1 # version tag
inputs: # declared inputs
issue:
description: The issue text to analyze
required: true # required input
on_error: fail_fast # stop on first error
steps: # ordered steps (a list)
- id: summarize # agent step — calls an LLM agent
type: agent
agent: summarizer # defined in agents/summarizer.yaml
inputs:
issue: inputs.issue # read from inputs
- id: classify # function step — calls a Python function
type: function
function: stubs.classify_severity
inputs:
issue: inputs.issue
- id: echo # tool step — calls a tool class
type: tool
tool: tools.echo
inputs:
message: steps.summarize.summary # read prior step outputNote on the - character: in YAML it means "this is a list item." You need - for each step and for each branch rule because those are lists.
Inputs
Inputs tell the runtime what data you will pass at run time.
inputsis optional.- If you declare
inputs, the runtime validates what you pass. - If you do not declare
inputs, anything you pass is accepted.
Example run:
ai run workflows/example.yaml -i issue="Login API fails for invalid token"How Data Moves
inputs.<name>reads workflow inputs.steps.<step_id>.<field>reads a specific field from a prior step's output.steps.<step_id>(2-segment path) resolves to the entire output dictionary of that step. Use this when you want to pass all outputs from one step into a downstream step as a single object.
Each step type controls its output keys differently:
- Function steps — the Python function returns a dict; its keys become the output. E.g.
return {"severity": "critical"}→steps.classify.severity. - Tool steps — the
ToolResult.outputdict keys become the output. E.g.output={"priority": "P1"}→steps.priority.priority. - Agent steps — when the LLM returns plain text, the runtime wraps it as
{output_key: "..."}whereoutput_keyis set in the agent YAML (default:text). E.g. agent withoutput_key: summary→steps.summarize.summary.
Example:
inputs:
issue:
required: true
steps:
- id: summarize
type: agent
agent: summarizer
inputs:
issue: inputs.issue
- id: classify
type: function
function: stubs.classify_severity
inputs:
summary: steps.summarize.summaryStep Types
agent step:
- Delegates to an agent definition in
agents/. - The agent runs its LLM pipeline internally and returns outputs.
- Use this when you need a language model.
function step:
- Calls a plain Python function in
functions/. - Signature:
(inputs: dict) -> dict. - Use this for deterministic logic and transformations.
tool step:
- Calls a tool class in
tools/. - Use this for external actions (HTTP, filesystem, shell, APIs).
- The built-in
tools.echoaccepts any JSON-serializable value formessage(string, dict, list, number). Non-string values are serialized to indented JSON automatically.
Retry Policy
steps:
- id: unstable_call
type: tool
tool: tools.http
inputs:
url: inputs.url
retry:
attempts: 3
backoff: exponential
initial_delay: 1Branching
Branching lets you jump to another step based on a condition.
steps:
- id: classify
type: function
function: stubs.classify_severity
inputs:
issue: inputs.issue
next:
- when: state.inputs.issue == "bug"
goto: bug_path
- when: state.inputs.issue == "feature"
goto: feature_path
- default: default_pathBranch targets must point to valid step ids. The runtime validates this at load time.
Error Policy
Workflow-level on_error can be:
fail_fast(default) stops on the first error.continueattempts the remaining steps.
Versioning
Use workflow.version like v1, v2, etc. The CLI resolves the highest version when you run by id:
ai run issue_triage
ai run issue_triage@v2Common Mistakes (Quick Fixes)
- Forgot
-before a step: YAML will parse incorrectly. - Used a file path instead of a reference:
function: my_functions.summarizenotfunction: functions/my_functions.py. - Referenced
steps.xbefore it runs: only read outputs from earlier steps.
For deeper function, agent, and tool coverage, see Writing Agents, Writing Functions, and Writing Tools.