Delegate a big or high-stakes job to a fleet of parallel subagents, orchestrated deterministically; runs unattended and reports back
Scanned 9/1/2026
Install to Claude Code
npx -y skills add vellum-ai/vellum-assistant --skill workflows --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Workflows?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/vellum-ai-workflows)More formats (shields.io, HTML) on the badges page.
---
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.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!