Hephaestus is modular by design. The core package provides the workflow engine with in-memory storage (ETS) and local execution (GenServer per instance) — enough to develop, test, and run single-node deployments. Extension packages plug into the same adapter contracts to add production capabilities without changing your workflow definitions or step implementations.

This guide explains what each extension does, when to use it, and how to set it up.

Choosing the right stack

You need...PackagesWhy
Prototyping, tests, single-nodehephaestusETS storage + local runner. Zero external deps.
Workflows that survive restartshephaestus + hephaestus_ectoEcto adapter persists instances as JSONB in PostgreSQL.
Distributed execution, retries, job queueshephaestus + hephaestus_ecto + hephaestus_obanOban runner replaces GenServer with durable, retryable jobs.
Full production stackAll three + built-in telemetryPersistent, distributed, observable workflows.

Start with core alone during development. Add extensions as your requirements grow — each one is a dependency swap, not a rewrite.

hephaestus_ecto

Ecto/PostgreSQL storage adapter. Replaces the built-in ETS storage with a single workflow_instances table backed by JSONB and GIN indexing.

What it provides

  • HephaestusEcto.Storage — implements the Hephaestus.Runtime.Storage behaviour (get/1, put/1, delete/1, query/1).
  • Migration generatormix hephaestus_ecto.gen.migration creates the workflow_instances table with UUID primary key, B-tree indexes on workflow and status, and a GIN index (jsonb_path_ops) on the state JSONB column.
  • Serializer — handles round-trip conversion between Elixir types (atoms, MapSets, DateTime) and JSONB-safe representations. Deserialization uses String.to_existing_atom/1 — no arbitrary atom creation from database values.
  • Named instances — multiple storage instances can coexist for multi-tenant setups.

When to use

  • Your workflows must survive VM restarts or deployments.
  • You need to query workflow state from outside the running application (admin dashboards, reporting, debugging).
  • You're running multiple nodes and need a shared persistence layer.

Setup

Add to your mix.exs:

def deps do
  [
    {:hephaestus, "~> 0.2.0"},
    {:hephaestus_ecto, "~> 0.1.0"}
  ]
end

Generate and run the migration:

mix hephaestus_ecto.gen.migration
mix ecto.migrate

Configure your engine module to use Ecto storage:

defmodule MyApp.Hephaestus do
  use Hephaestus,
    storage: {HephaestusEcto.Storage, repo: MyApp.Repo},
    runner: Hephaestus.Runtime.Runner.Local
end

Everything else stays the same — your workflows, steps, and supervision tree don't change. Instances are now persisted to PostgreSQL instead of ETS.

Workflow versioning in hephaestus 0.2.x does not require a new Ecto migration: the resolved workflow_version is stored inside the serialized instance state.

Querying instances

The storage adapter supports filtering by status and workflow:

HephaestusEcto.Storage.query(status: :running)
HephaestusEcto.Storage.query(workflow: MyApp.OrderWorkflow)
HephaestusEcto.Storage.query(status: :waiting, workflow: MyApp.PaymentWorkflow)

For advanced queries against the JSONB state column, use Ecto directly:

import Ecto.Query

from(i in HephaestusEcto.Schema.Instance,
  where: fragment("state @> ?", ^%{"context" => %{"initial" => %{"order_id" => 123}}})
)
|> MyApp.Repo.all()

Requirements

  • PostgreSQL 9.4+ (JSONB and GIN indexes)
  • Ecto SQL ~> 3.10

hephaestus_oban

Oban-based runner adapter. Replaces the built-in local runner (one GenServer per instance) with durable Oban jobs. Brings distributed execution, persistent job queues, automatic retries with backoff, and advisory-lock-based concurrency control.

Requires hephaestus_ecto — the Oban runner persists instances through the Ecto storage adapter.

What it provides

  • HephaestusOban.Runner — implements the Hephaestus.Runtime.Runner behaviour using three Oban workers.

  • Three-worker architecture:

    WorkerRoleWrites to Instance?
    AdvanceWorkerOrchestrator. Reads step results, applies engine transitions, persists the instance. Serialized per instance via Oban unique + pg_advisory_xact_lock.Yes (single writer)
    ExecuteStepWorkerExecutes a single step. Writes result to step_results table, enqueues AdvanceWorker. Idempotent via existence check.No
    ResumeWorkerHandles external events and durable timers. Writes to step_results, enqueues AdvanceWorker.No
  • Zero-contention fan-out — during parallel execution, each step worker writes to an auxiliary hephaestus_step_results table instead of the instance directly. The AdvanceWorker is the single writer, serialized via advisory lock.

  • Failure handling — when a step exhausts all retries, the FailureHandler telemetry listener marks the workflow as :failed and cancels remaining jobs.

  • Durable timersschedule_resume/3 creates an Oban job that survives VM restarts, unlike the local runner's process-local timers.

  • Job observability — automatically populates Oban job meta and tags from the workflow's __tags__/0 and __metadata__/0 (declared via use Hephaestus.Workflow, tags: [...], metadata: %{...}). Enables filtering in Oban Web by workflow type, instance ID, step name, or custom labels.

  • Retry configuration — resolves with most-specific-wins priority:

    1. Step.retry_config/0 (per-step override)
    2. Workflow.default_retry_config/0 (per-workflow default)
    3. Library default: %{max_attempts: 5, backoff: :exponential, max_backoff: 60_000}

When to use

  • You need workflows to execute across multiple nodes (distributed).
  • Steps must retry automatically on failure with configurable backoff.
  • You need durable timers that survive deployments.
  • You want Oban's job management UI (Oban Web) for monitoring workflow execution.

Setup

Add to your mix.exs:

def deps do
  [
    {:hephaestus, "~> 0.2.0"},
    {:hephaestus_ecto, "~> 0.1.0"},
    {:hephaestus_oban, "~> 0.1.0"}
  ]
end

Generate and run both migrations (Ecto migration must come first):

mix hephaestus_ecto.gen.migration
mix hephaestus_oban.gen.migration
mix ecto.migrate

Configure your engine module:

defmodule MyApp.Hephaestus do
  use Hephaestus,
    storage: {HephaestusEcto.Storage, repo: MyApp.Repo},
    runner: {HephaestusOban.Runner, oban: MyApp.Oban}
end

Set up your supervision tree:

def start(_type, _args) do
  children = [
    MyApp.Repo,
    {Oban, name: MyApp.Oban, repo: MyApp.Repo, queues: [hephaestus: 10]},
    MyApp.Hephaestus
  ]

  Supervisor.start_link(children, strategy: :one_for_one)
end

The hephaestus: 10 queue configuration means up to 10 Oban jobs run concurrently. In a fan-out of 20 steps, only 10 execute at once — the rest wait in the queue. Adjust based on your workload.

Workflow versioning also requires no extra Oban migration. The runner reads the resolved workflow module and workflow_version from the serialized instance state.

For fine-grained control, use separate queues for orchestration and execution:

{Oban, queues: [hephaestus_advance: 5, hephaestus_execute: 20]}

Requirements

  • Oban >= 2.14
  • PostgreSQL (advisory locks and JSONB)
  • hephaestus_ecto ~> 0.1.0

Combining extensions

The typical production stack uses both extensions together. Here's a complete setup:

# mix.exs
def deps do
  [
    {:hephaestus, "~> 0.2.0"},
    {:hephaestus_ecto, "~> 0.1.0"},
    {:hephaestus_oban, "~> 0.1.0"}
  ]
end
# lib/my_app/hephaestus.ex
defmodule MyApp.Hephaestus do
  use Hephaestus,
    storage: {HephaestusEcto.Storage, repo: MyApp.Repo},
    runner: {HephaestusOban.Runner, oban: MyApp.Oban}
end
# lib/my_app/application.ex
def start(_type, _args) do
  # Telemetry is built-in — just attach the log handler
  Hephaestus.Telemetry.LogHandler.attach()

  children = [
    MyApp.Repo,
    {Oban, name: MyApp.Oban, repo: MyApp.Repo, queues: [hephaestus: 10]},
    MyApp.Hephaestus
  ]

  Supervisor.start_link(children, strategy: :one_for_one)
end

Your workflows and steps remain identical to the core-only version. The only change is the adapter configuration in use Hephaestus and the supervision tree.

Migration path

Hephaestus is designed to grow with your application:

  1. Start with core alone. Use ETS storage and the local runner during development and testing. No database, no external dependencies. Focus on getting your workflow logic right.

  2. Add Ecto when you need persistence. When workflows must survive restarts or you need to query state externally, add hephaestus_ecto. Swap the storage adapter — everything else stays the same.

  3. Add Oban when you need distribution. When you deploy to multiple nodes or need automatic retries and durable timers, add hephaestus_oban. Swap the runner adapter.

  4. Enable telemetry when you need observability. Telemetry is built into the core since 0.1.5. Just call Hephaestus.Telemetry.LogHandler.attach() in your application startup for structured logs, or plug Hephaestus.Telemetry.Metrics.metrics() into your reporter for dashboards.

Each step is additive. You never need to rewrite workflows or steps — the adapter pattern ensures that the runtime changes underneath while your business logic stays exactly the same.