Workflows
Hooks react to one session. A workflow runs your whole process: a JavaScript or
TypeScript file in .spwn/workflows/ that starts sessions, prompts them, waits for their
replies and decides what happens next. One session per issue, a persona per board column, a
review loop, a merge queue: all of it is code in your script. spwn runs the script and hands
it an API.
A workflow’s sessions are ordinary sessions, each with its own worktree, branch and hooks,
so you can watch one work or take it over from the sidebar. They carry a ⇄ chip.
Quickstart
Section titled “Quickstart”- Open a project’s ⇄ Workflows row in the sidebar.
- Click Allow workflows…. Workflows are code from the repository and run with your permissions: they can start sessions, run commands and use your GitHub token. Nothing runs until you allow it, per project. Turn off stops every run and disallows them again.
- Click + New workflow, give it a name, and tick TypeScript if you like. spwn writes
a starter to
.spwn/workflows/<name>.js(or.ts), plusspwn.d.ts(the API’s types, for your editor) andAGENTS.md(a guide for an agent writing workflows) beside it. - Click Run.
The starter creates one session, asks it to summarise the repository, and logs the reply.
A real one
Section titled “A real one”// .spwn/workflows/triage.ts — one session per labelled issue, kept across runsimport type { Spwn, WorkflowMeta } from "./spwn";
export const meta: WorkflowMeta = { description: "Open a session for each new bug report", inputs: { repo: { description: "owner/name", required: true }, label: { default: "bug" }, },};
export default async function main(spwn: Spwn, inputs: { repo: string; label: string }) { const issues = await spwn.github.rest("GET", `/repos/${inputs.repo}/issues?labels=${inputs.label}`); for (const issue of issues) { const key = `issue-${issue.number}`; if (await spwn.sessions.find(key)) continue; // already has a session const s = await spwn.sessions.create({ key, title: issue.title, prompt: `Reproduce and fix this bug:\n\n${issue.title}\n\n${issue.body}`, }); const turn = await s.waitForTurn(); // the reply, after its commit spwn.log(turn.blocked ? `${issue.title}: needs you` : turn.text); }}key is your stable id for the session: spwn.sessions.find(key) finds it again after the
workflow or spwn restarts, so a re-run doesn’t duplicate work. waitForTurn() resolves once
the reply is complete and its commit hooks have run. turn.blocked means the agent stopped
on a permission prompt or a question; leave it for a human (it shows in the
Fleet) or answer it from the script.
File rules
Section titled “File rules”- A workflow is one ES module at the top level of
.spwn/workflows/, named<name>.js,.mjs,.tsor.mts. The file stem is its name. export defaultmust be a function:main(spwn, inputs).export const metais optional:nameanddescriptionfor the panel,keepAlive, andinputs.- Files starting with
_,*.d.tsfiles and anything in a subdirectory (saylib/) aren’t workflows. They’re modules you import with a relative path.
Inputs
Section titled “Inputs”Each entry in meta.inputs becomes a field in a form shown when you click Run. type is
"string" (the default), "number" or "boolean". An empty field gets its default, and
required: true refuses to start without a value. Runs started automatically use the
defaults.
Not Node
Section titled “Not Node”Workflows run in an embedded QuickJS engine, each run on its own thread. Modern JavaScript
works: async/await, modules, Map, regexes, setTimeout, console.*. There is no
require, no npm packages, no Node built-ins (fs, child_process, process), no fetch,
and no bare imports. TypeScript has its types stripped and is never type-checked at run
time.
The spwn API covers what you’d reach for instead: spwn.exec / spwn.sh for commands,
spwn.fs for project files, spwn.fetch for HTTP, spwn.github for the REST and GraphQL
APIs (with the token saved in Settings), spwn.state for JSON that survives across runs, and
spwn.agents.run for a one-shot headless run with no conversation.
Long-running workflows
Section titled “Long-running workflows”Set keepAlive: true and spwn restarts the workflow whenever main returns or throws, with a
backoff from 5 seconds up to 5 minutes. A poller loops until stopped:
export const meta = { keepAlive: true };
export default async function main(spwn) { while (!spwn.stopping) { try { await pollOnce(spwn); } catch (e) { spwn.error("poll failed:", e); } await spwn.sleep(60_000); }}Tick Start with spwn on a workflow to start it whenever spwn starts, and right away if
it isn’t running. The two are separate: keepAlive restarts a run that ends; Start with
spwn starts one when the server boots.
Running, stopping and logs
Section titled “Running, stopping and logs”- Run starts it; Stop stops it. Stopping rejects every pending
spwn.*call with an error whosecodeis"stopped". - Only one run of a workflow at a time.
- The status link on each card opens the run’s log: everything from
spwn.log,spwn.warn,spwn.errorandconsole.*, live, with links to the sessions the run created. spwn keeps the last 2,000 lines per run. - A workflow that doesn’t parse shows its error on its card and can’t be run.
spwn watches .spwn/workflows, so a file you write, an agent writes, or a git pull brings
in shows up straight away. A running workflow keeps the code it started with; stop it and
run it again to pick up an edit.
With hooks
Section titled “With hooks”- Workflow sessions run the project’s hooks like any other session.
- A hook’s
spwn promptin a workflow session goes to theonHookPrompthandler passed tosessions.create; without one it’s declined. spwn.on("session-turn", …)and the other lifecycle events fire for every session in the project, including ones you started by hand.workflow-startedandworkflow-stoppedhooks run around each run, withSPWN_WORKFLOW,SPWN_WORKFLOW_RUN_IDand (on stop)SPWN_WORKFLOW_STATUSset took,errororstopped.
Examples
Section titled “Examples”examples/workflows/github-board.tsworks a GitHub Projects board with a persona per column, one session per ticket that follows it across the board.examples/workflows/merge-queue.tslands finished sessions one at a time, handing each conflict back to the session that wrote the code.
To use one, click + New workflow once so spwn writes spwn.d.ts, delete the starter,
and copy the example into .spwn/workflows/.
The full API, from sessions to events, is in the Workflows API reference.