# `ObanClaude`
[🔗](https://github.com/genagent/oban_claude/blob/v0.10.2/lib/oban_claude.ex#L1)

Run a Claude Code job (via [`claude_wrapper`](https://hex.pm/packages/claude_wrapper))
on an Oban queue.

`oban_claude` is the thin seam between Oban and claude:

  * `run/2` is the engine: a string-keyed args map in, a `{oban_return, result}`
    tuple out. It calls `ClaudeWrapper.query/2` and maps the typed
    `%ClaudeWrapper.Result{}` / `%ClaudeWrapper.Error{}` onto Oban's return
    values via a classifier (see `ObanClaude.Outcome`).
  * `ObanClaude.Worker` is the drop-in worker (`use ObanClaude.Worker`) with a
    single `c:ObanClaude.Worker.handle_result/2` override point.

It owns no state and runs no daemon, and takes no position on what consumes a
result. Whether the agent effects its own writes (full-auto) or returns
structured data for a downstream effector is encoded in the *job's* args
(prompt + permission mode), never here, so `oban_claude` supports both.

## Example

Build the job's args with `ObanClaude.Args.new/1` (atom keys, validated) rather
than a hand-written string map:

    defmodule MyApp.ClaudeJob do
      use ObanClaude.Worker, queue: :claude, max_attempts: 3
    end

    ObanClaude.Args.new(prompt: "summarize the repo", working_dir: "/path/to/repo")
    |> MyApp.ClaudeJob.new()
    |> Oban.insert()

The raw string-keyed map is still accepted as the low-level escape hatch:

    %{"prompt" => "summarize the repo", "working_dir" => "/path/to/repo"}
    |> MyApp.ClaudeJob.new()
    |> Oban.insert()

## Telemetry

`run/2` emits the following events via `:telemetry`:

### `[:oban_claude, :run, :stop]`

Emitted after a successful `ClaudeWrapper.query/2` call. This includes
`is_error: true` results, which are returned without raising.

  * Measurements:
    * `:duration` -- wall time of the query in native time units (convert with
      `System.convert_time_unit/3`)
    * `:cost_usd` -- the reported cost in USD from the result (`0.0` when the
      result carries no cost)
  * Metadata:
    * `:result` -- the `%ClaudeWrapper.Result{}` struct
    * `:rate_limit_observations` -- ordered typed
      `%ClaudeWrapper.RateLimitObservation{}` values from this invocation;
      `[]` when the default query did not use an observed runner or no valid
      rate-limit event arrived. These are provider-reported observations,
      not a durable availability decision.
    * `:args` -- the string-keyed args map passed to `run/2`
    * `:job` -- a slim map `%{id, queue, worker, attempt, max_attempts, meta}`
      for the `Oban.Job`, or `nil` when `run/2` was called without `:job`

### `[:oban_claude, :run, :exception]`

Emitted when the query returns `{:error, _}` -- both a typed
`%ClaudeWrapper.Error{}` and an off-contract error term (which the classifier
cancels, so it still surfaces here).

  * Measurements:
    * `:duration` -- wall time of the query in native time units
    * `:cost_usd` -- the run's spend when the error carries one (a rail-stop
      `:max_budget_exceeded` / `:max_turns_exceeded` reason map does; `0.0`
      otherwise, including every off-contract term). Summing `:cost_usd` across
      *both* events gives a complete spend total.
  * Metadata:
    * `:error` -- the `%ClaudeWrapper.Error{}` struct, or the raw error term on
      the off-contract path
    * `:rate_limit_observations` -- any events observed before failure or
      timeout, with the same meaning as on `:stop`
    * `:args` -- the string-keyed args map passed to `run/2`
    * `:job` -- as above

> #### Data handling {: .warning}
>
> `:args` (prompt, system prompt, `:meta`), `:result`, and `:error` (which can
> embed raw CLI stdout/stderr) all ride on these events. Redact before shipping
> telemetry to a log aggregator.

# `classifier`

```elixir
@type classifier() :: (wrapper_outcome() -&gt; {oban_return(), term()})
```

Maps a `t:wrapper_outcome/0` onto the `{oban_return, payload}` envelope. The
first element MUST be a valid `t:oban_return/0`; `run/2` raises otherwise. A
common mistake is a flat `{:cancel, reason}` -- that is a bare verdict, not
the `{verdict, payload}` envelope the `ObanClaude.Worker` path unwraps.

# `oban_return`

```elixir
@type oban_return() ::
  :ok
  | {:ok, term()}
  | {:error, term()}
  | {:cancel, term()}
  | {:snooze, Oban.Period.t()}
```

An `c:Oban.Worker.perform/1` return value. `:snooze` accepts Oban's full
`t:Oban.Period.t/0` -- a `pos_integer` of seconds or a `{n, unit}` tuple. The
default classifier (`ObanClaude.Outcome`) never returns `:snooze` (it would
bypass `max_attempts`); a `:classifier` override may.

# `query_fun`

```elixir
@type query_fun() :: (String.t(), keyword() -&gt; wrapper_outcome())
```

The claude entrypoint: `(prompt, query_opts) -> t:wrapper_outcome/0`.

# `wrapper_outcome`

```elixir
@type wrapper_outcome() ::
  {:ok, ClaudeWrapper.Result.t()} | {:error, ClaudeWrapper.Error.t() | term()}
```

What `ClaudeWrapper.query/2` (or a custom `:query_fun`) returns: a typed
`%Result{}`, or a typed `%Error{}` (an arbitrary `term()` only off the
documented contract).

# `cost_usd`

```elixir
@spec cost_usd(ClaudeWrapper.Result.t() | ClaudeWrapper.Error.t() | term()) ::
  float() | nil
```

Read the run's cost in USD from a run's payload, or `nil` if unavailable.

Like `session_id/1`, it hides the success-vs-rail-stop shape split:
`result.cost_usd` on a `%Result{}`, `error.reason.cost_usd` on a rail-stop
`%Error{}`. Use it in `c:ObanClaude.Worker.handle_error/3` to record spend
before enqueuing a resume job.

# `outcome`

```elixir
@spec outcome(ClaudeWrapper.Result.t()) :: String.t() | nil
```

Read the `"outcome"` string from a result's `structured_output`, when a
`--json-schema` run produced one. Returns `nil` otherwise.

Useful inside `c:ObanClaude.Worker.handle_result/2` to branch on a typed
outcome (e.g. `"done"` vs `"blocked"`).

# `run`

```elixir
@spec run(ObanClaude.Args.t(),
  classifier: classifier(),
  query_fun: query_fun(),
  job: Oban.Job.t()
) ::
  {oban_return(), ClaudeWrapper.Result.t() | ClaudeWrapper.Error.t() | term()}
```

Run a claude job from a string-keyed `args` map.

Returns `{oban_return, %Result{} | %Error{}}` so a caller can both act on the
Oban verdict and inspect the underlying run (cost, session id, structured
output). `:prompt` is required; a curated subset of `ClaudeWrapper.query/2`
options (the keys in the `ObanClaude.Args` options table) is forwarded, and any
other key in the map is silently ignored.

Options:

  * `:classifier` -- a `t:classifier/0`: a 1-arity fun mapping the claude
    call's outcome onto the `{oban_return, payload}` envelope. The first
    element is the Oban verdict; `run/2` raises `ArgumentError` if it is not a
    valid `t:oban_return/0` (guarding the flat-`{:cancel, reason}` mistake
    that Oban would otherwise silently treat as success). Defaults to
    `&ObanClaude.Outcome.classify/1`.
  * `:query_fun` -- a `t:query_fun/0`, the claude entrypoint. Defaults to
    `&ClaudeWrapper.query/2`. Override to stub claude in tests, or to route
    through a different wrapper entrypoint.
  * `:job` -- the `Oban.Job` this run belongs to. Its identity (`id`, `queue`,
    `worker`, `attempt`, `meta`) rides along in both telemetry events'
    `:job` metadata for cost attribution. `ObanClaude.Worker` passes it
    automatically; bare `run/2` callers may omit it (`:job` is then `nil`).

# `session_id`

```elixir
@spec session_id(ClaudeWrapper.Result.t() | ClaudeWrapper.Error.t() | term()) ::
  String.t() | nil
```

Read the claude session id from a run's payload, or `nil` if there is none.

The handle lives in two different places depending on the outcome, and this
reader hides that split so callers never touch `claude_wrapper` internals:

  * on a success (or `is_error` `%Result{}`), it is `result.session_id`;
  * on a rail-stop `%Error{}` (`:max_turns_exceeded` / `:max_budget_exceeded`),
    it is `error.reason.session_id`.

Use it inside `c:ObanClaude.Worker.handle_error/3` to enqueue a `resume:` job
after a rail stop. Any other error shape (an atom reason, a missing field)
yields `nil`.

# `structured`

```elixir
@spec structured(ClaudeWrapper.Result.t()) :: map() | list() | nil
```

Return the full schema-validated `structured_output` from a result (a
`--json-schema` run), or `nil` when the run produced none.

Use inside `c:ObanClaude.Worker.handle_result/2` to branch on a typed result
object. `outcome/1` is the convenience for the common `"outcome"` key.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
