A durable, resumable workflow engine for Elixir, similar to Temporal/Inngest.
- Declarative DSL - Clean macro-based workflow definitions
- Resumability - Sleep, wait for events, wait for human input
- Reliability - Automatic retries with configurable backoff strategies
- Observability - Built-in log capture per step (coming soon)
- Persistence - PostgreSQL-backed execution state
Add durable to your list of dependencies in mix.exs:
def deps do
[
{:durable, "~> 0.1.0"}
]
end# config/config.exs
config :durable,
ecto_repos: [Durable.Repo]
config :durable, Durable.Repo,
username: "postgres",
password: "postgres",
hostname: "localhost",
database: "durable_dev"mix ecto.create
mix ecto.migratedefmodule MyApp.OrderWorkflow do
use Durable
use Durable.Context
workflow "process_order", timeout: hours(2) do
step :validate do
order = input().order
put_context(:order_id, order.id)
put_context(:items, order.items)
end
step :calculate_total do
items = get_context(:items)
total = Enum.sum(Enum.map(items, & &1.price))
put_context(:total, total)
end
step :charge_payment, retry: [max_attempts: 3, backoff: :exponential] do
total = get_context(:total)
{:ok, charge} = PaymentService.charge(get_context(:order_id), total)
put_context(:charge_id, charge.id)
end
step :send_confirmation do
EmailService.send_confirmation(get_context(:order_id))
end
end
end{:ok, workflow_id} = Durable.start(MyApp.OrderWorkflow, %{order: order}){:ok, execution} = Durable.get_execution(workflow_id)
execution.status # => :completed
execution.context # => %{order_id: 123, total: 99.99, charge_id: "ch_xxx"}workflow "name", timeout: hours(2), max_retries: 3 do
# steps...
endstep :name do
# step logic
end
step :name, retry: [max_attempts: 3, backoff: :exponential] do
# step with retry
end
step :name, timeout: minutes(5) do
# step with timeout
enduse Durable.Context
# Read
context() # Get entire context
get_context(:key) # Get specific key
get_context(:key, default) # Get with default
input() # Get initial input
workflow_id() # Get current workflow ID
# Write
put_context(:key, value) # Set single key
put_context(%{k1: v1}) # Merge map
update_context(:key, &(&1 + 1))
delete_context(:key)
# Accumulators
append_context(:list, value)
increment_context(:counter, 1)seconds(30) # 30,000 ms
minutes(5) # 300,000 ms
hours(2) # 7,200,000 ms
days(7) # 604,800,000 ms:exponential- Delay = base^attempt * 1000ms (default):linear- Delay = attempt * base * 1000ms:constant- Fixed delay between retries
step :api_call, retry: [
max_attempts: 5,
backoff: :exponential,
base: 2,
max_backoff: 60_000 # Cap at 1 minute
] do
ExternalAPI.call()
endDurable.start(Module, input)
Durable.start(Module, input,
workflow: "name",
queue: :high_priority,
priority: 10,
scheduled_at: ~U[2025-01-01 00:00:00Z]
)Durable.get_execution(workflow_id)
Durable.get_execution(workflow_id, include_steps: true)
Durable.list_executions(
workflow: MyApp.OrderWorkflow,
status: :running,
limit: 100
)Durable.cancel(workflow_id)
Durable.cancel(workflow_id, "reason")- Wait primitives (
sleep_for,wait_for_event,wait_for_input) - Decision steps and branching
- Parallel execution
- Loop and foreach constructs
- Compensation/Saga patterns
- Cron scheduling
- Graph visualization
- Phoenix LiveView dashboard
MIT