Writing Flows
A language that gets out of the way
Most workflow tools make you think in their language — dragging boxes, wiring JSON, configuring YAML. Pipedown goes the other direction:
Write what you mean, one step per line.
The syntax disappears so you can focus on the pipeline.
Think of it like Markdown for automation. Markdown took the most common text formatting patterns and gave them the thinnest possible syntax — just enough structure, then it gets out of the way. Pipedown does the same thing for pipelines: a handful of symbols on top of plain language, and that’s it.
This page is the complete syntax reference. Everything you can write in a flow is here. But rather than just listing features, we’ll walk through them the way you’d actually learn them — starting simple, building up, and showing why each piece exists.
Naming your flow
Every flow starts with a name and description. The # Title is what shows up
in the UI, and the plain text after it explains what the flow does:
That’s it. # Title sets the display name. Everything between the title and the
first executable line is the description — no special syntax needed.
Legacy # name: / # description: headers also work, but the cleaner style
above is preferred:
The two step types
There are really only two kinds of steps. Once you internalize this, everything else is just control flow around them.
App steps
Call an installed app’s action with /app action args:
The format is /appId actionName followed by whatever natural language the app
needs. The text after the action name is free-form — the app interprets it
however makes sense. If you skip the action name, the app just receives the
bare command:
LLM steps
Any line that isn’t a special keyword is an LLM step — plain language that AI processes using the previous step’s output:
LLM steps are the glue between app steps. Need to reshape data between two apps? Just say what you want in plain language. This is what makes Pipedown feel so different from traditional workflow tools — instead of writing transformation code, you describe the transformation.
For longer instructions, indent continuation lines. They get joined into a single instruction:
A line is an LLM step if it doesn’t start with /, *, -, !, if,
cancel, or a block name. Everything else is just a natural language
instruction for AI.
Variables and data flow
Here’s where things get interesting. By default, each step’s output flows straight to the next step:
This linear pipeline is clean and readable for sequential work. But what happens when step 5 needs the output from step 2? Or when you’re combining results from three parallel fetches? That’s where variables come in.
Setting a variable
Drop = variableName at the end of any step to capture its output:
Referencing a variable
Use {{variableName}} anywhere in a later step to inject that saved output:
Without variables, that combine step would only see the output of the
/stripe step before it. Variables let you reach back and pull in anything from
earlier in the pipeline.
Property access
When an app returns structured data, use dot notation to drill in:
Nested access works too: {{pr.author.login}}. If the variable holds JSON,
{{variable.prop}} extracts the named property.
One thing to know: property access works when apps return structured data
directly (like GitHub or Stripe). Apps that store data via the data[]
mechanism (like Cloudflare or one-dollar-stats) return text output — use the
variable value directly rather than dot-accessing properties.
Running steps in parallel
Sequential pipelines are great, but sometimes you need to fetch from three
services at once. Prefix a step with * and it runs in parallel with its
*-prefixed siblings:
All three fetches run concurrently. The next non-parallel step automatically waits for all of them to finish — it acts as an implicit barrier. Use variables to reference the outputs, since there’s no single “previous output” when multiple steps run at once.
The /data step is sequential, so it naturally waits for both fetches.
Barriers between parallel groups
Usually the implicit barrier is all you need. But when you have two parallel
groups where the second depends on results from the first, use ---:
Without ---, all six * steps would form one parallel group — the store
steps would run before {{issues}} and {{prs}} exist.
Sequential prefix
You can prefix a step with - for explicit sequential ordering. This is
purely cosmetic — steps are sequential by default — but it can make a flow more
readable:
Blocks
As flows get larger, you want ways to organize them. Blocks give you that structure.
Separators
--- is a barrier between sections. Steps after a --- wait for everything
above to complete:
A --- also resets conditional chains (if/else-if/else), so conditions
after a barrier start fresh.
Named blocks
Give a group of steps a name with name = on its own line. The block’s final
output is assigned to that variable:
Named blocks are really about readability — they let you scan a long flow and immediately see its structure. The variable name must start with a letter and can contain letters, numbers, hyphens, and underscores.
Each blocks
Sometimes you need to do the same thing to every item in a collection. each
iterates over a variable’s output, running the indented body for each item:
Each iteration receives its item as the implicit input to the first sub-step — you don’t need to reference it by name. The item flows through sub-steps just like any other pipeline input.
Iterate over literal values when you know the set upfront:
Combine multiple variables with + to zip them together:
Or use , to iterate over multiple refs independently:
Inside an each block, sub-steps receive the current item as input, not the
whole collection. Don’t use {{issues.title}} to access properties of the
current item — let the AI extract what it needs from the input, or use an LLM
step to reshape the item first.
* works on each the same way as any line — it makes the block parallel with
its siblings:
Here /chat and the each block run in parallel. Inside the body, steps are
sequential. Want to parallelize within each iteration too? Use * on the inner
steps:
Conditionals
Flows need to make decisions. The conditional system is designed to feel natural — you describe conditions the way you’d explain them to a person, and AI evaluates them against the actual data.
if
Skip subsequent steps in the same block when a condition is false:
The condition is evaluated by AI. Any {{variable}} references are
automatically resolved and shown to the AI alongside the condition:
The AI receives the full value of {{health}} as context — even though the
syntax is concise, the AI has complete information to make an accurate decision.
For exact expression evaluation, use backticks:
else if / else
Chain conditional branches. Only the first matching branch runs:
Once a branch matches, the remaining branches are skipped. A --- separator or
a new if resets the chain.
cancel if
Sometimes you want to bail out of the entire flow early. cancel if stops
everything:
With backtick expressions:
Error handling
Things fail — APIs go down, rate limits hit, services timeout. Pipedown gives you three levels of error control, from lightweight to comprehensive.
Inline suffixes
The quickest way to handle errors — append ? or ! to any step:
?— optional step, gracefully continues on failure!— must-succeed step, retries on failure
? is shorthand for ! ignore. ! is shorthand for ! retry. For quick
one-line error handling, these are all you need.
Retry
Retry the preceding step up to N times with exponential backoff (1s, 2s, 4s, capped at 5s):
Ignore
Swallow the error and keep going:
Fallback
When a step fails, run replacement steps instead. Indent the fallback body:
If the primary fetch fails, the indented fallback steps run and the flow continues with their output. If the fallback also fails, the flow stops.
Wait
Some things need a human in the loop. /wait pauses a flow until someone
approves or a timer expires.
Wait for approval
The flow pauses until Cathy approves. If she denies, the flow fails.
Wait for a duration
Wait for approval with timeout
Wait for Cathy to approve, but auto-continue after 24 hours if no response.
Data storage
Flows don’t just transform data — they can persist it. The workspace data store lets flows save values for display in blocks or later querying.
Auto-storage
Many apps automatically store their results when they run. No explicit /data
step needed:
The daily stats are auto-stored and immediately queryable from blocks:
Manual storage with /data
Use /data upsert to store a named stat, and /data series to append a
time-series data point. Specify the value explicitly with value=:
The value= argument accepts:
- A variable reference:
value={{payments.revenue}}(extracts property from structured output) - An object literal:
value={revenue: {{payments.revenue}}, count: {{payments.count}}} - A plain string:
value=up
Without value=, the data app uses AI to extract a numeric value from the
current pipeline input — useful but less reliable. For precision, always specify
value= explicitly.
Querying stored data in blocks
Blocks query stored data via:
query="key"— fetch a named entryquery={{ app: "appId", type: "typeName", sort: "newest" }}— filter by app and typequery={{ category: "name", type: "series" }}— fetch time-series data
The extract prop pulls a specific property from the stored value object.
Common patterns
Here’s the thing about Pipedown’s minimal syntax — the same small set of features composes into patterns that handle surprisingly complex scenarios. These aren’t special constructs you need to learn. They emerge naturally from what you already know.
Gather and report
The parallel + variables pattern. Fetch from multiple sources at once, then combine:
Conditional notification
Guard with cancel if to bail early when there’s nothing to report:
Retry with fallback
Layer error handling for critical paths — try the primary, retry twice, then fall back to a backup:
Approval gate
Human-in-the-loop before irreversible actions:
Putting it all together
This is where everything clicks. Read through this flow and notice how every concept from this page shows up naturally — the header, parallel fetches, variables, named blocks, conditionals, wait, and notification. Nothing feels bolted on because it’s all the same small syntax composing together:
A scheduled flow that gathers from three services in parallel, combines the results with AI, gets human approval (with a timeout), and delivers the report. Twelve lines of plain text.
Syntax reference
Every element on one page. Keep this open while you’re writing.
| Element | Syntax | Example |
|---|---|---|
| Header (name) | # Title | # Daily Digest |
| Header (description) | plain text after title | Sends a daily summary |
| App step | /app action args | /github fetch open PRs |
| LLM step | plain text | summarize in bullet points |
| Variable (set) | = name at end of step | /stripe payments = rev |
| Variable (reference) | {{name}} in step text | combine {{issues}} and {{rev}} |
| Variable (property) | {{name.prop}} in step text | send {{pr.title}} to team |
| Named block | name = on its own line | gather = |
| Parallel | * prefix | * /github fetch issues |
| Sequential | - prefix | - /stripe payments |
| Separator | --- | --- |
| Each (variable) | each {{var}}: with indented body | each {{issues}}: |
| Each (literals) | each a, b: with indented body | each prod, staging: |
| Each (combine) | each {{a}} + {{b}}: with indented body | each {{repos}} + {{errors}}: |
| If | if condition | if there are results |
| If (expr) | if `expression` | if `{{count}} > 0` |
| Else if | else if condition | else if there are warnings |
| Else | else | else |
| Cancel if | cancel if condition | cancel if no data |
| Cancel if (expr) | cancel if `expression` | cancel if `{{total}} == 0` |
| Optional step | ? suffix | /stripe payments ? |
| Must-succeed step | ! suffix | /notify push @team ! |
| Retry | ! retry N | ! retry 3 |
| Ignore errors | ! ignore | ! ignore |
| Fallback | ! fallback | ! fallback |
| Wait (person) | /wait @person | /wait @cathy |
| Wait (duration) | /wait duration | /wait 2h |
| Wait (both) | /wait @person duration | /wait @cathy 24h |