Build workflows
Data flow
How steps pass data with templates, how dependencies and ordering work, and how to branch with conditions.
Steps share data through templates, run in an order decided by their dependencies, and can be switched on or off with conditions.
Templates
A template is a path in double braces: {{ steps.pr.output.title }}. Templates can appear in any step option — prompts, tool arguments, JavaScript inputs.
| Path | What it holds |
|---|---|
trigger.* |
The trigger payload — a GitHub event, a Slack message, a webhook body |
inputs.* |
The run's inputs, with defaults applied |
steps.<key>.output.* |
Another step's output |
steps.<key>.status |
succeeded, failed or skipped |
steps.<key>.error |
The error message when a step failed or was skipped |
run.number, run.trigger_kind, run.started_at |
Details of the current run. The number is the one shown in the app, counted within your account |
workflow.name, workflow.slug |
The workflow itself |
artifacts.<slug>.body, .title, .version, .url, .updated_at |
An artifact as it is when the step starts |
Rules worth knowing:
- Type is preserved when the whole value is one template.
rows: "{{ steps.query.output.rows }}"passes an array, not a string. Numbers, booleans and objects work the same way. - Inside longer text, values are inserted as text. Objects and arrays are inserted as JSON:
"Found {{ steps.search.output }}". - Array items use numeric indexes:
{{ steps.search.output.0.title }}. - Missing paths become empty (or
nullfor a whole-value template) rather than failing the step. - There are no filters or expressions. To format dates, join lists or do arithmetic, add a javascript step.
Each step's page in a run shows its Input — the step's options after templates were filled in — which makes it easy to see exactly what a model or tool received.
Dependencies and order
A step that references {{ steps.pr... }} automatically depends on pr, as does a step whose condition reads steps.pr. Use depends_on only when you need ordering without passing data:
- key: notify
kind: tool
depends_on: [deploy]
tool: slack.post_message
args: { channel: C0123, text: "Deploy finished" }
Murmurator orders steps so that every step runs after the steps it depends on, and steps run one at a time in that order. The steps diagram draws these dependencies as arrows: steps in the same column don't depend on each other. Circular dependencies are rejected when the workflow is saved.
A step only runs when every step it depends on succeeded. If a dependency failed or was skipped, the step is skipped too, with the reason recorded — and so on down the chain. Steps on other branches carry on.
Conditions
Add if to run a step only in some situations. A skipped step isn't a failure.
- key: alert
kind: tool
if: { path: steps.review.output.data.risk, equals: high }
tool: slack.post_message
args: { channel: C04ENGREVIEW, text: "High-risk PR: {{ trigger.pull_request.html_url }}" }
A condition compares the value at path (a template path, without braces) using one operator:
| Operator | True when the value… |
|---|---|
equals / not_equals |
Equals / doesn't equal the given value |
in |
Is one of a list: in: [medium, high] |
exists |
Is present (true) or missing (false) |
matches |
Matches a regular expression: matches: "^release/" |
gt, gte, lt, lte |
Is a number greater / less than the given value |
truthy |
Is non-empty and not false/0 (true), or the reverse (false) |
Combine conditions with all, any and not:
if:
all:
- { path: trigger.action, equals: opened }
- { not: { path: trigger.pull_request.draft, equals: true } }
- any:
- { path: trigger.pull_request.base.ref, equals: main }
- { path: trigger.pull_request.base.ref, matches: "^release/" }
Patterns
Branch on a model's judgement. Give the llm step a schema, then put conditions on steps.<key>.output.data.*. Structured output makes branches dependable.
Fan in. A final step that references several earlier steps waits for all of them, and is skipped if any of them didn't succeed.
Report failures. To post when something goes wrong, check a status in a condition — but remember the reporting step must not depend on the failing step, or it will be skipped along with it. Put the check on an earlier step's output instead (for example, "if the query returned no rows").