Run an agent-written JavaScript workflow from a Codex desktop shell call. The Bash launcher uses the app's bundled Node and Codex executables. The single workflow.mjs runner uses only Node built-ins and launches the bundled Codex CLI directly. Completion uses the desktop conversation bridge verified in this project. There are no npm dependencies, generated bundles, or MCP server.
AGENTS.md is the short entry point for agents; this README is the
complete reference. Codex automatically discovers AGENTS.md in its project
instruction chain when a run starts. Keeping it beside the runner makes it
available when this directory is the project or working directory. An agent
working in another project must explicitly read it, or that project's existing
AGENTS.md can reference these workflow instructions. The runtime's location
alone does not add its instructions to other projects. See the
official discovery rules.
The self-contained codex-workflows skill
packages the runtime, launcher, examples, and a runtime reference. Installing it
at user scope makes its name and description available across projects. Codex
can choose it when a task matches, or you can invoke $codex-workflows
explicitly. Its full instructions load only when used. See the
official skills guide.
Share release/codex-workflows-skill.zip. Extract it into ~/.agents/skills
(the current documented user-skill location), producing
~/.agents/skills/codex-workflows/SKILL.md. Existing Codex installations also
use ${CODEX_HOME:-$HOME/.codex}/skills; the local installation location is
verified with the bundled CLI during setup. Install into one user-skill location
to avoid duplicate entries. An update should replace that skill folder as a
unit. Codex detects skill changes automatically; restart it if the skill does
not appear.
The package resolves its resources from its own directory and runs in the calling project's working directory. Write workflows and run state there. The installed skill folder holds reusable code and instructions. The desktop bridge requirements below also apply to the skill.
To refresh the packaged skill after runtime, example, or documentation changes:
"$CODEX_MCP_NODE_PATH" scripts/package-skill.mjsThis copies the canonical root runtime into the skill and creates the zip; there is no compilation or dependency installation. The skill's seven-file payload contains no account data, local run state, or machine-specific paths.
bash /path/to/codex-workflows/workflow.sh /path/to/review-changes.mjs \
--args '{"base":"origin/main"}' \
--model gpt-6.1-sol --effort xhighPass the originating conversation's actual model and effort rather than assuming the CLI default matches the app. Launch from Codex: it supplies CODEX_MCP_NODE_PATH, CODEX_CLI_PATH, CODEX_THREAD_ID, CODEX_PERMISSION_PROFILE, and CODEX_APP_TOOLS_PIPE_PATH. The launcher also discovers bundled Node relative to CODEX_CLI_PATH when the Node variable is absent.
The command checks module syntax without evaluating it, persists the source, starts a detached worker, and returns JSON containing runId, taskId, transcriptDir, scriptFile, and dashboardFile. The worker establishes its desktop bridge connection while its trusted parent is alive. It then continues after the launcher and orchestrator turn end.
The default state directory is .workflow-state in the project. Use --state-dir consistently when changing it. The completion worker waits for the originating turn to finish and the destination chat to become idle, then sends one tool-output notification that starts a new turn. An unloaded chat can be resumed by that same path. Other user turns continue normally while the workflow runs.
This integration currently relies on the desktop app's internal bridge and signed-process checks. It was verified on macOS with the installed bundled runtime. It is not a documented public callback API, and compatibility with other desktop versions/platforms is not assumed. A missing bridge fails startup; the runner never silently substitutes a new conversation. For standalone execution, explicitly pass --no-notify.
Every new run automatically writes dashboard.html in its transcript directory. Open the returned dashboardFile directly in a browser. The page embeds the current snapshot: it never fetches another local file and needs no server or dependencies.
The dashboard shows planned/current phases, agent states and cache hits, token counts, elapsed time, the latest 20 narrator logs, errors, and a final result preview. Counts describe agents invoked so far, since later phases can add more. Full logs and results remain in the linked journal.
The runner rewrites the page atomically on progress changes and every minute during quiet waits. The browser reloads the HTML every 60 seconds; a manual refresh link is also available. The final page removes automatic refresh on completion, failure, or cancellation and remains as the run's report. Completion callback delivery continues independently. A resumed run gets its own dashboard.
export const meta = {
name: 'review',
description: 'Review changes across three lenses',
phases: [{ title: 'Review' }],
};
export default async function workflow() {
phase('Review');
const results = await parallel(['correctness', 'security', 'tests'].map(lens => () =>
agent(`Review ${args.base}..HEAD for ${lens}.`, {
label: lens,
readOnly: true,
})
));
return { results: results.filter(Boolean) };
}Workflows are ordinary ES modules exporting meta and a default async function. Put workflow execution inside that function. meta is JSON data with a lowercase name, description, and optional phases; trusted expressions are allowed. Node checks syntax before launch, then the bounded script worker loads the module once and validates its exports and metadata. Invalid exports fail the run with a journaled error.
Static imports, dynamic import(), and native import attributes are supported. The worker executes the saved .mjs snapshot and uses Node's synchronous module-resolution hook to resolve its imports beside the original workflow, including when resuming. Imported modules retain their own native resolution rules. The runner requires Node 24+ (the installed desktop runtime is Node 24). Relative imports and import.meta.resolve() use the original directory; import.meta.url identifies the saved snapshot. For files opened through fs, use an explicit input path in args or the configured working directory.
Scripts are trusted ordinary JavaScript, with access to Node and the host filesystem/network subject to their process permissions. This intentionally relaxes the original sandbox requirement. A worker thread separates script execution from agent scheduling and lets wall-clock cancellation stop a busy loop. It is not a security sandbox.
The workflow functions are available both in scope and on globalThis:
| API | Behavior |
|---|---|
agent(prompt, opts) |
Fresh CLI session; final text, validated JSON object, or null on agent failure/skip |
parallel(thunks) |
Concurrent execution and a barrier; thrown thunks become null; at most 4096 thunks |
pipeline(items, ...stages) |
Stages receive (previous, originalItem, index); a thrown stage stops that item; no stage-wide barrier |
phase(title) |
Progress group for subsequently invoked agents; prefer opts.phase inside concurrent branches |
log(message) |
Append narrator/progress output to the journal |
args |
The structured JSON value supplied by --args or --args-file |
Options for agent() are label, phase, schema, model, effort, agentType, isolation: 'worktree', readOnly, and replay: 'never'.
readOnly: true or agentType: 'reviewer' narrows agent permissions to read-only filesystem access, denies network/web search, and disables inherited connector/plugin tools. Writable agents retain the configured parent permission profile and workspace roots. New approvals are denied in background execution (approval_policy = "never"); the workflow sees a failure rather than an interactive approval prompt. No agent resumes the orchestrator thread or receives its latest user message. Each agent gets only its task, worker instructions, its optional profile instructions, and the project instructions loaded by Codex.
Structured output uses the CLI’s native --output-schema file. A small local validator checks the supported JSON Schema subset without dependencies. Invalid JSON or schema violations are sent back to the same agent thread for up to two correction attempts. Read-only API failures may retry once in a fresh thread; writable agents are not automatically retried after failure because their side effects may already have occurred.
The supported schema subset includes object properties/required/additionalProperties; array items, lengths, and uniqueness; primitive types (including unions); enum/const; numeric bounds; string lengths/pattern; property counts; and title/description/default. The root must be an object with properties, and required must be a subset of properties. Unsupported composition/ref keywords and detected contradictions are rejected. This is not a general JSON Schema satisfiability solver.
Agents execute concurrently under a FIFO concurrency cap. Results are released to the workflow in invocation order, independent of completion latency. After each release, the coordinator waits for a worker acknowledgment that promise reactions have drained and any newly invoked agents have been registered. The next result cannot overtake these reactions, including when it came from cache.
This fixes the pipeline replay problem: a cached result returning immediately cannot reorder downstream agent() calls. Nested fan-out and Promise.race are stable too; races among agent calls intentionally select by invocation order. The tradeoff is head-of-line waiting when an earlier agent is slow. Pipelines still overlap stages; there is no global barrier between stages.
Resume reads the old journal and replays its longest unchanged prefix. Call keys contain canonical prompt/options JSON (excluding display label/phase) and the execution context fingerprint. A prompt change in call 3 replays calls 1–2 and runs call 3 onward live. Context, args, runtime implementation, Codex binary metadata, configuration/instruction files, and workspace input changes invalidate reuse.
Only enforced read-only agents are reusable. If a run contains any non-reusable call, the next resume runs that whole run live: a concurrent writer could have changed the inputs observed by an earlier reader. Cached final responses are never treated as restored edits or worktrees. replay: 'never' disables reuse conservatively as well.
Workspace fingerprints include Git-tracked and untracked non-ignored file bytes, or a bounded recursive snapshot outside Git. Runtime state and node_modules are excluded. Use --cache-key to declare an explicit input revision for large workspaces or inputs outside that snapshot; change it whenever those inputs change. Do not mutate workflow inputs during execution. Deterministic control flow should depend only on args and workflow agent results, not timers or independent external I/O. Date.now(), zero-argument Date, and Math.random() throw to catch accidental nondeterminism; trusted scripts can bypass these guards, so they are a contract rather than an isolation boundary.
All launched/result entries have invocation indexes. Agent result records are written and fsynced when real work finishes; a separate deterministic release schedule controls what the script observes. Each live/replayed agent has its own JSONL transcript. Truncated completion messages point to the full journal result.
bash workflow.sh inspect wf_<id>
bash workflow.sh resume wf_<id>
# Use --script to resume with an edited script; args/settings otherwise carry forward.
bash workflow.sh resume wf_<id> --script edited-review.mjs
bash workflow.sh cancel wf_<id>
bash workflow.sh skip wf_<id> a_<agent-id>Skip accepts an unambiguous label or agent ID from inspect. Cancelling interrupts active CLI subprocesses, prevents queued agents from starting, and produces a cancelled completion notification. Returned worktrees are retained for inspection: there is no automatic merge, deletion, commit, or push.
Notifications maintain a persistent state: pending, sending, delivered, failed, or unknown. The desktop message API has no idempotency key, so an ambiguous timeout/disconnect after sending is recorded as unknown and never automatically retried. This avoids duplicate turns; check the destination chat before arranging another delivery. The idle wait has a finite deadline (default 24 hours). The bridge connection is not replaced with an unrelated server on disconnect.
Limits default to max(1, min(16, availableCPUs - 2)) running agents, 1000 lifetime calls, one hour per workflow, 20 minutes per agent, and 128 MiB for the script worker's old-generation heap. Calls returned without awaiting them fail the run. Optional nested workflows and a turn-wide token budget are not included. The dashboard provides progress inspection; CLI status and journals provide inspection/control.
For custom permissions/profiles, supply --context-file with JSON like:
{
"model": "gpt-6.1-sol",
"effort": "xhigh",
"cwd": "/path/to/project",
"permissionProfile": ":workspace",
"sandboxMode": "workspace-write",
"workspaceRoots": ["/path/to/project"],
"profiles": {
"skeptic": { "readOnly": true, "instructions": "Try to refute each finding using evidence." }
}
}The exported Workflow(input, context, options) function in workflow.mjs accepts exactly one of script or scriptPath, plus args and resumeFromRunId. Library scriptPath defaults to previously returned paths only. The CLI deliberately allows any explicitly supplied file path. Neither interface adds tools to Codex or installs a server.
No install or build step is needed. Run tests with the app's bundled Node:
"$CODEX_MCP_NODE_PATH" --test test/*.test.mjsShare workflow.mjs and the optional workflow.sh launcher. Recipients use their desktop app's bundled Node/CLI and existing Codex login. They can launch the runner directly from a Codex shell call:
"$CODEX_MCP_NODE_PATH" /path/to/workflow.mjs /path/to/review-changes.mjs \
--args '{"base":"origin/main"}' --model gpt-6.1-sol --effort xhighThe same runner file handles CLI commands, detached background hosting, and worker-thread execution. State files and workflow snapshots are created as runs execute. The optional full release archive contains the runner, launcher, examples, README, AGENTS.md, prepared skill, and packaging helper, with no runtime binary, dependencies, credentials, or journals. For skill installation, use the smaller codex-workflows-skill.zip described above.
To create that archive:
mkdir -p release
"$CODEX_MCP_NODE_PATH" scripts/package-skill.mjs
tar -czf release/codex-workflows.tar.gz workflow.mjs workflow.sh examples README.md AGENTS.md skills scriptsVersion 0.2 uses the native module format. Old async-body workflow scripts need their execution wrapped in export default async function workflow() { ... } before use. Resume caches from the previous implementation are invalidated by the runtime fingerprint/version.
Tests cover schema constraints and correction sessions, CLI JSONL transport/cancellation, module imports and evaluation, concurrency, pipeline/nested/race replay, prefix invalidation, permissions, side-effect caching rules, cancellation/skip, clock guards, busy-loop limits, background process lifetime, and desktop notification framing/queueing. A real CLI smoke workflow is provided in examples/cli-smoke.mjs.