Skip to content

Dynamic Workflow Plugin

The dynamic-workflow Codex plugin turns a multi-agent plan into a small, reviewable JavaScript program. JavaScript owns deterministic control flow; headless Codex workers own reasoning and tool use.

Codex skill -> workflow.js -> TypeScript runner -> codex exec workers
|
`-> durable state and cache

The runner calls codex exec --json directly. It does not require MCP, the Codex SDK, or separate API-key configuration.

The plugin can be installed independently of the repository’s Python CLI. Do not run uv tool install claude-code-tools, and do not run npm install. The plugin ships both the skill and a compiled JavaScript runner. It requires only an authenticated Codex CLI and Node.js 20 or newer to execute that runner.

Terminal window
codex plugin marketplace add pchalasani/claude-code-tools
codex plugin add dynamic-workflow@cctools-codex-plugins

Those two commands are the complete plugin installation.

Restart Codex, then invoke $dynamic-workflow or ask for a durable multi-agent workflow.

Scripts use injected globals, top-level await, and top-level return:

export const meta = {
name: "audit-routes",
description: "Audit routes in parallel",
}
const found = await agent("List route files.", {
id: "discover",
schema: {
type: "object",
required: ["files"],
properties: {
files: { type: "array", items: { type: "string" } },
},
},
})
const audits = await pipeline(
found.files,
file => agent(`Audit ${file}.`, {
id: "audit",
label: file,
}),
{ concurrency: 4, key: file => file, maxItems: 50 },
)
const summary = await agent(
`Synthesize these audits:\n${JSON.stringify(audits)}`,
{ id: "synthesize", cacheKey: audits },
)
return { audits, summary }

agent() returns the final worker message or parsed JSON when a schema is present. pipeline() fans out with bounded concurrency and preserves result order. parallel() handles heterogeneous callbacks. args contains parsed JSON supplied by the launcher.

The runner records state under ~/.codex/workflows/runs/<run-id>/ by default. A completed step is reused when its stable ID and execution fingerprint still match. Use maxItems to cap dynamic fan-out. Include upstream results in a downstream prompt or cacheKey so changed dependencies invalidate that completed step.

Terminal window
node bin/workflow.mjs run workflow.js --detach --json
node bin/workflow.mjs status <run-id> --json
node bin/workflow.mjs logs <run-id>
node bin/workflow.mjs pause <run-id>
node bin/workflow.mjs resume <run-id>
node bin/workflow.mjs cancel <run-id>
node bin/workflow.mjs wait <run-id>

Pause is cooperative: active workers finish, while new workers wait. Resume replays the JavaScript and returns cached results for compatible completed steps. This works across Codex CLI sessions because state lives on disk.

A detached wait process can observe completion without making model calls, but it cannot wake the main Codex thread. The plugin also supports an opt-in true callback through Codex’s shared app server:

detached workflow -> callback sidecar -> shared app server -> original thread
^
|
Codex TUI

For a one-command setup, install the optional Python CLI helper and launch Codex through it:

Terminal window
uv tool install claude-code-tools
codex-dynamic

If the package is already installed, refresh it with uv tool install --force claude-code-tools. To continue the most recent Codex conversation, use:

Terminal window
codex-dynamic resume --last

The helper quietly starts or reuses the server and then hands the terminal directly to Codex. It is not required for the plugin itself. Without the helper, the equivalent manual setup is:

Terminal window
# Terminal 1
codex app-server --listen unix://
# Terminal 2
codex --remote unix://

Callbacks require Codex CLI 0.136.0 or newer. That is the first compatible CLI release combining the WebSocket-over-Unix transport with the echoed client message IDs used to confirm callback delivery. Codex still marks the app-server and remote TUI interfaces as experimental, so their surfaces may evolve.

Callback preflight validates the connected App Server too, so a stale external server fails before the workflow is created.

To preserve an existing conversation manually, exit its current TUI and run codex resume --remote unix://, then select that session in the picker or pass its session ID. An already-running local TUI cannot reconnect in place.

Then ask $dynamic-workflow to run in the background and notify the current thread. Every run or resume needs explicit host execution for that exact trusted supervisor command. This lets the supervisor write durable state and launch headless workers without inheriting the outer tool sandbox. For a callback, the same authorization lets its notifier reach the host App Server socket. The workflow remains in the restricted VM, and each worker retains its declared sandbox. The corresponding direct runner option is:

Terminal window
node bin/workflow.mjs run workflow.js --detach --notify-current-thread

A sandboxed launch fails before creating a run. Approve only the exact runner command, never a generic node prefix. Do not grant workers danger-full-access to work around the callback boundary.

Codex automatically supplies the current thread ID to tool shells. The runner checks that the thread is loaded on the same server before it creates the run; there is no thread ID to copy or configure manually. A custom Unix socket can be selected with --app-server-endpoint unix://PATH when both the TUI and the runner use that path.

When the workflow becomes terminal, the sidecar starts a turn if the original thread is idle. If you are already chatting with Codex, it steers the completion into and extends the active turn instead. Delivery is confirmed with a stable client message ID, so a lost response can be reconciled against thread history before retry.

Do not start a waiter or polling loop after a launch whose response confirms an armed callback. Report the run ID and let the user continue chatting. This is conditional: a detached run without an armed callback still needs normal bounded monitoring when the user wants completion tracked.

When Codex was started with codex-dynamic, the runner automatically requests a callback for every detached run. This protects against an agent accidentally omitting --notify-current-thread. Use --no-notify-current-thread only as an explicit opt-out; in every case, trust the actual launch response rather than the requested flags.

Codex currently shows no persistent background-workflow card, spinner, or running-status message for the detached run. The launch response is the only immediate confirmation; completion later appears through the callback. The user can request a one-off status check at any time.

Passive supervision and the local app server add no model calls. Completion reporting starts a new turn only while the thread is idle; otherwise it extends the active turn. Either reporting path invokes the model and consumes tokens. This path uses the Codex app-server protocol directly; it does not use MCP, the Agents SDK, or another service. The optional helper manages only the local process lifecycle. Codex currently labels the app-server and remote TUI interfaces as experimental, so their CLI and protocol surfaces may evolve. See the official app-server documentation and the codex-dynamic tool reference.

Callback status is stored separately from workflow status. A socket outage can therefore produce a successful workflow with a failed callback. Inspect or retry it with:

Terminal window
node bin/workflow.mjs status <run-id> --json
node bin/workflow.mjs notify <run-id>

Retries have a 24-hour deadline by default, a seven-day maximum, and at most five delivery submissions. A callback in unknown state, or one left in sending after submission began, may already have been accepted. Inspect the target thread before explicitly retrying with notify <run-id> --force.

  • Workflow JavaScript has no injected filesystem, shell, process, or imports.
  • The JavaScript VM is not a hardened sandbox for hostile code; review scripts.
  • Codex workers default to the read-only sandbox.
  • Headless approval policy is never, so unavailable permissions fail.
  • Write workers must declare workspace-write and the launcher must pass --allow-workspace-write after review.
  • Danger-full-access also requires --allow-danger-full-access at launch.
  • Write authorization is bound to the reviewed workflow source hash.
  • Parallel write tasks should use non-overlapping files or worktrees.
  • Runs default to 100 agents and can be raised only as high as 1,000.
  • The default runner deadline is four hours; worker timeout is 30 minutes.
  • Pipelines without maxItems stop at 100 items; explicit caps stop at 1,000.
  • Prompts and results are capped at 1 MB; retries are limited to five.
  • A supervisor terminates runaway JavaScript and complete worker process groups.
  • Resume and cancel remove process groups orphaned by a supervisor crash.
  • An unexpected engine exit removes active worker groups before terminal state.
  • Workers receive prompts only after their process ownership is durable.
  • Every worker exit drains surviving descendants in its owned process group.
  • Persisted PIDs are signaled only when their process-start identity still matches, protecting unrelated processes after PID reuse.
  • Unconfirmed cleanup remains recoverable and nonterminal for a later retry.
  • Unawaited agent, pipeline, and parallel calls fail instead of leaking work.
  • Completion callbacks are explicit, detached-only, and limited to local Unix app-server sockets.
  • The callback verifies its target before launch and has an absolute retry deadline.
  • It never answers app-server approvals or user-input requests for the TUI.
  • Workflow results embedded in callback messages are marked as untrusted data.
  • Callback delivery failure does not rewrite the workflow’s terminal result.

Context-capacity failures are classified as non-retryable. Chunk the failing input or use tree reduction, then resume; completed compatible siblings remain cached. Every executed workflow source revision is retained in its run state directory for diagnosis. Failed context steps also retain any worker thread ID and token usage emitted before the failure.

The plugin commits a dependency-free JavaScript bundle for installation. Its TypeScript source and fake-worker integration tests live beside it.

Terminal window
cd plugins/dynamic-workflow
npm ci
npm run typecheck
npm test