ObanPowertools.Worker is the default builder-facing entry point when you want app code to
enqueue typed jobs without pushing validation and duplicate suppression into every caller.
What the wrapper adds
- typed
args:declarations backed by an embedded schema validate/1for synchronous argument validationenqueue/2for idempotent inserts through the Powertools receipt table- optional
limits:declarations when the worker also needs durable rate control
The runtime still executes an Oban.Worker. Powertools just makes the builder contract stricter.
Minimal worker
defmodule MyApp.Billing.ProcessInvoiceWorker do
use ObanPowertools.Worker,
queue: :billing,
args: [
account_id: :integer,
invoice_id: :integer,
reason: :string
]
@impl true
def process(%Oban.Job{args: %__MODULE__.Args{} = args}) do
MyApp.Billing.process_invoice(args.account_id, args.invoice_id, args.reason)
:ok
end
endWhat callers do
case MyApp.Billing.ProcessInvoiceWorker.enqueue(%{
account_id: 42,
invoice_id: 9001,
reason: "manual_retry"
}) do
{:ok, job} ->
{:ok, job.id}
{:conflict, job} ->
{:ok, job.id}
{:error, %Ecto.Changeset{} = changeset} ->
{:error, changeset}
endImportant behavior:
- invalid args fail before the job is inserted
- the worker receives a casted
%__MODULE__.Args{}struct - duplicate enqueue attempts return
{:conflict, job}for the original durable insert
That gives callers one stable API instead of ad hoc validation and “did we already queue this?” checks spread across controllers, LiveViews, and services.
Validation without enqueue
Use validate/1 when you want to fail fast before presenting or persisting a user-facing action:
MyApp.Billing.ProcessInvoiceWorker.validate(%{
account_id: 42,
invoice_id: 9001,
reason: "manual_retry"
})When to stay on the wrapper
Use ObanPowertools.Worker when your job benefits from:
- a stable typed argument shape
- duplicate suppression at enqueue time
- a durable limiter binding
Use plain Oban.Worker only when you explicitly do not want those guarantees.