# gpt-workflow
A deterministic multi-agent workflow runtime powered by Codex App Server.
Control flow — loops, branches, retries — is plain JavaScript in your script;
`agent()` delegates bounded judgment to Codex threads, and `parallel()`,
`pipeline()`, and child `workflow()` calls fan that work out.
Compared to driving Codex directly, a workflow adds:
- **Resumability** — long or token-expensive runs replay completed calls from
a durable journal instead of paying for them again.
- **Validated structured output** — pass a JSON schema to `agent()` and the
runtime validates the reply and retries invalid ones, instead of you
policing format in prompt text.
- **Multi-agent verification** — independent critics and judge panels, where
a failed call resolves to `null` instead of aborting the fan-out.
[How did I use Codex to build gpt-workflow?](./docs/BUILD.md)
## Install
Everything requires Bun 1.3 or newer, with the Codex CLI installed and
authenticated for live runs. Node.js is not a supported runtime.
The preferred install is the Codex plugin. It bundles a skill that authors,
runs, and debugs workflows for you, and it runs the published CLI through
`bunx`, so there is no separate package to install:
```sh
codex plugin marketplace add CyrusNuevoDia/gpt-workflow
codex plugin add gpt-workflow@gpt-workflow
```
Restart the ChatGPT desktop app after installing and start a new task so the
bundled skill loads; see
[plugin installation and behavior](docs/07-plugin.md).
To drive the CLI yourself, install globally:
```sh
bun add --global gpt-workflow
```
For library use:
```sh
bun add gpt-workflow
```
## Write a workflow
Store project workflows under `.codex/workflows/`; this example is
`summarize-files.js`:
```js
export const meta = {
name: "summarize-files",
description: "Summarize files concurrently and merge the findings",
};
const files = args.files;
const summaries = await parallel(
files.map(
(file) => () =>
agent(`Read ${file} and return three factual bullets.`, {
label: `summarize:${file}`,
})
)
);
return { summaries: summaries.filter(Boolean) };
```
`args` is the run's input; the `--args` flag in the next section supplies it.
`meta.name` is also the run-storage directory name, so it may contain only
letters, numbers, periods, underscores, and hyphens, and cannot be `.` or `..`.
If an agent's thread ends in an error, times out, returns no final message,
or exhausts its structured-output retries, that call resolves to `null` and is
recorded in the run's failures — the `filter(Boolean)` drops those slots. Script bugs,
setup problems such as missing models or bad option types, cancellation,
worktree-setup failures, and transport failures reject the whole run instead.
Workflow source is trusted repository code; it runs inside `node:vm` as a
semantic boundary, not a security sandbox for hostile JavaScript.
## Run and resume
A live run spends model tokens. Resume replays completed calls from the
journal — their tokens are not spent again — then runs the rest live:
```sh
gpt-workflow models
gpt-workflow run --default-model gpt-5.6-luna \
--args '{"files":["src/cli.ts","src/runtime.ts"]}' \
.codex/workflows/summarize-files.js
gpt-workflow run --default-model gpt-5.6-luna --resume workflow-123 \
--args '{"files":["src/cli.ts","src/runtime.ts"]}' \
.codex/workflows/summarize-files.js
```
`--default-model` supplies the model for `agent()` calls that omit
`options.model`; without either, the run rejects with a model error.
`--args` takes strict JSON and becomes the script's `args` global; pass a
plain string as quoted JSON, e.g. `--args '"triage"'`. Invalid JSON exits 1
with a usage error on stderr before any record is emitted. For `--resume`,
substitute the `runId` reported by your original run's records — real IDs
look like `workflow-<uuid>`; these examples shorten it to `workflow-123`.
Resume with the same `--args`: changed args change prompts, which miss the
journal and run live. Resume is strict: a missing run ID, duplicate run ID, or
run stored under a different workflow name exits before connecting to Codex.
`models` prints every model discovered from the authenticated App Server as
NDJSON without spending model tokens. Run accepts repeatable `--required-model`
flags plus `--request-timeout-ms`, `--thread-start-timeout-ms`, and
`--turn-timeout-ms`. `SIGINT` or `SIGTERM` cancels active agents, records a
terminal failure, flushes persisted events, and exits nonzero.
Stdout is ordered NDJSON; human diagnostics go to stderr. Every record
includes `schemaVersion`, `sequence`, `runId`, `scriptPath`, `runDirectory`,
`ts` (epoch milliseconds), and `type`. The opening `run.started` record
carries the script's `meta`; the final `run.completed` record carries `meta`,
`result`, `usage`, `failures`, and `journalPath`:
```json
{"meta":{"name":"summarize-files","description":"Summarize files concurrently and merge the findings"},"type":"run.started","runDirectory":"/home/me/.codex/projects/-repo/workflows/summarize-files/runs/workflow-123","runId":"workflow-123","schemaVersion":1,"scriptPath":"/repo/.codex/workflows/summarize-files.js","sequence":0,"ts":1783971328984}
{"failures":[],"journalPath":"/home/me/.codex/projects/-repo/workflows/summarize-files/runs/workflow-123/journal.jsonl","meta":{"name":"summarize-files","description":"Summarize files concurrently and merge the findings"},"result":{"summaries":["…","…"]},"type":"run.completed","usage":{"agentCount":2,"liveAgentCount":2,"modelUsage":{"gpt-5.6-luna":{"liveAgentCount":2,"replayedAgentCount":0,"subagentTokens":3412}},"peakConcurrentAgents":2,"replayedAgentCount":0,"subagentTokens":3412},"runDirectory":"/home/me/.codex/projects/-repo/workflows/summarize-files/runs/workflow-123","runId":"workflow-123","schemaVersion":1,"scriptPath":"/repo/.codex/workflows/summarize-files.js","sequence":9,"ts":1783971339402}
```
After valid metadata creates the run directory, a top-level failure makes the
CLI emit `run.failed` and exit non-zero. Source-read and metadata-parse failures
write only to stderr and create no run artifact. Agent-side `null` failures don't fail the run: they stay
visible in the `run.completed` record's `failures` and are retried on resume.
## Inspect past runs
There is no need to `tee` the stream for later inspection: every run also
persists a filtered copy of its NDJSON to
`$CODEX_HOME/projects/<encoded-project-path>/workflows/<workflow-name>/runs/<runId>/events.jsonl`
— the run, phase, and agent
status records needed to rebuild run state, without the high-volume
streaming deltas. Two commands read it back without spending model tokens:
```sh
gpt-workflow list
gpt-workflow status workflow-123
```
`list` prints one JSON line per run, newest first, with `runId`, `name`,
`scriptPath`, `status`, timestamps, and — once the run ended — `finishedAt`,
`failureCount`, and `usage`. `status` prints one JSON object that adds
ordered `phases`, per-agent progress and token totals, and the final
`result` and `failures`. Output is plain JSON, so `jq` applies:
```sh
gpt-workflow status workflow-123 | jq '.phases'
```
A run with no terminal record reports `"incomplete"`: an in-flight and an
interrupted run look identical on disk, so the CLI never claims "running" —
check `lastEventAt` for staleness. An unknown run ID makes `status` exit 1
with an error on stderr.
## Durable journals
Live runs persist an append-only replay journal at:
```text
$CODEX_HOME/projects/<encoded-project-path>/workflows/<workflow-name>/runs/<runId>/journal.jsonl
```
`CODEX_HOME` defaults to `~/.codex`. The project key is the absolute invocation
directory with path separators replaced by dashes, including the leading root
separator: `/repo` becomes `-repo`. Run state is local user data and does not
need a repository ignore rule.
The same directory holds `events.jsonl`, the inspection copy described
above; the journal remains the only replay substrate.
Resume reuses that `runId` and directory. Completed `agent()` calls are
matched by their prompt and options, regardless of the order they finished
in; at the first call with no journal match, that call and every later call
runs live and appends to the same journal.
To inspect a journal, parse it one record at a time with
`parseWorkflowJournalEntry` — [Getting started](docs/01-getting-started.md)
shows a streaming loop. The parser throws a `SyntaxError` on blank text,
malformed JSON, or records that are not valid journal entries; it never
returns `null`, so wrap each parse in try/catch when surveying a damaged
journal.
The journal is workflow replay state. Codex separately persists full agent
thread rollouts and exposes them through App Server thread APIs; their private
on-disk layout is not a `gpt-workflow` contract.
## Library API
```js
import {
AppServerClient,
REQUIRED_APP_SERVER_MODELS,
runWorkflowScript,
} from "gpt-workflow";
const source = `
export const meta = {
name: "summarize",
description: "Summarize a topic"
}
return await agent("Summarize " + args.topic)
`;
const client = await AppServerClient.connect({
defaultModel: "gpt-5.6-luna",
requiredModels: REQUIRED_APP_SERVER_MODELS,
});
try {
const execution = await runWorkflowScript(source, {
appServer: client,
args: { topic: "deterministic orchestration" },
});
console.log(execution.result, execution.journalPath);
} finally {
await client.close();
}
```
Running this example spends model tokens; inject `agent` to drive offline
tests without Codex. `REQUIRED_APP_SERVER_MODELS` is the model set the
runtime depends on — `connect` rejects when the App Server cannot start or is
missing any of them. `runWorkflowScript` accepts `runDirectory` for
caller-owned storage and `resumeFromRunId` for library resume, and splits
failures exactly as [Write a workflow](#write-a-workflow) describes:
agent-side failures resolve to `null` and land in `execution.failures`;
everything else rejects. `listRunSummaries` and `readRunStatus` expose the
`list` and `status` data programmatically; see the
[API reference](docs/03-api.md).
## Documentation
Start with [Getting started](docs/01-getting-started.md) or the full
[documentation index](docs/README.md). Structured output schemas, budgets,
agent options, and child workflows are covered in the
[API reference](docs/03-api.md); verification and fan-out shapes in
[Patterns](docs/05-patterns.md).
Migrating Claude Code workflows? See the
[Claude parity ledger](docs/08-claude-parity.md) and the
[migration checklist](skills/workflow/references/migration.md).
Working on this repository itself? See [AGENTS.md](AGENTS.md).