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: :continue edges 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 2 is 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.