Flows
A flow is a numbered list of steps in a markdown file under flows/. Steps run
in ascending group order, and each receives the results of every earlier
group, oldest first — a group-3 reporter sees what group 1 measured, not only
what group 2 diagnosed. Output passing is the default, not configuration.
The handoff is capped (about 30,000 characters); over it, the oldest groups
are dropped whole and the newest kept.
---
name: weekly-digest
trigger: schedule
schedule: "0 8 * * MON"
timezone: Australia/Sydney
---
1. [[researcher]] — gather this week's competitor updates
2. [[writer]] — turn the research into a 300-word digest at outputs/digest.md
The step line
<group><marker>. [[<agent>]] — <instruction>
- Group — steps sharing a number run in parallel; the next group starts when all of them finish and receives every result, labelled per agent.
- Marker —
?makes the step optional (it fails without failing the flow);!parks it for a human, the shorthand forapprove: true. - Target — the first link on the line is structural: the agent that runs,
or
[[flow:name]]for a whole flow composed in place. Cycles, and nesting past three levels, are errors. - Instruction — everything after the dash. Other
[[links]]in it resolve to real paths before the model sees them.
Options are indented lines under the step.
Every step option
| option | |
|---|---|
when: |
run only if this text appears in the previous results — non-exclusive, every match runs |
case: |
exclusive branch: the first matching case: in the group runs, the rest are routed past |
else: |
runs only when no case: in the group matched |
each: |
fan out — lines, items, or rows of <path> |
max: |
cap the fan-out (default 10, hard cap 20; dropped items are logged) |
loop: |
send the flow back one group up to N times (capped at 5) |
until: |
the marker that ends the loop |
output: |
json — the reply must end with one ```json block, parsed and passed on losslessly |
model: |
override the tier for this step |
effort: |
override how hard it thinks for this step |
retry: |
attempts after the first, clamped to 5 |
timeout: |
seconds — the only clock there is |
verify: |
a shell command, or an assertion (contains:, matches:, file:, judge:) |
approve: |
park until a person releases it |
ask: |
the same gate carrying a question; the typed answer reaches the prompt |
wait: |
3d (s/m/h/d, capped at 30 days) or event |
on-fail: |
another agent takes the step over, with the failure as context |
delegate: |
the step’s agent picks who runs next, from this set |
Routing
1. [[classifier]] — reply with exactly one word, BUG or QUESTION
2. [[debugger]] — investigate and fix
case: BUG
2. [[writer]] — answer it clearly
case: QUESTION
2. [[triager]] — neither label fit; say what is missing
else: true
when: is the non-exclusive sibling — every matching step runs — which is why
routing has its own vocabulary instead of a mode on when:.
The critic loop
1. [[writer]] — draft the post
2. [[editor]] — review it; end your reply with APPROVED when it is ready
loop: 3
until: APPROVED
The marker must stand alone on its own line (case-insensitive; emphasis and
trailing punctuation are ignored). “APPROVED once the citation is fixed”
contains the word but is a rejection, and reading it as a pass would end the
loop with the correction unmade. Notes may follow on later lines, so an
approval with a list of nits still passes. When no such line appears, the
previous group and this one run again, at most loop: extra cycles; a loop
that exhausts its budget fails the step.
Fan-out
1. [[scout]] — list the competitor URLs, one per line
2. [[analyst]] — analyse this one site
each: lines
max: 10
each: lines splits what the previous step concluded — its final reply,
not every turn it wrote along the way — into non-empty lines (list markers
stripped). An agent that narrates while it works (“Now let me write the
file…”) does not fan those sentences out. For a list that must survive
exactly, have the step write a file and use each: rows of. each: items reads the array an earlier output: json step
returned. each: rows of ../../storage/leads.csv runs one instance per data
row, each receiving the header plus its row. Instances run in parallel and are
labelled by their item in the next group’s context.
Structured handoff
1. [[extractor]] — return the rows you found
output: json
2. [[checker]] — check this one row
each: items
verify: jq -e '.total > 0'
output: json is the one structured handoff in the grammar: the value reaches
the next group beside the prose as <previous_step_data> — a URL step 1 found
arrives at step 3 as the URL, not as a re-reading of step 1’s paragraph — and
rides the run record as data. A shell verify: receives it on stdin.
Verification is where arithmetic goes
Step options are matched as text, deliberately. Anything that must be decided by a number belongs in a command that can be tested:
verify: jq -e '.total > 0'
verify: contains: $34
verify: judge: quotes the real price
contains:, not-contains: and matches: test the step’s conclusion —
its final reply, the same text the run’s headline is read from — so a step
that narrated between tool calls and then opened its answer with BAD — …
passes matches: ^BAD\b. judge: grades the whole result.
The command sees what the step’s scripts saw — its secrets, FOLDRUN_RUN_ID,
FOLDRUN_AGENT, FOLDRUN_DATE, TZ — so a marker a step leaves on disk can
be checked to name this run. That matters: a run’s copy-back never
propagates a deletion, so a marker from an earlier run rides into every later
sandbox. judge: is a toolless fast-tier grading call; the other four cost
nothing.
Delegation
2. [[triager]] — decide who should take this
delegate: enricher, emailer, writer
The agent ends its reply with agent: instruction lines, choosing only from
the declared set (at most five). The picks run as a fresh group immediately
after, and both the set and the picks are on the record. Choosing nobody is a
normal outcome, recorded as one.
Flow frontmatter
| field | |
|---|---|
name |
the flow’s identity |
description |
what it is for |
trigger |
how it starts — see below |
schedule / timezone |
5-field cron, IANA zone (default UTC) |
at |
ISO 8601 instant, for trigger: once |
after / on |
the flow to chain on, and whether on completed (default), failed or any |
path |
the storage/ prefix to watch, for trigger: storage |
url / every |
what to poll and how often (default 15m), for trigger: watch |
signature / signing_secret |
github | stripe | slack | hmac, and the vault entry to check it against |
model / effort |
defaults for every step |
budget |
the most one run may spend, in USD |
overlap |
skip or queue when a run is already live |
Triggers
trigger |
starts when | input |
|---|---|---|
manual (default) |
a person clicks Run, foldrun invoke, or an API call |
optional task |
schedule |
the cron matches | none |
once |
the instant in at: passes — fires once |
none |
webhook |
an HTTP POST reaches the flow’s hook URL | the request body |
email |
a message arrives at the flow’s inbox URL | from, to, subject, text |
flow |
the flow in after: settles the way on: says |
that run’s id, status, summary, result |
storage |
a file lands under path: |
the paths and who wrote them |
watch |
the content at url: changes |
the new content |
| (composed) | another flow reaches a [[flow:name]] step |
the previous step’s results |
A flow may not chain on itself. First sightings for watch record without
firing, and a once instant more than six hours gone at first sighting is
recorded and never fired.
Cost and overlap
budget: 5 is checked between groups: the group that crosses it is the last
one that runs, the rest are skipped as “over budget”, and the run fails saying
so. It is a literal, never an expression, so the cap is readable off the file.
The workspace’s monthly budget: still applies on top.
overlap: decides what a new fire does while a run of the same flow is live.
skip consumes the occurrence — a cron refiring over yesterday’s long run
almost never means “run two”. queue starts it but holds it until the live one
finishes. Unset, runs may overlap.
Model resolution
Nearest wins: the step, then the flow, then the agent’s own frontmatter. The run trace names which level won, so “why did this run on haiku” is answerable from the run rather than by opening three files.
The consequence people miss: a flow with model: fast in its frontmatter
runs every step on the fast tier, whatever each agent’s own model:
says. To lift one step, put model: on that step. A file-composing step is
the one to check — on the fast tier an agent asked to write a long file may
write it a line per call, re-sending its whole context each time; one
translator did that 92 times and cost $22 for twelve lines. The run trace
shows the tier (model: haiku — flow) and the write count.
What the platform never does
It sets no clock of its own — not on a step, a script tool, an HTTP tool, a
verify command, a consult, or a wait for a person. Every limit that exists is
one written in a markdown file. The run’s events say which it is (timeout: 3000s or no timeout) at every step start.
A required step failing fails the flow, and the remaining groups are skipped. A
workspace with one agent and no flows is normal — foldrun run <agent> runs it
as a one-step flow, so there is a single execution path either way.