foldrun 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/flows.md ADDED
@@ -0,0 +1,670 @@
1
+ # Flows
2
+
3
+ A flow is a numbered list of steps in a markdown file under `flows/`. Steps run
4
+ in ascending group order, and each receives the results of **every** earlier
5
+ group, oldest first — a group-3 reporter sees what group 1 measured, not only
6
+ what group 2 diagnosed. Output passing is the default, not configuration.
7
+ The handoff is capped (about 30,000 characters); over it, the oldest groups
8
+ are dropped whole and the newest kept.
9
+
10
+ ```markdown
11
+ ---
12
+ name: weekly-digest
13
+ trigger: schedule
14
+ schedule: "0 8 * * MON"
15
+ timezone: Australia/Sydney
16
+ ---
17
+
18
+ 1. [[researcher]] — gather this week's competitor updates
19
+ 2. [[writer]] — turn the research into a 300-word digest at outputs/digest.md
20
+ ```
21
+
22
+ ## The step line
23
+
24
+ ```
25
+ <group><marker>. [[<agent>]] — <instruction>
26
+ ```
27
+
28
+ - **Group** — steps sharing a number run **in parallel**; the next group starts
29
+ when all of them finish and receives every result, labelled per agent. A
30
+ gate (`!`, `ask:`, `wait: event`) holds its whole group, so a gated step
31
+ numbered the same as another starts beside it on approval, not after it —
32
+ `check` warns about that, and about a gap in the numbers (3, 3, 4, 5, 5!, 7
33
+ is a renumbering that missed one step).
34
+ - **Marker** — `?` makes the step optional (it fails without failing the flow);
35
+ `!` parks it for a human, the shorthand for `approve: true`.
36
+ - **Target** — the first link on the line is structural: the agent that runs,
37
+ or `[[flow:name]]` for a whole flow composed in place. Cycles, and nesting
38
+ past three levels, are errors. Options under a `[[flow:name]]` line are not
39
+ applied to the nested steps — only `?` and the instruction carry — and
40
+ `check` names any it finds.
41
+ - **Instruction** — everything after the dash. Other `[[links]]` in it resolve
42
+ to real paths before the model sees them.
43
+
44
+ Options are indented lines under the step.
45
+
46
+ ## What a gate shows
47
+
48
+ A parked step's approval box shows the previous group's replies, rendered,
49
+ and every file under `storage/` those replies name — images as pictures,
50
+ markdown and text as text. `preview:` makes that explicit:
51
+
52
+ ```
53
+ 5! [[publisher]] — commit and push
54
+ preview: draft/*.mdx, draft/images/*.webp
55
+ ```
56
+
57
+ The globs are resolved against `storage/` at the moment the run parks, so
58
+ the approver reads the whole article and sees its cover before saying GO,
59
+ whether or not the reviewer remembered to name them. The rule for the step
60
+ before a gate is still to reply with a summary and name its files; the
61
+ declaration is what makes the preview not depend on that.
62
+
63
+ An instruction may wrap. Indent the continuation, the way an option is
64
+ indented, and the whole thing is one instruction:
65
+
66
+ ```
67
+ 1. [[writer]] — write the article and make sure it covers
68
+ the three points we agreed, then hand it to the editor
69
+ with a note about tone
70
+ timeout: 900
71
+ ```
72
+
73
+ **An instruction ends where the step ends.** A line joins the instruction
74
+ only if it follows the step line, or a line that already joined it, with
75
+ nothing in between: the first blank line, option, unindented line, heading,
76
+ bullet, quote or bold paragraph finishes the instruction, and everything
77
+ after it is prose. Without that rule a flow's commentary below the last step
78
+ became part of that step's orders — the indented *wrapped lines* of a bullet
79
+ list read as instructions while the `- ` lines around them did not.
80
+
81
+ Unindented prose between steps stays prose and is ignored, so a flow file's
82
+ commentary is undisturbed.
83
+
84
+ `foldrun check` notes every instruction that wraps. It is read in full, but
85
+ the parser had to decide where it stopped, and the line under a wrapped
86
+ instruction is one edit away from being a paragraph. Prefer one line, however
87
+ long, so what the agent is told does not depend on where the next paragraph
88
+ begins.
89
+
90
+ ## Every step option
91
+
92
+ | option | |
93
+ |---|---|
94
+ | `when:` | run only if the **previous group's** result has a line **beginning** with this marker — non-exclusive, every match runs. `when: rows of <path>` runs only if that CSV has a data row |
95
+ | `case:` | exclusive branch: the **first** `case:` whose marker begins a line of the previous group's result runs, the rest are routed past |
96
+ | `else:` | runs only when no `case:` in the group matched |
97
+ | `each:` | fan out — `lines`, `items`, or `rows of <path>` |
98
+ | `max:` | cap the fan-out (default 10, hard cap 20; dropped items are logged) |
99
+ | `parallel:` | how many of an `each:` step's instances run at once (1 to 20); unset is all of them |
100
+ | `loop:` | send the flow back one group up to N times (capped at 5) |
101
+ | `until:` | the marker that ends the loop |
102
+ | `output:` | `json` — the reply must end with one ```json block, parsed and passed on losslessly |
103
+ | `schema:` | with `output: json` — the JSON Schema the value must satisfy: one-line JSON, a YAML block under the key, or a file under the workspace |
104
+ | `model:` | override the tier for this step |
105
+ | `effort:` | override how hard it thinks for this step |
106
+ | `retry:` | attempts after the first, clamped to 5. Each retry waits — 15 s, 30 s, 1 m, 2 m, 4 m, jittered — holding no sandbox while it waits; a stop ends the wait. An attempt the cluster ended for want of memory or disk (OOMKilled, Evicted) is retried one `size:` class up. An `outward` step whose check failed after its tools ran is not retried — the retry would send again; `check` and `deploy` warn on `retry:` on an outward step, because a failure mid-send is still retried |
107
+ | `timeout:` | seconds, or `90s` / `15m` / `4h` / `3d` — **the only clock there is** |
108
+ | `max_turns:` | the most model turns before the step is stopped (1 to 500) — the third bound beside `budget:` and `timeout:` |
109
+ | `limits:` | this step's call limits, on one line — `limits: {web.search: 10, calls: 50}` — over the agent's, key by key. A call past one is refused, never run ([Tools](tools#limits--how-many-calls-a-step-may-make)) |
110
+ | `verify:` | a shell command, or an assertion (`contains:`, `matches:`, `file:`, `judge:`). The shell runs in the agent's folder and gets the step's final turn in the file `$FOLDRUN_REPLY_FILE`; `workspace/…` reaches the workspace root there, and in `file: workspace/storage/x.md`; `node workspace/tools/x/check.mjs` runs as its own main module, so a check it makes counts ([Agents](agents#where-files-live-from-the-agents-point-of-view)). The check runs on what is left of the step's `timeout:` (at least 30 s of it); with no `timeout:` it has no clock either. A stop ends it |
111
+ | `approve:` | park until a person releases it |
112
+ | `ask:` | the same gate carrying a question; the typed answer reaches the prompt |
113
+ | `preview:` | what the gate shows: paths under `storage/` (a `workspace/storage/` prefix is accepted), comma-separated, globs allowed — `draft/*.mdx, draft/images/*.webp`; at most 12 files |
114
+ | `wait:` | `3d` (`s`/`m`/`h`/`d`, capped at 30 days) or `event` |
115
+ | `on-fail:` | another agent takes the step over, with the failure as context |
116
+ | `delegate:` | the step's agent picks who runs next, from this set |
117
+
118
+ ### Routing
119
+
120
+ ```markdown
121
+ 1. [[classifier]] — reply with exactly one word, BUG or QUESTION
122
+ 2. [[debugger]] — investigate and fix
123
+ case: BUG
124
+ 2. [[writer]] — answer it clearly
125
+ case: QUESTION
126
+ 2. [[triager]] — neither label fit; say what is missing
127
+ else: true
128
+ ```
129
+
130
+ `when:` is the non-exclusive sibling — every matching step runs — which is why
131
+ routing has its own vocabulary instead of a mode on `when:`.
132
+
133
+ **A marker leads a line.** `when: BLOCKED` and `case: BUG` look for a line
134
+ that begins with the marker, not for the word anywhere in the text. Markdown in front of it is
135
+ fine — a heading, a bullet, bold — and a longer word that merely starts the
136
+ same way is not a match.
137
+
138
+ **And it is the previous group's line.** The prompt hands a step every
139
+ earlier group; a gate reads only the nearest earlier group that produced a
140
+ result. A classifier's `BUG` two groups back does not reopen `case: BUG`
141
+ after the debugger has answered it, and a group routed past entirely is
142
+ looked through rather than read as silence.
143
+
144
+ This matters more than it sounds. It used to be a plain search of the whole
145
+ text, which meant an agent writing
146
+
147
+ > There are no BLOCKED items this week.
148
+
149
+ opened the gate. Saying a marker is absent necessarily names it, so the more
150
+ carefully an agent explained itself the more likely it was to trip its own
151
+ condition. Write verdicts as headlines — the line leads with the marker,
152
+ which is the house convention anyway — and both readings agree.
153
+
154
+ **Or ask the data.** `when: rows of workspace/storage/queue.csv` runs the step
155
+ only if that CSV has a data row — the same file, read the same way, as
156
+ `each: rows of`. Use it for the steps after a fan-out:
157
+
158
+ ```markdown
159
+ 1. [[scan]] — write every open item to storage/queue.csv
160
+ 2. [[writer]] — one draft per item
161
+ each: rows of workspace/storage/queue.csv
162
+ 3. [[sheet]] — collate the drafts
163
+ when: rows of workspace/storage/queue.csv
164
+ 4! [[poster]] — publish what was approved
165
+ when: rows of workspace/storage/queue.csv
166
+ ```
167
+
168
+ On a day with nothing in the queue the writer skips itself, and without the
169
+ `when:` the sheet and the gate would still run — a skipped fan-out leaves no
170
+ result, so there is no marker to read, and a person is asked to approve
171
+ nothing. With it, the run ends after the scan. A missing file, a header on
172
+ its own, or a path outside the workspace all skip, and the step says which.
173
+
174
+ For `case:` it matters more, because routing is exclusive. A label picked
175
+ out of a sentence does not merely run an extra step: "this is not a
176
+ COMPLAINT, it is a QUESTION" used to reach the complaints handler and skip
177
+ the right branch entirely.
178
+
179
+ ### The critic loop
180
+
181
+ ```markdown
182
+ 1. [[writer]] — draft the post
183
+ 2. [[editor]] — review it; end your reply with APPROVED when it is ready
184
+ loop: 3
185
+ until: APPROVED
186
+ ```
187
+
188
+ The marker must stand **alone on its own line** (case-insensitive; emphasis and
189
+ trailing punctuation are ignored). "APPROVED once the citation is fixed"
190
+ contains the word but is a rejection, and reading it as a pass would end the
191
+ loop with the correction unmade. Notes may follow on later lines, so an
192
+ approval with a list of nits still passes. When no such line appears, the
193
+ previous group and this one run again, at most `loop:` extra cycles; a loop
194
+ that exhausts its budget fails the step.
195
+
196
+ ### Fan-out
197
+
198
+ ```markdown
199
+ 1. [[scout]] — list the competitor URLs, one per line
200
+ 2. [[analyst]] — analyse this one site
201
+ each: lines
202
+ max: 10
203
+ ```
204
+
205
+ `parallel: 2` under an `each:` step runs two instances at a time instead of
206
+ all of them — for twenty browser steps against one site, or one vendor's
207
+ rate limit. An instance waiting for a slot rents nothing, and the fan-out
208
+ event says `6 instances, 2 at a time (parallel:)`. Unset is what fan-out
209
+ always did: everything at once.
210
+
211
+ `each: lines` splits what the previous step **concluded** — its final reply,
212
+ not every turn it wrote along the way — into non-empty lines (list markers
213
+ stripped). An agent that narrates while it works ("Now let me write the
214
+ file…") does not fan those sentences out. For a list that must survive
215
+ exactly, have the step write a file and use `each: rows of`. `each: items` reads the array an earlier `output: json` step
216
+ returned. `each: rows of workspace/storage/leads.csv` runs one instance per data
217
+ row, each receiving the header plus its row. Instances run in parallel and are
218
+ labelled by their item in the next group's context.
219
+
220
+ ### Structured handoff
221
+
222
+ ```markdown
223
+ 1. [[extractor]] — return the rows you found
224
+ output: json
225
+ 2. [[checker]] — check this one row
226
+ each: items
227
+ verify: jq -e '.total > 0'
228
+ ```
229
+
230
+ `output: json` is the one structured handoff in the grammar: the value reaches
231
+ the next group beside the prose as `<previous_step_data>` — a URL step 1 found
232
+ arrives at step 3 as the URL, not as a re-reading of step 1's paragraph — and
233
+ rides the run record as `data`. A shell `verify:` receives it on stdin.
234
+
235
+ `schema:` says what shape that value must have:
236
+
237
+ ```markdown
238
+ 1. [[extractor]] — return the rows you found
239
+ output: json
240
+ schema:
241
+ type: array
242
+ items:
243
+ type: object
244
+ required: [name, url]
245
+ properties:
246
+ url: { type: string, pattern: "^https://" }
247
+ ```
248
+
249
+ One-line JSON works too (`schema: {"type": "array"}`), and so does a file
250
+ under the workspace (`schema: workspace/schemas/lead.json`, JSON or YAML). The
251
+ schema is quoted whole in the step's prompt, so the model is told what it
252
+ must produce rather than left to guess from field names, and the value is
253
+ checked where it is parsed. One that does not fit fails the step with the
254
+ field named — `schema: the value does not fit — /0/url: does not match
255
+ ^https://` — the same failure as no value at all. The check is the
256
+ hand-written subset of JSON Schema: types, `required`, `properties`,
257
+ `enum`, ranges, `pattern`, `items`, `additionalProperties`, `anyOf` /
258
+ `oneOf` / `allOf` / `not`, and a local `$ref`; formats are accepted and not
259
+ checked, so a date that must be a date belongs in a `verify:`.
260
+
261
+ ### Verification is where arithmetic goes
262
+
263
+ Step options are matched as **text**, deliberately. Anything that must be
264
+ decided by a number belongs in a command that can be tested:
265
+
266
+ ```markdown
267
+ verify: jq -e '.total > 0'
268
+ verify: contains: $34
269
+ verify: judge: quotes the real price
270
+ ```
271
+
272
+ `contains:`, `not-contains:` and `matches:` test the step's **conclusion** —
273
+ its final reply, the same text the run's headline is read from — so a step
274
+ that narrated between tool calls and then opened its answer with `BAD — …`
275
+ passes `matches: ^BAD\b`. `judge:` grades the whole result.
276
+
277
+ The command sees what the step's scripts saw — its secrets, `FOLDRUN_RUN_ID`,
278
+ `FOLDRUN_AGENT`, `FOLDRUN_WORKSPACE`, `FOLDRUN_DATE`, `TZ` — so a marker a step leaves on disk can
279
+ be checked to name *this* run. That matters: a run's copy-back never
280
+ propagates a deletion, so a marker from an earlier run rides into every later
281
+ sandbox. `judge:` is a toolless fast-tier grading call; the other four cost
282
+ nothing.
283
+
284
+ ### Delegation
285
+
286
+ ```markdown
287
+ 2. [[triager]] — decide who should take this
288
+ delegate: enricher, emailer, writer
289
+ ```
290
+
291
+ The agent ends its reply with `agent: instruction` lines, choosing only from
292
+ the declared set (at most five). The picks run as a fresh group immediately
293
+ after, and both the set and the picks are on the record. Choosing nobody is a
294
+ normal outcome, recorded as one.
295
+
296
+ ## Flow frontmatter
297
+
298
+ | field | |
299
+ |---|---|
300
+ | `name` | the flow's identity |
301
+ | `description` | what it is for |
302
+ | `trigger` | how it starts — see below |
303
+ | `schedule` / `timezone` | 5-field cron, and the zone it fires in — which is also the calendar every step in this flow works to (see below) |
304
+ | `at` | ISO 8601 instant, for `trigger: once` |
305
+ | `after` / `on` | the flow to chain on, and whether on `completed` (default), `failed`, `blocked` or `any` |
306
+ | `path` | the `storage/` prefix to watch, for `trigger: storage` |
307
+ | `url` / `every` | what to poll and how often (default 15m), for `trigger: watch` |
308
+ | `signature` / `signing_secret` | `github` \| `stripe` \| `slack` \| `hmac`, and the vault entry to check it against |
309
+ | `model` / `effort` | defaults for every step |
310
+ | `budget` | the most one run may spend, in USD — a number, or `unlimited`; unset is no limit |
311
+ | `overlap` | `skip` or `queue` when a run is already live |
312
+ | `priority` | `high`, `normal` or `low` — where this flow's runs stand in the queue against everyone else's |
313
+ | `idempotency` | the header (or `body:<field>`) that identifies a delivery, so the same one twice is one run |
314
+ | `throttle` | the least time between two runs of this flow |
315
+ | `debounce` | how long a burst of events must go quiet before it becomes one run |
316
+ | `catchup` | `last` (default) or `none` — what a schedule does about a fire the platform slept through |
317
+ | `disable_after` | consecutive failures after which this flow stops firing |
318
+ | `pause_when` | a marker (`DONE`, `NOTHING LEFT`): when a step's reply in the flow's latest run leads a line with it, the schedule and every other trigger stop firing the flow, and you are told once — for a flow whose job can finish. Worked out from that run, so nothing needs un-setting: run the flow by hand when there is new work, and a run that does not say the marker resumes it. Test runs and stopped runs do not count |
319
+ | `sla` | how long a run is expected to take; past it, one notification |
320
+ | `approvers` | who may decide this flow's gates — addresses, or `admins` |
321
+ | `approve_within` | how long a gate waits before the run is rejected |
322
+ | `live` | `true` to make this flow's runs real where the platform would otherwise make them [test runs](runs#test-runs): in a preview workspace, and as an eval's subject. Only ever an opt-out of the safe default |
323
+
324
+ Every duration here is written the way `wait:` is — `45s`, `10m`, `2h`, `3d`,
325
+ or bare seconds — and capped at thirty days.
326
+
327
+ ### `timezone:` — the clock the steps work to
328
+
329
+ `timezone:` is one value doing two jobs: the cron fires in it, and every step
330
+ of the flow works to it. `TZ`, the date the agent is told, and `$FOLDRUN_DATE`
331
+ in its scripts and its `verify:` shell all follow it — so a flow scheduled at
332
+ 08:00 Sydney writes files stamped with the Sydney date, not the container's.
333
+
334
+ It resolves nearest-wins, and each level inherits the one above unless it
335
+ says otherwise:
336
+
337
+ ```
338
+ agent frontmatter → this flow's timezone: → workspace AGENTS.md →
339
+ account AGENTS.md → FOLDRUN_TIMEZONE → UTC
340
+ ```
341
+
342
+ An agent that names its own zone overrides the flow's, for its steps only.
343
+ The value is either an IANA name (`Australia/Sydney`) or a plain fixed offset
344
+ — `UTC+10`, `GMT-5`, `+10:00`, `-05:30`, or a bare `UTC`:
345
+
346
+ ```yaml
347
+ timezone: UTC+10
348
+ ```
349
+
350
+ `foldrun check` refuses anything else. A bad value that reaches a run never
351
+ fails a step: the step falls through to the next level and says once, in the
352
+ trace, what it could not read and what it used instead.
353
+
354
+ ### Triggers
355
+
356
+ | `trigger` | starts when | input |
357
+ |---|---|---|
358
+ | `manual` (default) | a person clicks Run, `foldrun invoke`, or an API call | optional task |
359
+ | `schedule` | the cron matches | none |
360
+ | `once` | the instant in `at:` passes — fires once | none |
361
+ | `webhook` | an HTTP POST reaches the flow's hook URL | the request body |
362
+ | `email` | a message arrives at the flow's inbox URL | from, to, subject, text |
363
+ | `flow` | the flow in `after:` settles the way `on:` says — `completed` (the default), `failed`, `blocked` or `any` | that run's id, status, verdict, summary, result |
364
+ | `storage` | a file lands under `path:` | the paths and who wrote them |
365
+ | `watch` | the content at `url:` changes | the new content |
366
+ | *(composed)* | another flow reaches a `[[flow:name]]` step | the previous step's results |
367
+
368
+ `on: completed` does **not** fire after a run whose verdict is `BLOCKED`: a
369
+ follow-up to a publish that refused itself should not run as if the publish
370
+ had happened. Write `on: blocked` for the flow that should react to exactly
371
+ that, or `on: any` for both.
372
+
373
+ A flow may not chain on itself. First sightings for `watch` record without
374
+ firing, and a `once` instant more than six hours gone at first sighting is
375
+ recorded and never fired.
376
+
377
+ ### How many of those starts are real
378
+
379
+ A trigger says what happens. These four say how much of it becomes a run,
380
+ and each is a literal in the flow file rather than anything the platform
381
+ decides on your behalf.
382
+
383
+ `idempotency:` names where the delivery's own identifier lives — a request
384
+ header (`idempotency: x-github-delivery`) or a top-level field of a JSON
385
+ body (`idempotency: body:message_id`, case-sensitive, a scalar). The same identifier twice inside
386
+ twenty-four hours starts one run; the second delivery is answered `200`
387
+ with `started: false` and a reason, because a sender that retries must not
388
+ be told to try again. It is a **name**, never an expression: nothing is
389
+ computed, combined or nested. Only `trigger: webhook` and `trigger: email`
390
+ carry a delivery to read one from.
391
+
392
+ `throttle: 15m` is a floor between runs. A trigger arriving inside the
393
+ window is dropped and logged. Every run resets the window, including one a
394
+ person started, because the window is about how often the work happens.
395
+
396
+ `debounce: 2m` waits for the burst to stop. Each event pushes the moment
397
+ out; when the events stop for the whole window, one run starts with the
398
+ **last** event as its task and a note saying how many collapsed. A deploy
399
+ that writes forty files under a watched prefix is one run, not forty.
400
+
401
+ `catchup:` is what a schedule does about a fire that happened while nothing
402
+ was running. `last` — the default, and the behaviour that always applied —
403
+ makes one run for however many were missed. `none` skips it and waits for
404
+ the next real occurrence, for a desk whose 07:00 report is worth nothing at
405
+ 11:00. A fire under a minute and a half late is never a make-up run; that is
406
+ a tick running slightly behind.
407
+
408
+ `disable_after: 5` stops firing a flow whose last five automatic runs all
409
+ failed, and sends one notification saying so. It is worked out from the runs
410
+ themselves rather than stored as a flag, so it clears itself: run the flow by
411
+ hand after a fix and one success is enough. A run a person started counts
412
+ only when it succeeds; its failure is a test, not a verdict. A run someone
413
+ stopped is not a failure of the flow either, so bulk-stopping runs after a
414
+ bad deploy never switches flows off.
415
+
416
+ `sla: 20m` is an expectation, not a limit. A run still going well past it is
417
+ reported once and **left running**: the platform sets no clock of its own,
418
+ and a slow run may still be doing the work. What to do about it is yours.
419
+
420
+ ### Who answers a gate
421
+
422
+ `approvers:` is the list of email addresses that may decide this flow's
423
+ gates, or the single word `admins` for anyone at admin or above. Unset, the
424
+ existing rule applies: anyone who can run the workspace can answer.
425
+
426
+ Whatever the list says, **nobody approves a run they started themselves**. A
427
+ gate exists so a second person looks, and a run someone kicked off and then
428
+ waved through is a gate that never happened. Rejecting your own run is
429
+ always allowed — stopping something needs no second opinion. An API key
430
+ carries a role but no address, so a key passes this rule and only an
431
+ `approvers:` list constrains it.
432
+
433
+ `approve_within: 2d` stops a gate waiting forever. The deadline is stamped
434
+ when the gate opens, not when the run started, so it means two days from
435
+ being asked. Past it the gate is **rejected** and the run fails, which is
436
+ the only safe way for a clock to answer a question a person was asked.
437
+ Unset, a gate waits indefinitely, which remains the default.
438
+
439
+ ### Cost and overlap
440
+
441
+ `budget: 5` is a hard cap. Between groups, a run that has reached it does
442
+ not start the next group — the rest are skipped as "over budget" and the
443
+ run fails saying so. What counts as spent is the whole bill — the tokens
444
+ **and** the sandbox seconds behind them — so a step that calls no model is
445
+ capped too. Within a group, each step launched together is given
446
+ an equal share of what is left and **stops itself mid-turn** when its
447
+ share is spent — the step's usage is priced as each turn arrives, from the
448
+ catalogue's rate for the model (a conservative Opus-class rate when the
449
+ catalogue has none) — so a fan-out of twenty steps cannot end the run
450
+ twenty steps over: the shares sum to the remainder. The stopped step fails
451
+ with `over budget — this step reached $x of its $y ceiling mid-turn`. It is
452
+ a literal, never an expression, so the cap is readable off the file. An
453
+ agent's own `budget:` — the most it may spend in one run — applies to its
454
+ steps too, and the tighter of the two wins. It is shared the same way: the
455
+ agent's steps running at once split what is left of it, so a fan-out of
456
+ twenty copies cannot spend the agent's cap twenty times. The workspace's and the
457
+ account's caps over time still apply on top.
458
+
459
+ `overlap:` decides what a new fire does while a run of the same flow is live.
460
+ `skip` consumes the occurrence — a cron refiring over yesterday's long run
461
+ almost never means "run two". `queue` starts it but holds it until the live one
462
+ finishes. Unset, runs may overlap.
463
+
464
+ `priority:` decides where the flow's runs stand when the platform has more
465
+ runs than slots. `high` is claimed before the round-robin, `low` after it,
466
+ `normal` (unset) takes its turn. It adds to the account's own `priority:`
467
+ (the plan's lane, set in the account's `AGENTS.md`), so a high flow on a low
468
+ account is normal. The account's `concurrency:` still bounds how many of its
469
+ runs are live at once — a high lane jumps the queue, it does not hold the
470
+ fleet.
471
+
472
+ ## Model resolution
473
+
474
+ Nearest wins: the step, then the flow, then the agent's own frontmatter. The
475
+ run trace names which level won, so "why did this run on haiku" is answerable
476
+ from the run rather than by opening three files.
477
+
478
+ The consequence people miss: a flow with `model: fast` in its frontmatter
479
+ runs **every** step on the fast tier, whatever each agent's own `model:`
480
+ says. To lift one step, put `model:` on that step. A file-composing step is
481
+ the one to check — on the fast tier an agent asked to write a long file may
482
+ write it a line per call, re-sending its whole context each time; one
483
+ translator did that 92 times and cost $22 for twelve lines. The run trace
484
+ shows the tier (`model: haiku — flow`) and the write count.
485
+
486
+ ## Editing on the canvas
487
+
488
+ The dashboard's Flows page lists each flow **collapsed**: its name, file,
489
+ trigger and schedule, step count, the last runs and what they cost, Run flow,
490
+ and a count of what `foldrun check` flags. Click the name to open it (Expand
491
+ all / Collapse all sit by the search); a flow whose check has errors starts
492
+ open, a search opens the flows it matches, `#flow-<name>` in the URL opens that
493
+ one, and each viewer's open or shut is remembered in their browser. Open, it
494
+ shows each flow on a **canvas**: the trigger, then
495
+ one column per group, left to right, parallel steps stacked in their column.
496
+ `case:`/`else:` branches are labelled edges, `loop:` is an arc back to the
497
+ group before, `on-fail:` a red dashed line to the rescuer, a `[[flow:x]]` step
498
+ a dashed node, and a fan-out step a stack. Under each step hangs its agent's
499
+ team: sub-agents (`subagents:`) on solid lines, consults (`agents:`) on dotted
500
+ ones, plus the model, the tool count and whether it **may ask you** (`ask` in
501
+ `tools:`). Problems `foldrun check` would report sit on the node they are about.
502
+
503
+ The layout is worked out from the markdown every time — nothing about
504
+ positions is saved. Dragging a step into a column runs it in parallel;
505
+ between columns makes it its own step; a move the grammar cannot express (a
506
+ `case:` step first, with nothing to route on) is refused with the reason.
507
+ Everything a drop writes is ordinary flow grammar:
508
+
509
+ | Palette block | What it writes |
510
+ |---|---|
511
+ | an agent, Chain | a new step in its own group where it is dropped |
512
+ | Parallel | a new step in the group it is dropped on |
513
+ | Run another flow | `N. [[flow:x]]` |
514
+ | Router | a triage step, then one group of `case: X` steps and an optional `else: true` |
515
+ | Fan-out | `each: lines`, `items` or `rows of <path>`, and `max:` |
516
+ | Evaluator loop | `loop: N`, `until: MARKER`, optionally `verify: judge: …` |
517
+ | Approval gate | the `!` marker (`approve: true` on a `?` step) |
518
+ | Ask a person before | `ask: <question>` |
519
+ | Wait | `wait: 30m` or `wait: event` |
520
+ | Rescue | `on-fail: [[agent]]` |
521
+ | Orchestrator + workers, Consult | the agent added to `subagents:` / `agents:` in the step's **agent file** |
522
+
523
+ The last row, and the "may ask you" toggle, edit the agent's own file, which
524
+ every flow that uses the agent shares — the canvas says which flows before it
525
+ writes. A sub-agent without a `description:` is asked for one first. The
526
+ **List** tab is the older card view; the choice is remembered per browser.
527
+
528
+ To **delete a step**, use the × on its node (shown on hover or focus, always
529
+ on the selected step), the Delete or Backspace key on a selected step, or
530
+ **Delete step** in the step's panel. It asks first, naming the step and what
531
+ else changes: `case:`/`when:` steps after it that will now route on the step
532
+ before, the groups that renumber, the other steps in a parallel group that
533
+ stay. The step line and its indented options go; groups after an emptied one
534
+ renumber so there is no gap; prose around the steps stays and the agent's own
535
+ file is not touched. A delete that would give `foldrun check` a new error is
536
+ refused. The × on a sub-agent or consult chip removes it from the agent's
537
+ `subagents:` / `agents:` the same way, after the same confirm.
538
+
539
+ ### Runs on the canvas, and the controls
540
+
541
+ A canvas opens fitted to its card once the card is open — the whole flow
542
+ across when it fits (a five-step flow on a laptop-width page), otherwise from
543
+ the trigger, to pan — and is as tall as the drawing. Once you pan or zoom, the
544
+ view is yours until the page reloads.
545
+
546
+ While a run of the flow is live, each step is coloured by where that run is —
547
+ pending, running, done, failed, skipped, waiting for you — with how long it
548
+ has taken and what it has cost, updating as the run moves. Otherwise each step
549
+ wears the last finished run's status, time and cost in grey. The squares above
550
+ the canvas are the recent runs: click one to colour the canvas by it, × to go
551
+ back. A step's record is found by agent and group order, since a run is not
552
+ the flow one for one: a fan-out's items and an `on-fail:` rescuer count
553
+ towards the step they came from, and a `[[flow:x]]` step stands for the nested
554
+ flow's steps — the steps after it are matched from the end of the run, so an
555
+ agent the nested flow also runs is not taken for the one after it. A step
556
+ added since that run has no record and shows none.
557
+
558
+ | Control | What it does |
559
+ |---|---|
560
+ | ⋯ on a step, or right-click | **Run from here** (a new run from the last finished one, earlier steps carried — asks first unless that run was a test), **Test this step alone** (that agent once, with the step's instruction, as a test run), **Pause before** (the `!` marker on or off), Duplicate, Copy, Paste after, Fold the parallel group, Delete |
561
+ | Duplicate (⌘/Ctrl+D) | a copy of the step and its options, in the same group |
562
+ | Copy, Paste (⌘/Ctrl+C, V) | the step's markdown — its line and options — so it pastes into any flow, here or in another tab; pasted steps go after the selection and keep which of them ran in parallel |
563
+ | Undo, Redo (⌘/Ctrl+Z, Shift+⌘/Ctrl+Z) | the last 50 flow edits made on this canvas, each written back as a new revision; refused, and the history cleared, when the file changed some other way since |
564
+ | Shift-click, Shift-drag | select several steps: drag them together (onto a column they join it, between columns they become one group), Delete removes them in one edit, Copy copies them |
565
+ | ⌘/Ctrl+K | the canvas's commands: add a step or pattern, run or test-run the flow, go to step N, fold a group, the minimap, undo. Right after you touch a canvas; elsewhere it is the page's Find |
566
+ | M, ? | the minimap (remembered in this browser), the shortcut sheet |
567
+ | Fold | a parallel group drawn as one node — a view choice, nothing is written |
568
+ | ⚙ Options → Call limits | rows of key and count written as `limits: {web.search: 10, calls: 300}` on the step — a key is `web.<action>`, `web`, `calls` or one of the agent's tools, a count a whole number from 1; the node then shows a `limits:` chip |
569
+
570
+ Starting a live run from the canvas always asks first. No shortcut fires while
571
+ you type in a field or an editor.
572
+
573
+ The same edits from a terminal, written by the same code: `foldrun flow add
574
+ <flow> <pattern>` for a palette block (it prints the diff and refuses one
575
+ `foldrun check` would call an error), `foldrun flow rm-step <flow> --step
576
+ <n|agent>` for the delete, `foldrun flow dup-step <flow> --step <n|agent>`
577
+ for Duplicate, `foldrun flow copy-step` and `foldrun flow paste` for Copy
578
+ and Paste, `foldrun flow move-step <flow> --step <n|agent> --group <g> |
579
+ --after <g>` for a drag, `foldrun flow set <flow> --step <n|agent>
580
+ key=value …` for ⚙ Options (call limits included), `foldrun flow trigger`
581
+ for the trigger picker, `foldrun flow draft "…"` for Draft with AI,
582
+ `foldrun agent link <agent> --subagent | --consult | --can-ask` (and
583
+ `unlink`) for the docks and the toggle, and `foldrun flow show <flow>` for
584
+ the canvas itself, as text. Undo's counterpart is `foldrun history restore
585
+ <path> --id <revision>`. See the [CLI reference](cli).
586
+
587
+ ### History
588
+
589
+ **History** on a flow's card opens every revision of its file — who saved
590
+ it, when and with what message — the same history `GET …/history` serves.
591
+ Pick one to see what it changed, or **Compare with current** to see it
592
+ against the file as it is now. **Restore this version** writes that
593
+ revision's text back as a new revision, `restored <rev> from history`, so a
594
+ restore is undone the same way. It is refused, and nothing is written, if the
595
+ file changed after the drawer opened (a canvas edit in another tab, a
596
+ deploy): reopen History to see the newer version first. From a terminal:
597
+ `foldrun history flows/<file>.md --to <workspace>` lists the revisions and
598
+ `foldrun history restore flows/<file>.md --id <revision>` restores one.
599
+
600
+ ### Run with saved inputs
601
+
602
+ **Run with…** on a flow's card starts it with a saved input set — a named
603
+ task. The sets are markdown in `evals/`: the flow's own
604
+ `evals/<flow>-inputs.md`, and every case of an eval that runs this flow
605
+ (each already has a `task:`). **Save current as input set** writes the task
606
+ you typed into the inputs file:
607
+
608
+ ```markdown
609
+ ---
610
+ name: publish-inputs
611
+ flow: publish
612
+ inputs: true # saved inputs, not a test
613
+ trigger: manual
614
+ ---
615
+
616
+ ## rain gauges
617
+ task: Write about cleaning a rain gauge.
618
+
619
+ ## weekly
620
+ task: |
621
+ This week's round-up.
622
+ * five items at most
623
+ ```
624
+
625
+ `inputs: true` makes it a file of inputs rather than an eval: `foldrun check`
626
+ validates the flow it names, but no deploy, Run button or `foldrun eval` runs
627
+ it, because there is nothing to assert. A Test run is the default; a Live run
628
+ asks first. From a terminal, `foldrun invoke <flow> --inputs <name>` sends
629
+ the same set.
630
+
631
+ ### Draft with AI
632
+
633
+ **Draft with AI** (beside New flow) and **Redraft with AI** (on a card) take
634
+ a description in words and propose the flow file, plus an `agent.md` for each
635
+ agent it needs that the workspace does not have — existing agents are used by
636
+ name. The proposal is checked with the deploy's own checks and the flow lint
637
+ `foldrun check` runs; if they find errors the model is shown them and
638
+ repairs once, and anything still wrong is listed beside the files. Each file
639
+ is shown as a diff against what is there. **Nothing is written** until you
640
+ press Save and confirm; then each file is saved as an ordinary revision.
641
+
642
+ The model is the workspace's own Anthropic-format `provider:` when it has
643
+ one (a Chat-Completions provider is refused), else the platform's models,
644
+ billed to the account like a run (one ledger line, flow `ai-draft`). A
645
+ description is at most 4,000 characters and an answer at most 8,000 tokens.
646
+ Drafting needs the editor role.
647
+
648
+ ## What the platform never does
649
+
650
+ It sets **no clock of its own** — not on a step, a script tool, an HTTP tool, a
651
+ verify command, a consult, or a wait for a person. Every limit that exists is
652
+ one written in a markdown file. The run's events say which it is (`timeout:
653
+ 3000s` or `no timeout`) at every step start.
654
+
655
+ Three bounds a step can carry, one each for money, time and turns:
656
+ `budget:` (on the flow or the agent) stops it mid-turn at its share;
657
+ `timeout: 15m` stops it at the clock, whether or not a message has arrived
658
+ — a step waiting on one long tool call is cut at the limit, with the files
659
+ it wrote so far kept; `max_turns: 20` stops it after that many model turns
660
+ (a reply and its tool calls is one), failing with `stopped after 20 turns
661
+ (max_turns: in the flow file)` and what it spent on the record.
662
+
663
+ A value the option cannot read is a check error, never a guess: `timeout:
664
+ abc`, `retry: 9`, `loop: 0`, `each: columns`, `output: yaml` each fail
665
+ `foldrun check` naming the line. Before this, `timeout: 15m` ran as one
666
+ second.
667
+
668
+ A required step failing fails the flow, and the remaining groups are skipped. A
669
+ workspace with one agent and no flows is normal — `foldrun run <agent>` runs it
670
+ as a one-step flow, so there is a single execution path either way.