Build a claude job's args map without knowing claude_wrapper or the CLI.
new/1 is the first-class front door: a keyword list with atom keys and native
Elixir values in, the string-keyed JSON map Oban stores out. It is the
preferred way to construct args; ObanClaude.run/2 and ObanClaude.Worker
still accept a raw string-keyed map directly as the low-level escape hatch.
ObanClaude.Args.new(prompt: "summarize the repo",
working_dir: "/repo",
permission_mode: :plan)
#=> %{"prompt" => "summarize the repo",
# "working_dir" => "/repo",
# "permission_mode" => "plan"}The result is a plain map, so it drops straight into a worker:
MyApp.ClaudeJob.new(ObanClaude.Args.new(prompt: "...")) |> Oban.insert()Worker defaults
defaults/1 is the same builder with :prompt optional, for the worker's
:args (the constant, prompt-less config). It evaluates at compile time, so it
works directly in the use:
use ObanClaude.Worker,
queue: :claude,
args: ObanClaude.Args.defaults(working_dir: ".", model: "sonnet",
permission_mode: :bypass_permissions)Job metadata
The :meta option carries non-claude values (an issue number, a correlation id)
through to the Oban job args, where handle_result/2 and telemetry can read
them. It is merged flat into the map (keys stringified) and is not validated as
a claude option:
ObanClaude.Args.new(prompt: "...", meta: %{"issue" => "173"})
#=> %{"prompt" => "...", "issue" => "173"}Because it is merged flat, :meta has two guardrails (both raise at build
time):
- A meta key may not collide with a claude option name (or
"prompt") -- otherwise it would become an unvalidated query option and could override a worker default across the merge. Rename the key, or set the option directly. - Meta values must be JSON-encodable (Oban stores args as JSON). A tuple or a
struct without a
Jason.Encoderraises here, naming the key, rather than deep insideOban.insert.
One caveat the guardrails cannot catch: an atom meta value is valid JSON
but round-trips back as a string (:high in, "high" out). Hand-built
%Oban.Job{args: ...} test jobs skip that round-trip, so a handle_result/2
clause matching on the atom passes in tests and silently never matches in
production. Use string meta values, and prefer Oban.Testing.perform_job/3
(which JSON-recodes args) over a hand-built job in worker tests.
Validation
Unlike the raw-map path (which forwards known keys and silently ignores the
rest), the builder validates against the schema below: unknown keys raise, and
each option's type (including the :permission_mode and :effort vocabularies)
is checked. Errors surface at construction / enqueue time rather than when the
job runs.
Options
:prompt(String.t/0) - Required. The claude prompt. The only required option fornew/1.:model(String.t/0) - Model name, e.g."sonnet"or"opus".:fallback_model(String.t/0) - Model to fall back to if the primary is unavailable.:working_dir(String.t/0) - Directory claude runs in.:binary(String.t/0) - Path to theclaudeCLI binary. Pin a known-good version per worker (e.g. indefaults/1) so a PATH auto-update mid-batch can't drift the run -- pairs well withhermetic. (The wrapper's:bundledmode is not exposed here; use a custom:query_funfor that.):add_dir- Extra directories claude may access -- a single path or a list of paths.:system_prompt(String.t/0) - Replace claude's system prompt.:append_system_prompt(String.t/0) - Append to claude's system prompt.:permission_mode- How claude handles tool-permission prompts.:allowed_tools(list ofString.t/0) - Whitelist of tools claude may use.:disallowed_tools(list ofString.t/0) - Blacklist of tools claude may not use.:mcp_config(list ofString.t/0) - MCP server config file paths.:agent(String.t/0) - Named agent/subagent to run as.:effort- Reasoning effort level.:max_turns(pos_integer/0) - Cap on agent turns in the run.:max_budget_usd- Cost ceiling for the run, in USD.:timeout(pos_integer/0) - Subprocess timeout in milliseconds.:json_schema(String.t/0) - An inline JSON Schema string for structured output, passed verbatim to the CLI's--json-schema(not a file path -- read the file yourself and pass its contents).:worktree- Run in a git worktree (--worktree).truefor an ephemeral worktree, a string for a named one (reusable across jobs -- e.g."issue-173"). Requiresworking_dirto be a git repo. Recommended for full-auto workers that write to a repo -- set it indefaults/1.:hermetic- Seal the ambient~/.claudeconfig so the run's surface is exactly what these args set (--setting-sources,--strict-mcp-config,--exclude-dynamic-system-prompt-sections; auth untouched).trueis an alias for:full.:fulldrops user + project + local ambient config;:projectseals project + local but keeps the user's global~/.claude. Recommended indefaults/1for reproducible server runs that should not depend on the host's config.:resume(String.t/0) - Resume a prior claude session by id (--resume). The id comes from a previous run --ObanClaude.session_id/1reads it off a%Result{}or a rail-stop%Error{}. The canonical use is the resume-after-rail-stop recipe: a follow-on job withresume:+ the same namedworktreecontinues a stopped run cheaply. Session state is node-local (see the "session-resume handoff" pattern in the Agent worker patterns guide).:session_id(String.t/0) - Pin the session id for this run (--session-id), so a known id can be resumed later. Distinct fromresume, which continues an existing id.:no_session_persistence(boolean/0) - Do not persist this run's session transcript under~/.claude(--no-session-persistence). Recommended for fire-and-forget batch workers: it stops unattended runs accumulating transcripts (disk growth and plaintext data at rest). Mutually exclusive with the resume recipe -- it deletes the transcriptresumewould need.:fork_session(boolean/0) - Fork a new session from the resumed one rather than continuing it in place (--fork-session), so the original transcript is left untouched.:meta- Non-claude metadata merged into the args map untouched (keys stringified). For valueshandle_result/2or telemetry needs -- an issue number, a correlation id. Explicit claude options win on a key collision. Note: meta set as a worker default (viadefaults/1) reaches telemetry (the merged args) but NOThandle_result/2, which sees only the job's own args -- put meta a handler reads on the job, not the worker default.
Summary
Types
The string-keyed args map the builder produces and ObanClaude.run/2
consumes -- what Oban serializes as the job's args. Keys are strings (claude
options plus any stringified :meta); values are JSON-clean.
Types
The string-keyed args map the builder produces and ObanClaude.run/2
consumes -- what Oban serializes as the job's args. Keys are strings (claude
options plus any stringified :meta); values are JSON-clean.
Functions
Build a worker-level defaults map: the same as new/1, but :prompt is
optional (worker :args are the prompt-less config the per-job args fill in).
Because it evaluates at compile time, it can be used directly in the worker's
use (see "Worker defaults" above).
@spec keys() :: [atom()]
The options the builder accepts.
Build a string-keyed args map from a keyword list of atom-keyed options.
Validates opts against the schema documented above (see "Options"). Raises
NimbleOptions.ValidationError on a missing :prompt, an unknown key, or a
value of the wrong type. Returns the map Oban serializes as the job's args.