workflows · git:20260622.69c562a · 2026-06-22 · sha256 5570cd8aee85b5fb
workflows git:20260622.69c562aA
Immutable. This exact content is served forever at /api/v1/blob/5570cd8aee85b5fb.
---
name: workflows
description: Delegate a big or high-stakes job to a fleet of parallel subagents, orchestrated deterministically; runs unattended and reports back
compatibility: "Designed for Vellum personal assistants"
metadata:
emoji: "⚙️"
vellum:
display-name: "Workflows"
category: "system"
always-candidate: true
activation-hints:
- "Batch — apply one operation to each of MANY items (score / rate / rank / classify / extract / summarize each of a large set)"
- "Comprehensive coverage — exhaustively sweep, audit, or find EVERY instance across a large surface"
- "Research & synthesize — gather across many sources or pages and combine into one answer"
- "Confidence — generate several independent attempts and judge them, or adversarially verify findings before trusting the result"
- "Scale — work too large to finish well in one inline pass"
avoid-when:
- "A single inline answer, a quick lookup, or a small one-off"
- "Interactive, conversational back-and-forth rather than unattended fan-out"
---
A workflow is a short JS/TS script you author that runs in a sandbox and fans work
out across many short-lived **leaf agents**, orchestrated deterministically. Launch
one with `run_workflow` (inline `script` OR saved `name`, exactly one). It returns a
`runId` immediately; the run is asynchronous and you are notified in this
conversation when it completes — **do NOT poll**.
Reach for one when a job is too big, too parallel, or too important for one inline
pass. That is more than batch/map-reduce over many items — it also covers exhaustively
sweeping or auditing a large surface, researching across many sources and synthesizing,
and generating several independent attempts to judge or adversarially verify before
trusting the result. For a single task or a quick lookup, do it inline.
## The script model
These are the load-bearing invariants. Get them wrong and the run misbehaves silently.
### Scripts are SYNCHRONOUS — never `await`
Host functions block and return their result directly. Write straight-line code.
```js
const r = agent("Summarize this thread."); // r is the result, right here
```
Do **not** write `await agent(...)`, and do **not** make the script `async`. An
`async` script deadlocks on its second host call — the sandbox can suspend the main
evaluation stack but not a promise continuation.
### Every script begins with a literal `meta`
The first statement must be a pure-literal export — no computed values, template
strings, or concatenation:
```js
export const meta = {
name: "triage-inbox",
description: "Triage and label inbox messages",
};
```
`meta` is extracted **statically**, without executing the script, so it must be a
plain object literal with string `name` and `description`. The `name` is how a saved
workflow is referenced by `workflow(name)` and the scheduler.
### You must `return` the result
The script body runs as a function. Its result is whatever it `return`s at the top
level — a bare trailing expression (e.g. `result;`) is **discarded** and the run
finishes with no result. Always `return` the value you want surfaced.
```js
const result = agent(`Write the final summary: ${JSON.stringify(parts)}`);
return result;
```
### Determinism (this is what makes runs resumable)
Every leaf call is journaled by sequence number and input hash, so a resumed run can
replay the unchanged prefix instead of re-spawning agents. That only holds if the
script is deterministic, so `Date.now()`, `Math.random()`, and argless `new Date()`
**throw**. Pass any timestamps or random seeds in through `args`.
## Host API
All functions are synchronous from the script's perspective.
| Function | Returns | Notes |
| ---------------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `agent(prompt, opts?)` | the leaf's result | Runs ONE leaf. **Throws** on leaf failure (fails the whole run). |
| `leaf(prompt, opts?)` | a leaf descriptor | Runs nothing on its own; used inside `parallel`/`map`/`pipeline`. |
| `parallel(specs)` | `results[]` | Runs an array of `leaf(...)` descriptors concurrently, results in input order. A failed leaf becomes `null` (never throws). |
| `map(items, build)` | `results[]` | `build(item, i)` returns a `leaf(...)` descriptor per item; runs them like `parallel`. |
| `pipeline(items, ...stages)` | `results[]` | Each `stage(prev, i)` returns a `leaf(...)` descriptor (run an agent) OR a plain value (pass through unchanged — filter/transform locally, no agent spent). Per-stage barrier: stage N+1 starts only after all of stage N finishes. |
| `phase(title)` | — | Marks a named phase for progress reporting. |
| `log(msg)` | — | Emits a progress log line. |
| `usage()` | `{ agentsSpawned, inputTokens, outputTokens }` | Live snapshot so a script can self-moderate. |
| `workflow(name, args?)` | the child's result | Runs a SAVED workflow inline, depth 1 only (a child may not call `workflow()`). |
| `args` | the run input | The `args` object passed to `run_workflow`. |
Use `agent` for a single sequential leaf (throws on failure). Use `parallel`/`map`/
`pipeline` for fan-out (a failed leaf is `null`, so a batch survives a few bad items).
## Leaf options (`opts` for `agent` / `leaf`)
| Option | Type | Effect |
| --------- | -------------------------- | --------------------------------------------------------------------------------------- |
| `schema` | JSON Schema object literal | Forces structured output via a tool. A schema leaf runs with **no tools** — no `file_read`/`file_list`/`recall`/`web_search`, so it **cannot read files or recall memory** (pure judge/extractor). Pass anything it must judge **inline** in the prompt; a schema leaf told to "read these files" answers from the model's prior, not real data. Use a plain JSON Schema literal, not Zod. |
| `label` | string | Short display/diagnostic label for the leaf. |
| `profile` | string | Overrides the model profile. Must exist in `llm.profiles` or the leaf throws. See [Listing profiles](#listing-available-profiles). |
| `persona` | boolean | `true` makes the leaf speak AS the assistant (identity + memory) — use for output meant to be in the assistant's voice. Default is anonymous — use for impartial judging/extraction. |
`persona: true` is the costly path (it runs the full memory-injection pipeline). Use
it only for the few leaves whose output must be in the assistant's voice; keep bulk
judging/extraction anonymous.
## Capabilities — the single consent point
The `capabilities` argument to `run_workflow` declares **once**, up front, what the
run's leaves may do. There are **no per-call permission prompts inside a running
workflow**.
```jsonc
{
"tools": ["file_write", "gmail_send"], // side-effecting tools granted to leaves
"hostFunctions": [], // host-function names the run may invoke
"persona": true // grant leaves persona (identity + memory)
}
```
- **Read-only baseline** (available to **tool** leaves, no declaration, no launch prompt):
`file_read`, `file_list`, `recall`, `web_search`. **A schema leaf gets none of these**
(it runs as a single forced-tool-choice call) — pass it inline content, never tell it to read.
- **`web_fetch` is NOT in the baseline** — an outbound fetch is side-effecting (its
URL can exfiltrate read data), so a leaf that must fetch a URL has to declare
`"web_fetch"` in `capabilities.tools`.
- **Declaring ANY side-effecting tool** (writes, sends, shell, `web_fetch`, …) or
host function makes the LAUNCH prompt the user for approval **once** — that single
approval covers the whole run. A read-only run (no declared tools) launches with no
prompt. Declare the minimum you need.
Runs are autonomous but BOUNDED by a per-run agent cap — spend is structurally
capped and you cannot exceed it.
## Listing available profiles
Before choosing a `profile` for a leaf, look up the valid values rather than guessing
— an unknown profile throws.
- **Preferred (model-accessible):** call `manage_workflows` with action
`list_profiles`. It returns the profile names defined in `llm.profiles` plus the
workspace-wide active profile.
- The same data is served by the daemon route `GET config/llm/profiles` (operationId
`llm_profiles_list`), which clients use to populate profile dropdowns.
Omit `profile` to use the default: a persona leaf mirrors the main agent (the active
profile floats above the call-site default); an anonymous leaf uses the cost-optimized
`workflowLeaf` default.
## Run management
Use `manage_workflows` to inspect and control runs:
| Action | Requires | Purpose |
| --------------- | ---------- | ---------------------------------------------------------------- |
| `status` | `run_id` | Status + agent/token counts for one run (NOT the result). |
| `get_result` | `run_id` | The full result of a finished run. |
| `list_runs` | — | Recent runs, newest first. |
| `abort` | `run_id` | Signal an in-flight run to abort. |
| `resume` | `run_id` | Resume an `interrupted` run (see below). |
| `list_profiles` | — | List defined profiles + the active profile (for leaf `profile`).|
The completion notification injected when a run finishes carries a **truncated**
preview of the result (large results are cut off). To read the complete result,
call `manage_workflows` with action `get_result` and the `run_id` — `status`
deliberately omits the result to stay lightweight.
### Crash recovery / resume
Resume is **not automatic**. If the assistant restarts mid-run, the run is reconciled
to status `interrupted` (the agent/token accounting is preserved so the agent cap
still carries across the restart). It sits there until you explicitly resume it by
`run_id` via `manage_workflows` action `resume`. Resuming re-invokes the engine with
the same `runId`: the journal **replays the completed prefix** without re-spawning (or
re-paying for) finished leaves, then continues from the first unfinished leaf under the
run's originally-declared capabilities. Only `interrupted` runs are resumable; a
`completed` / `failed` / `aborted` run is terminal.
## Worked example
Score each inbox item in parallel (anonymous schema leaves), then write one summary in
the assistant's voice (a single persona leaf). The item list comes in via `args` —
never fetched inside the script.
```js
export const meta = {
name: "triage-inbox",
description: "Score and summarize inbox items",
};
phase("score");
const scored = map(args.items, (item) =>
leaf(`Rate this message's urgency 0-10 with a one-line reason:\n${item.subject}\n${item.body}`, {
label: `score:${item.id}`,
schema: {
type: "object",
properties: { urgency: { type: "number" }, reason: { type: "string" } },
required: ["urgency", "reason"],
},
}),
);
phase("summarize");
const summary = agent(
`Here are scored inbox items. Write a short triage summary, highlighting anything urgent:\n${JSON.stringify(scored)}`,
{ persona: true },
);
return summary;
```
A failed scoring leaf shows up as `null` in `scored`; the run continues.