Run Codex CLI work on an Oban queue.
oban_codex is the thin seam between codex_wrapper and Oban:
run/2accepts the string-keyed map Oban stores, executes a JSONL Codex turn, and maps the outcome to an Oban return value.ObanCodex.Workersupplies the drop-in worker and two result hooks.ObanCodex.Argsvalidates native Elixir options before enqueue.- result helpers (
text/1,structured/1,session_id/1,usage/1) hide Codex's JSONL event representation.
The underlying successful payload stays a %CodexWrapper.Result{}. That
keeps the seam honest while the helpers offer the ergonomic fields available
directly in oban_claude.
Example
defmodule MyApp.CodexJob do
use ObanCodex.Worker, queue: :codex, max_attempts: 3
end
ObanCodex.Args.new(
prompt: "summarize the repository",
working_dir: "/path/to/repo",
sandbox: :read_only
)
|> MyApp.CodexJob.new()
|> Oban.insert()Telemetry
Every run emits [:oban_codex, :run, :start], followed by either
[:oban_codex, :run, :stop] when the CLI produced a result (including a
non-zero exit), or [:oban_codex, :run, :exception] when execution failed
before a result was available.
Measurements include :duration in native time units and :cost_usd, which
is always 0.0 because Codex's JSON event contract doesn't report price.
Metadata includes the stored args and a slim job identity map.
Data handling
Prompts, metadata, JSONL output, and CLI error text can ride on telemetry events. Redact them before exporting telemetry to a log aggregator.
Summary
Types
Maps a wrapper outcome to the {oban_return, payload} envelope.
An Oban.Worker.perform/1 return. :snooze accepts Oban's period form.
The injectable Codex entrypoint: (prompt, query_opts) -> outcome.
The result of the Codex entrypoint.
Functions
Return reported cost in USD.
Parse the Codex JSONL stdout into event structs.
Read the conventional top-level "outcome" string from structured output.
Run one Codex turn from a string-keyed args map.
Return the Codex thread/session id carried by a result or normalized error.
Decode the final agent message as a JSON object or array.
Return the final agent message text, or nil when no message was emitted.
Return token usage from the final turn.completed event, or nil.
Types
@type classifier() :: (wrapper_outcome() -> {oban_return(), term()})
Maps a wrapper outcome to the {oban_return, payload} envelope.
@type oban_return() :: :ok | {:ok, term()} | {:error, term()} | {:cancel, term()} | {:snooze, Oban.Period.t()}
An Oban.Worker.perform/1 return. :snooze accepts Oban's period form.
@type query_fun() :: (String.t(), keyword() -> wrapper_outcome())
The injectable Codex entrypoint: (prompt, query_opts) -> outcome.
@type wrapper_outcome() :: {:ok, CodexWrapper.Result.t()} | {:error, ObanCodex.Error.t() | term()}
The result of the Codex entrypoint.
Functions
@spec cost_usd(CodexWrapper.Result.t() | ObanCodex.Error.t() | term()) :: number() | nil
Return reported cost in USD.
Codex doesn't currently include price in its JSONL contract, so a normal
result returns nil. The error-map clause supports custom query functions.
@spec events(CodexWrapper.Result.t()) :: [CodexWrapper.JsonLineEvent.t()]
Parse the Codex JSONL stdout into event structs.
@spec outcome(CodexWrapper.Result.t()) :: String.t() | nil
Read the conventional top-level "outcome" string from structured output.
@spec run(ObanCodex.Args.t(), classifier: classifier(), query_fun: query_fun(), job: Oban.Job.t() ) :: {oban_return(), CodexWrapper.Result.t() | ObanCodex.Error.t() | term()}
Run one Codex turn from a string-keyed args map.
The return is {oban_return, payload}: callers get both the Oban verdict and
the underlying %CodexWrapper.Result{} or %ObanCodex.Error{}.
Options:
:classifier— maps the query outcome to the envelope; defaults toObanCodex.Outcome.classify/1.:query_fun— injectable(prompt, opts)entrypoint; defaults toObanCodex.Query.run/2.:job— the current%Oban.Job{}for telemetry attribution.
@spec session_id(CodexWrapper.Result.t() | ObanCodex.Error.t() | term()) :: String.t() | nil
Return the Codex thread/session id carried by a result or normalized error.
Current Codex versions emit thread_id; session_id remains a defensive
fallback for older versions and forks.
@spec structured(CodexWrapper.Result.t()) :: map() | list() | nil
Decode the final agent message as a JSON object or array.
Codex's --output-schema validates the final message but still emits it as
text inside an item.completed event. Scalar JSON values intentionally
return nil; structured worker contracts should use an object or array.
@spec text(CodexWrapper.Result.t()) :: String.t() | nil
Return the final agent message text, or nil when no message was emitted.
Query runs always force JSONL. For a custom query function that returns plain stdout, a non-empty stdout string is used as a compatibility fallback.
@spec usage(CodexWrapper.Result.t()) :: map() | nil
Return token usage from the final turn.completed event, or nil.