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
nullinstead of aborting the fan-out.
How did I use Codex to build gpt-workflow?
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:
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.
To drive the CLI yourself, install globally:
bun add --global gpt-workflow
For library use:
Write a workflow
Store project workflows under .codex/workflows/; this example is
summarize-files.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:
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:
{"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:
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:
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:
$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
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
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 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.
Documentation
Start with Getting started or the full documentation index. Structured output schemas, budgets, agent options, and child workflows are covered in the API reference; verification and fan-out shapes in Patterns.
Migrating Claude Code workflows? See the Claude parity ledger and the migration checklist.
Working on this repository itself? See AGENTS.md.