OTP-native durable execution engine for Elixir.
Continuum lets you write a multi-step business process as straight-line Elixir code. The process survives crashes, node restarts, and partitions: the engine journals each effect to Postgres and replays the workflow's history through the same orchestration code on resume.
See Continuum.Workflow for the workflow DSL and Continuum.Activity for
activities (the only place side effects are allowed inside a workflow).
Public API
children/1— Postgres runtime child specs for host supervision treesdrain/1,readiness/1,ready?/1, anddrained?/1— deployment lifecyclestart/3— start a new workflow runsignal/3— deliver an external signal to a running workflowcancel/2— cancel a running workflowawait/2— block until a workflow completes (test/synchronous use)query/1andget_run/2— inspect durable runsset_attributes/3— externally update run search attributesnow/0,uuid4/0,random/0,side_effect/1— deterministic primitives callable from workflow codepatched?/1— journaled patch marker for compatible workflow changes
Summary
Functions
Block until the run completes. Test/synchronous use only.
Cancel a running workflow.
Returns runtime child specs for a Continuum instance.
Drains one Continuum runtime without stopping its supervisor.
Returns true after a successful bounded drain has completed.
Load one durable run by id.
Whether we are currently executing inside a workflow process. Useful in helper modules that branch on context.
The current wall-clock time, journaled and replayed deterministically.
Journaled patch marker for in-place, backward-compatible workflow changes.
Query durable runs with a closed, structured query spec.
Query durable runs for a named Continuum instance.
A pseudo-random float in [0, 1), journaled and replayed deterministically.
Returns the local lifecycle and claim-readiness state for one runtime.
Returns true only while the local runtime accepts new run claims.
Merge JSON-encodable search attributes into a durable run row.
General-purpose escape hatch for an impure read whose result must be journaled and replayed.
Deliver a signal to a running workflow. Signal payloads must not contain PIDs, references, ports, or functions.
Deliver a signal to a running workflow, selecting a Continuum instance with
:instance. Signal payloads follow the same durable-term restrictions as
workflow inputs.
Start a new workflow run.
The current UTC date, journaled and replayed deterministically.
Recover an activity's raw return value from a compensation handle.
A v4 UUID, journaled and replayed deterministically.
Types
Functions
Block until the run completes. Test/synchronous use only.
Failed workflows return a %Continuum.RunFailure{} in the result map's
:error field. Diagnostic stacktraces are intentionally omitted; use
get_run/2 to inspect the separate :error_stacktrace field.
Cancel a running workflow.
Cancelling the root of a continue_as_new chain cancels the live tip. When
the run's engine is alive on another reachable node, the cancel is forwarded
to it. If the owner holds a live lease but no engine can be reached
(partition, overload), the request is recorded durably and honored by the
owning engine on its next lease heartbeat — the call returns
{:error, :owned_elsewhere} so the caller knows cancellation is pending
rather than complete.
@spec children(keyword()) :: [Supervisor.child_spec()]
Returns runtime child specs for a Continuum instance.
For the default instance, add these children to the host application's supervision tree after its Ecto repo:
children =
[
MyApp.Repo
] ++ Continuum.children()Named instances additionally own their isolated PubSub, registry, and run supervisor:
children =
[
MyApp.Repo,
Continuum.children(name: :billing_continuum, repo: MyApp.Repo)
]Continuum.Application owns the default instance's Repo-independent base
processes. Continuum.children() returns only its Repo-dependent runtime
children, so it does not duplicate those process names.
A named instance given a :repo uses the Postgres journal for every run
started, signalled, cancelled, or awaited through it; pass :journal to
override. The default instance follows config :continuum, :journal.
Child-specific options may be passed with :workflow_modules,
:activity_executor, :activity_max_concurrency, :heartbeater, :run_supervisor,
:activity_supervisor, :recovery, :dispatcher, :activity_dispatcher,
:timer_wheel, :signal_router, :snapshotter, and :partition_maintainer.
Partition maintenance is disabled unless :partition_maintainer is true
or an option list because it requires runtime DDL privileges.
Passing false for a child omits it from the returned list.
Drains one Continuum runtime without stopping its supervisor.
Readiness becomes false before new run claims are paused. Locally owned workflow engines then get the configured heartbeater drain deadline to release their leases; overdue engines are stopped and fenced. Repeated calls are idempotent and return the same completed drain summary.
Pass instance: name for a named runtime and timeout: milliseconds to
override its configured :drain_timeout_ms for this drain.
Returns true after a successful bounded drain has completed.
Load one durable run by id.
@spec in_workflow?() :: boolean()
Whether we are currently executing inside a workflow process. Useful in helper modules that branch on context.
The current wall-clock time, journaled and replayed deterministically.
Journaled patch marker for in-place, backward-compatible workflow changes.
def run(input) do
if Continuum.patched?(:add_fraud_check_v2) do
activity FraudCheck.v2(input)
else
activity FraudCheck.v1(input)
end
endInside a workflow the first decision for patched?(name) is memoized for
the run. A live decision journals a patched event with value: true;
histories recorded before the patch line existed decide false without
consuming an event. Every later call with the same name returns that first
decision, including calls at another source line or after a suspension.
Outside a workflow process (test setup, ordinary code) it returns false.
Like now/0 and uuid4/0, this is a macro so it captures __CALLER__ for a
stable command identity; modules that call it must require Continuum
(use Continuum.Workflow does this for you).
Query durable runs with a closed, structured query spec.
See Continuum.Query for supported :where, :order_by, and pagination
options. Querying requires a Postgres-backed Continuum instance.
@spec query( atom() | Continuum.Runtime.Instance.t(), keyword() ) :: {:ok, map()} | {:error, term()}
Query durable runs for a named Continuum instance.
A pseudo-random float in [0, 1), journaled and replayed deterministically.
Returns the local lifecycle and claim-readiness state for one runtime.
The state is one of :ready, :draining, :drained, :degraded, or
:not_started. A degraded drain is not ready and reports a non-zero
unreleased_count in its last drain summary.
Returns true only while the local runtime accepts new run claims.
Merge JSON-encodable search attributes into a durable run row.
This is external metadata. It is not journaled and workflow code cannot read it during replay.
General-purpose escape hatch for an impure read whose result must be journaled and replayed.
The function is called once on first execution; its return value is
journaled and returned on every subsequent replay. Return values must be
replay-safe across processes and nodes. PIDs, references, ports, and
functions are rejected recursively with a path-aware
Continuum.DurableTermError.
This is a macro so Continuum can capture the source call site for a stable
command identity. Workflow modules that use Continuum.Workflow already
require Continuum; other modules must require Continuum before calling it.
Helper-module caveat
Command identity includes the call site's module and line. Inside a
workflow module that is safe: any edit changes the version hash and
in-flight runs keep resuming through the old version's entrypoint. A
Continuum.Pure helper module has no such protection — editing a helper
so that a side_effect call moves to a different line changes its command
identity and in-flight runs replaying through it raise
Continuum.ReplayDriftError on the next deploy. Prefer keeping
side_effect calls in the workflow module itself.
Deliver a signal to a running workflow. Signal payloads must not contain PIDs, references, ports, or functions.
Deliver a signal to a running workflow, selecting a Continuum instance with
:instance. Signal payloads follow the same durable-term restrictions as
workflow inputs.
@spec start(workflow_module(), input(), keyword()) :: {:ok, run_id()} | {:error, term()}
Start a new workflow run.
Options include :instance for selecting a named Continuum instance,
:namespace for soft tenant scoping of list/query paths, :trace_context for
persisting an opaque W3C traceparent binary that
observability integrations can use to link resumed run attempts, and
:attributes for JSON-encodable search metadata stored on the run row.
Inputs cross a journal boundary and must not contain PIDs, references, ports,
or functions. Continuum.DurableTermError reports the path to an invalid
nested value before the run is inserted.
The current UTC date, journaled and replayed deterministically.
Recover an activity's raw return value from a compensation handle.
When an activity/2 call uses compensate:, a success is returned as
{:ok, %Continuum.ActivityRef{}} rather than a bare term. unwrap/1 peels the
ref back to the activity's raw return:
unwrap(%Continuum.ActivityRef{raw_result: raw})→rawunwrap({:ok, %Continuum.ActivityRef{} = ref})→ref.raw_resultunwrap(other)→other(activities withoutcompensate:are unchanged)
A v4 UUID, journaled and replayed deterministically.