Workflows
A workflow is one TypeScript file that says which agents do what, in what order, and what comes back. This page covers the file itself: what it takes from the command line, what it returns, what it prints and saves, and how it stops when it can't go on. The words it uses (agent, turn, outcome, run) are defined in How it works.
A workflow file
import { typed } from "agentswf/typebox";
import { defineWorkflow } from "agentswf/workflow";
import Type from "typebox";
export const CHECK = typed(
Type.Object(
{ problems: Type.Array(Type.String()) },
{ additionalProperties: false },
),
);
export default defineWorkflow({
meta: {
name: "check-docs",
description: "Check each doc file against the code.",
},
prepare: ({ argv }) => {
if (argv.length === 0) throw new Error("name the files to check, after --");
return { files: [...argv] };
},
async run(wf, args) {
const checker = await wf.agents.open({ key: "checker", runtime: "codex" });
const checked: { file: string; problems: string[] }[] = [];
for (const file of args.files) {
const { outcome } = await checker.run({
prompt: `Check every command and API that ${file} shows against the code.`,
schema: CHECK,
});
if (!outcome.answered)
return wf.stop(`${file}: ${outcome.kind}: ${outcome.reason}`);
checked.push({ file, problems: outcome.value.problems });
}
return { checked };
},
present: (result) =>
result.checked
.map(
({ file, problems }) =>
`${file}: ${problems.length === 0 ? "ok" : problems.length}`,
)
.join("\n"),
report: (result, ending) => {
if (result) {
const lines = result.checked.flatMap(({ file, problems }) => [
`## ${file}`,
...(problems.length === 0
? ["No problems."]
: problems.map((p) => `- ${p}`)),
]);
return ["# Docs checked", ...lines].join("\n\n");
}
if (ending.kind === "completed" || ending.kind === "parked")
return undefined;
return `# Check ${ending.kind}\n\n${ending.reason}\n\nTo go on: \`${ending.continue}\`\n`;
},
});Saved as check-docs.ts, it runs with the files to check after --:
awf run check-docs.ts -- README.md docs/setup.mdOne codex agent checks each file in turn. When every file has an answer, the run completes and prints one line a file:
README.md: 1
docs/setup.md: okand saves the problems it found as report.md beside the run's record. When a file gets no answer, the run stops there and says why.
The parts
defineWorkflow({ … }) is the file's default export. Two parts are required:
meta:nameanddescription.namenames the folder the workflow's runs are kept in,.awf/runs/{name}, so it is up to 128 letters, digits,.,_,@and-, starting with a letter or digit. An optionalversion, semver, is recorded with each attempt; Runs says what it guards.run(wf, args): an async function.wfis the workflow's context, everything it can do;argsis whatpreparemade of the command line. What it returns is the run's result.
The rest are optional: prepare for arguments, present and report for the result, and id(args), which names the run from its arguments, such as by a ticket's key (Runs).
Arguments and the result are JSON: plain objects, arrays, strings, numbers, booleans and null. They are kept in the run's record, and a type that isn't JSON fails tsc.
Arguments: prepare
The sample's prepare gets argv, the words after --, and cwd, the directory the run works in, and returns args. Its return type is the type run sees, so args.files is string[] with no annotation.
It runs before anything starts. A prepare that throws refuses the run: awf prints awf: prepare: {message} and opens no agent. Check your arguments there, not halfway through the run. It runs again on every --continue, with the arguments and directory the run was started with; if the code has changed so that it throws on them, the continue is refused, and you start a new run.
Everything up to -- is awf's: its options may go before or after the file. Everything after belongs to the workflow:
awf run --timeout 20m check-docs.ts -- README.md
awf run check-docs.ts --cwd ../other-repo -- README.mdA workflow without prepare takes no arguments: its args are null, and a run given any is refused. So a run that declares arguments needs a prepare, or tsc fails it.
Control flow is plain TypeScript
There is no graph to declare. The sample's run opens one agent and gives it a turn a file: a for over the files, an if on the outcome, an early return. A turn's outcome is a value, never an exception, so you branch on it with outcome.answered and decide what an agent that didn't answer means for your workflow.
The same goes for structure: split the work into functions that take wf, import helpers from other files, and run agents side by side with wf.parallel or loop until a reviewer approves (Loops).
Stopping with a reason
wf.stop(reason) ends the run stopped: on purpose, with a reason, rather than failed. Use it when the workflow can't go on as asked and a person should look; let a bug throw, and the run fails with the error. In the sample, a file with no answer stops the run, and awf's closing block gives the reason, the command that goes on, and where the report is:
■ stopped: README.md: failed: {reason}
check-docs {id} · 1 agent · {time} · …
go on awf run check-docs.ts --id {id} --continue
report .awf/runs/check-docs/{id}/report.md
records .awf/runs/check-docs/{id}- It never returns. Write
return wf.stop(…), so TypeScript knows the code after it isn't reached. - It works by throwing, so don't catch it. A
try/catcharound it catches the stop, and the stage that caught it, or the run, fails instead. - A stopped run exits 3. Every exit code is in How an attempt ends.
- The go-on command continues it.
--continuemakes another attempt of the same run. A stop inside a stage leaves that stage to be redone; a stop between stages leaves no record, so the continue runs the same check again (Stages).
The result: present and report
What run returns is kept in the run's out.json. Two optional functions turn it into something for a person.
present(value, ending) is what a completed run prints, on standard output. The sample's prints a line a file. Without it, or when it returns undefined, awf run prints the run's record as JSON. --json prints the record of any run that came to be, and otherwise a refusal. A run that didn't complete never calls present: awf prints how it ended itself.
report(value, ending) returns Markdown that awf saves as report.md in the run's folder, and the closing block says where. It is called however the run ended, so a stopped run can still hand off what it found: value is the result only when the run completed. ending.kind says how it ended: completed, or stopped, failed, timed-out or cancelled, which carry reason, stage when it ended in one, and continue, the go-on command. (parked is only for a workflow that waits on events.) Each kind also lists its stages and their values. Returning undefined saves none.
Catches
- Only the words after
--reachprepare. A second file name before--is refused withput -- before workflow arguments, and an option awf doesn't know withunknown option. presentandreportdon't fail the run. One that throws is reported on standard error; awf prints the JSON instead, or saves no report, and the result is still inout.json.report.mdis the last attempt's. A later attempt whosereportreturnsundefinedremoves the earlier one; areportthat throws leaves the earlier attempt's file in place.- The closing block goes to standard error, and only what
presentreturns, or the JSON, to standard output:awf run check-docs.ts -- README.md > result.txtkeeps the result alone.
Next
- A workflow file in the workflow API: every part and its type.
- Runs: a run's id, stages, exit codes, and going on after a stop.
- Testing: testing a workflow,
prepareincluded, on scripted answers. - Agents: opening an agent and what each outcome means.