ObanPowertools.Workflow is a durable DAG definition and reconciliation layer. It is useful when
your app needs to persist step dependencies, unblock descendants from durable results, and show
why a step is still blocked.
It is not presented as a full orchestration platform. The current public contract is the stored workflow graph plus the runtime transitions documented here.
Build a workflow
alias ObanPowertools.Workflow
workflow =
Workflow.new(
name: "sync_customer",
workflow_context: %{"account_id" => 42, "label" => "Customer sync"}
)
|> Workflow.add(:fetch_customer, MyApp.FetchCustomerWorker.new(%{"account_id" => 42}))
|> Workflow.add(
:sync_billing,
MyApp.SyncBillingWorker.new(%{
"account_id" => 42,
"customer" => Workflow.result(:fetch_customer)
}),
deps: [:fetch_customer]
)
|> Workflow.add(
:notify,
MyApp.NotifyWorker.new(%{
"account_id" => 42,
"billing" => Workflow.result(:sync_billing)
}),
deps: [:sync_billing]
)Then persist it:
{:ok, persisted_workflow} = Workflow.insert(workflow, MyApp.Repo)Workflow.insert/2 validates:
- duplicate step names
- missing edge targets
- self-loops
- cycle creation
- JSON-safe workflow and step payloads
Complete a step
The runtime entry point today is Workflow.complete_step/4:
Workflow.complete_step(
MyApp.Repo,
persisted_workflow.id,
:fetch_customer,
status: :completed,
payload: %{customer_id: 1}
)That writes a durable step result, updates workflow counters, and reconciles descendants.
Current runtime semantics
These are the important builder-facing rules:
- a successful dependency releases children into
available - a retryable upstream step keeps descendants
pending - terminal failures cascade-cancel descendants by default
- explicit
policy: :continueedges let cleanup or compensating work continue - duplicate PubSub delivery does not duplicate child release or stored results
- DB-first reconciliation keeps the workflow correct even if no PubSub follow-up is observed
Canonical Workflow Semantics Contract
- Semantics version
2is the current workflow lifecycle contract. - Durable workflow, step, await, signal, callback, and recovery rows are the source of truth.
- Duplicate, late, ambiguous, dropped, and replayed signal paths remain durable evidence instead of hidden retries.
- Cancel requests remain durable request evidence, while final workflow outcome is recorded separately.
- Public workflow telemetry stays under
[:oban_powertools, :workflow, *]with bounded metadata only. <!-- workflow-semantics-contract:end -->
Diagnose why a step is still blocked
ObanPowertools.Explain.workflow_step(
persisted_workflow.id,
:notify,
repo: MyApp.Repo
)That returns the step state, blocker codes, and dependency snapshot that explain why a step is
still pending.
Rendered operator surfaces should lead with shared control-plane language such as Needs Review,
Blocked, Waiting, or Runnable, then preserve raw workflow semantics underneath.
Good fit
Use workflows when you need:
- a durable dependency graph
- visible blocker causality
- explicit release or cancellation semantics between steps
If you only need “enqueue a few follow-up jobs,” this layer is probably more than you need.