# `AshHooks.Worker`
[🔗](https://github.com/baselabs/ash_hooks/blob/v2.0.3/lib/ash_hooks/worker.ex#L1)

The host-injected Oban worker (ADR-0004): the consuming app defines one
module, and the Oban beam compiles only where Oban exists. This macro
expands `use Oban.Worker` inside the host's compilation, so the package
itself never references a loaded Oban module and compiles Oban-free
(the CI no-optional leg's proof).

    defmodule MyApp.WebhookDeliveryWorker do
      use AshHooks.Worker,
        deliveries: MyApp.OutboundDelivery,
        endpoints: MyApp.WebhookEndpoint,
        secret_resolver: {MyApp.Secrets, :webhook_secret},
        queue: :webhooks,
        oban: MyApp.Oban
    end

Consumers pass `enqueue: {MyApp.WebhookDeliveryWorker, :enqueue}` to
`AshHooks.dispatch/4`; the generated `enqueue/2` is that seam.

Options:

  * `:deliveries`, `:endpoints` (required) — the consumer's resource
    modules carrying the `AshHooks.OutboundDelivery` / `AshHooks.Endpoint`
    extensions.
  * `:secret_resolver` (required, `{m, f}`) — resolves an endpoint's
    secret reference: `f(ref) :: {:ok, secret_binary} | {:error, term}`.
    The returned value signs the Standard Webhooks envelope
    (its `whsk_`/`whsec_` prefix only selects the key slot for
    rotation); legacy envelopes, when the signing mode uses them, are
    signed from the endpoint's `legacy_secret_ref` /
    `legacy_previous_secret_ref` references through this same resolver.
  * `:tenant_aware_secrets` (optional, default `false`) — switches the
    resolver contract to 2-arity: `f(ref, tenant)`. The tenant is the
    run's args-threaded tenant (the delivery row's tenant, serialized at
    enqueue). Explicit, no arity magic: the default 1-arity contract is
    unchanged.
  * `:snippet_redactor` (`{m, f}`, optional) — a consumer callback run
    on the raw captured body ahead of the package's snippet floor
    (domain-specific tokens need raw input). Only consulted on per-call
    `snippet_capture: true` diagnostic runs; a crash or invalid return
    degrades to the sanitized summary, never raw bytes. The capture
    flag itself is deliberately not a macro option (ADR-0005's snippet
    amendment: compile-time knobs are broad and quiet) — pass it in the
    `AshHooks.Delivery.run/2` config for a one-row diagnostic re-drive.
  * `:http` — the `AshHooks.Http` adapter (default `AshHooks.Http.Bounded`).
  * `:http_opts` — adapter options as a literal keyword list or a
    `{module, function, args}` callback resolved inside each job's monitored
    delivery deadline.
  * `:oban` — the Oban instance name (default the unnamed instance).
  * `:queue` (default `:ash_hooks`), `:timeout` (35,000 ms), and
    `:max_attempts` (20) — Oban Worker options. The job timeout must be
    greater than `:attempt_timeout + :finalization_allowance`.
  * `:attempt_timeout` (25,000 ms) and `:finalization_allowance` (5,000 ms)
    — the driver deadline and extra send-lease interval. The attempt budget
    includes endpoint lookup, secret resolution, signing, destination and
    DNS checks, HTTP, and result persistence. Keep application clocks
    synchronized because lease comparisons use the application clock.
  * `:delivery_max_attempts` (default 10), `:base_backoff_seconds` (2),
    `:max_backoff_seconds` (3600), `:retry_after_cap_seconds` (86_400) —
    the row-driven retry policy. For direct `AshHooks.Delivery.run/2` calls,
    `:delivery_max_attempts` becomes `:max_attempts`; the other retry option
    names and defaults are unchanged. Omitted or `nil` retry options use
    these defaults. The `Retry-After` cap is
    receiver-held-state budget: a receiver returning a large
    `Retry-After` holds its delivery row and its Oban job for up to the
    cap per attempt (the 86,400 default = up to 24 hours per attempt on
    one header; a receiver honoring `Retry-After` deliberately asks for
    exactly that). Lower the cap when the chosen posture is
    exhaust-fast (a wedged receiver dead-letters at the ceiling instead
    of holding state). The cap clamps `Retry-After`; it does not alter the
    backoff ladder.

The generated enqueue uses `fields: [:args]` with the complete delivery
key, delivery and endpoint resource names, source, route, endpoint/event,
and tenant keys. Uniqueness has an infinite period over runnable states
(`available`, `scheduled`, `executing`, and `retryable`). Completed,
canceled, and discarded jobs therefore permit a later recovery trigger.
Enqueue succeeds only after Oban returns a persisted runnable job whose
identity args match the request. Oban.Basic may briefly report a uniqueness
conflict without the winning job's ID while its advisory-lock peer commits;
the generated path retries that result for at most twenty 5 ms waits, then
returns `{:error, :job_not_persisted}`. Before insertion it binds a deferred
route once with a compare-and-set update; a different stored route returns
`{:error, :dispatch_route_conflict}`.
On multitenant deliveries the enqueue also serializes the row's tenant
into job args (the attribute value inverted through the resource's
`tenant_from_attribute`, so `Delivery.run/2`'s forward
`parse_attribute` round-trips for non-identity parsers too); the
uniqueness identity includes the serialized tenant. The
tenant must round-trip Oban's JSON encoding — string tenants (uuids,
slugs) are the supported shape; adopters with a custom `parse_attribute`
pair it with `tenant_from_attribute` (the default inverse is identity).

---

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