gpt-workflow
v0.4.1
Published
Deterministic multi-agent workflows powered by Codex App Server
Readme
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
nullinstead of aborting the fan-out.
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-workflowRestart 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-workflowFor library use:
bun add gpt-workflowWrite 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-123list 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.jsonlCODEX_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.
