foldrun 0.5.0 → 0.7.2
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/README.md +98 -15
- package/bin/foldrun.mjs +331 -47
- package/docs/agents.md +483 -0
- package/docs/api-usage.md +156 -0
- package/docs/api.md +435 -0
- package/docs/backups-and-restore.md +171 -0
- package/docs/budgets-and-billing.md +362 -0
- package/docs/cli.md +428 -0
- package/docs/coding-agents.md +224 -0
- package/docs/deploying.md +77 -0
- package/docs/evals.md +90 -0
- package/docs/faq.md +53 -0
- package/docs/flows.md +670 -0
- package/docs/getting-started.md +141 -0
- package/docs/glossary.md +78 -0
- package/docs/harness.md +128 -0
- package/docs/how-it-works.md +255 -0
- package/docs/introduction.md +47 -0
- package/docs/knowledge-and-memory.md +85 -0
- package/docs/notifications.md +320 -0
- package/docs/providers.md +241 -0
- package/docs/runs.md +238 -0
- package/docs/scheduling.md +103 -0
- package/docs/secrets.md +163 -0
- package/docs/security.md +167 -0
- package/docs/self-hosting.md +65 -0
- package/docs/status.md +73 -0
- package/docs/storage-and-sharing.md +118 -0
- package/docs/team-and-access.md +146 -0
- package/docs/tools.md +1810 -0
- package/docs/troubleshooting.md +155 -0
- package/docs/versions.md +79 -0
- package/docs/workspaces.md +160 -0
- package/package.json +15 -5
- package/src/commands.mjs +6984 -331
- package/src/credentials.mjs +3 -0
- package/src/guide.mjs +172 -0
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.
|