A Continuum instance is a named runtime — its own registry, run supervisor, dispatchers, timer wheel, signal router, lease heartbeater, snapshotter, optional partition maintainer, and recovery process — bound to a single Ecto repo. Most applications only ever run one instance and never have to think about this. Umbrella apps and library hosts that genuinely have more than one repo can run more than one Continuum instance in the same BEAM.
The Default Instance
Continuum ships with a default instance named Continuum. The library
application always starts only the repo-independent base processes: PubSub,
Registry, the in-memory journal, and the default run supervisor. Configuring a
repo does not start the durable runtime workers, because a dependency
application starts before the host application's repo.
Configure the Postgres journal and repo:
# config/config.exs
config :continuum,
repo: MyApp.Repo,
journal: Continuum.Runtime.Journal.PostgresThen supervise the repo-dependent children in the host application, after the repo:
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children =
[MyApp.Repo] ++
Continuum.children() ++
[MyAppWeb.Endpoint]
Supervisor.start_link(children,
strategy: :one_for_one,
name: MyApp.Supervisor
)
end
endContinuum.children() returns the lease heartbeater, recovery, workflow and
activity dispatchers, activity supervisor, snapshotter, timer wheel, signal
router, and version registry for the configured repo. Do not place these
children before MyApp.Repo.
All public calls operate on the default instance unless you say otherwise:
{:ok, run_id} = Continuum.start(MyApp.OrderFlow, input)
:ok = Continuum.signal(run_id, :fraud_review, :approved)
{:ok, _} = Continuum.await(run_id, 5_000)For an intentionally in-memory default instance, leave :repo unset;
Continuum.children() then returns [] because the library application
already owns every process that instance needs.
Named Instances
To run a second instance side by side with the default, or to embed Continuum
in a host that owns the repo, add child specs with Continuum.children/1:
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [
MyApp.Repo,
MyApp.Billing.Repo
] ++ Continuum.children(name: :billing_continuum, repo: MyApp.Billing.Repo)
Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
end
endContinuum.children/1 registers the instance during child-spec construction
(via :persistent_term), so the name is resolvable by the time the
supervisor starts the children. The returned list includes one child for each
of: PubSub, registry, run supervisor, activity worker supervisor, lease
heartbeater, dispatcher, activity dispatcher, snapshotter, timer wheel, and
signal router — all named under the instance.
Calling Continuum.children(name: Continuum, repo: MyApp.Repo) returns the
repo-dependent default runtime children. It deliberately omits PubSub,
Registry, and the run supervisor because Continuum.Application already owns
those base processes.
You can compose multiple Continuum.children/1 calls into one tree:
children =
[MyApp.Repo, MyApp.Billing.Repo] ++
Continuum.children(name: :orders_continuum, repo: MyApp.Repo) ++
Continuum.children(name: :billing_continuum, repo: MyApp.Billing.Repo)Per-child options are accepted for fine-grained tuning: :heartbeater,
:run_supervisor, :activity_supervisor, :recovery, :dispatcher,
:activity_dispatcher, :timer_wheel, :signal_router, :snapshotter, and
:partition_maintainer. Pass false for any child to omit it from the list
(for example, omit :activity_dispatcher if the host wants its own custom
worker pool). Partition maintenance is false by default because it requires
runtime DDL permissions; enable it with true or an option list.
Calling Into a Named Instance
Public entry points accept an :instance option:
{:ok, run_id} =
Continuum.start(MyApp.BillingFlow, input, instance: :billing_continuum)
:ok = Continuum.signal(run_id, :invoice_paid, %{txid: "..."}, instance: :billing_continuum)
:ok = Continuum.cancel(run_id, instance: :billing_continuum)
{:ok, _} = Continuum.await(run_id, 5_000, instance: :billing_continuum)The four-arity Continuum.signal/4 keeps the payload positional; do not pass
options as the third argument.
Unknown instance names raise Continuum.InstanceNotRegisteredError. Register
the instance once (Continuum.children/1) before any call that names it.
Run Isolation
Two instances are fully isolated:
- A run started in one instance is invisible to the other.
- The same
run_idcan exist in both instances simultaneously without collision — the registry, lease owner, and PubSub topics are all namespaced. - The lease-owner string includes the instance name
(
node()/instance/monotonic_int), so Postgres CAS writes from one instance cannot accidentally validate against another.
Durable Cancellation Without a Local Engine
Continuum.cancel(run_id, instance: name) works even when no Engine process is
alive for that run. The facade resolves the instance, acquires the run lease
through the Postgres journal, and runs the cancellation transaction directly.
A live engine still owning the lease causes the cancel to fail cleanly, and
the caller can retry once the engine has released or lost the lease.
This is the same code path used for normal cancellation; there is no separate
cancel_durable/2 API.
Telemetry and Tracing
All Continuum telemetry events include instance: name in metadata. Use that
key to split dashboards or filter spans when more than one instance is active
in the same BEAM. The OpenTelemetry bridge (Continuum.OpenTelemetry.setup/1)
emits the same attribute as continuum.instance on every run-attempt and
activity-attempt span.
When to Use More Than One Instance
Run more than one instance when:
- the host application genuinely has more than one repo and each repo owns different business workflows
- you need hard isolation between unrelated tenants and tenant repos are already separate
- a library embeds Continuum and the host already owns the Continuum repo, so the library should not start the default instance from app env
Otherwise, keep the default; it is the simpler operational story.