Continuum stores a content hash for every workflow run. On resume, the engine
dispatches through the run row's (workflow, version_hash) pair instead of
blindly calling the latest module with that name.
Generated Entrypoints
use Continuum.Workflow generates a hash-keyed entrypoint module for the
workflow body that was compiled:
defmodule MyApp.OrderFlow do
use Continuum.Workflow, version: 2
def run(input), do: ...
endCompiling that module also defines a hidden entrypoint named like
MyApp.OrderFlow.V_<hash>. Start runs with the public workflow module:
{:ok, run_id} = Continuum.start(MyApp.OrderFlow, input)The run row stores the logical workflow and the content hash. Fresh durable runs
delegate to the current generated entrypoint, and resumed durable runs resolve
the journaled (workflow, version_hash) back to the generated entrypoint that
matches the old body. A suspended run therefore resumes on old code even after a
new version of the public module is loaded.
__continuum_entrypoint__/0 returns the generated module for the currently
loaded public module. The generated modules are hidden from ExDoc with
@moduledoc false; keep old releases or generated beam files available until
runs that need those hashes have completed or been cancelled.
You can still use workflow: MyApp.LogicalFlow when several public modules
should share a logical workflow identity, but ordinary version upgrades no
longer require hand-written V1/V2 wrapper modules.
Durable Registry
Each Continuum instance supervises a registrar that upserts loaded workflow
versions into continuum_workflow_versions on boot. Transient database failures
are retried with bounded exponential backoff. The first time an engine resolves
a workflow that was not in the boot scan, it also waits for that version to be
registered durably before executing it. The hot path uses an in-memory registry
backed by :persistent_term; the table gives operators a durable view of known
workflow hashes.
Configure boot-time registration explicitly when your app can:
Continuum.children(
name: :orders_continuum,
repo: MyApp.Repo,
workflow_modules: [MyApp.OrderFlow]
)If workflow_modules: is omitted, Continuum falls back to
config :continuum, :workflow_modules and then to loaded modules that expose
__continuum_workflow__/0.
mix continuum.audit --repo MyApp.Repo reports the registrar state and lists
any loaded workflow hashes missing from the durable table.
Before deploying, check the inverse requirement: every version referenced by a live run must exist in the release being deployed.
mix continuum.versions.check --repo MyApp.Repo --strict
The task joins distinct non-terminal run versions with the entrypoints loaded
in the current BEAM. It exits non-zero in strict mode if a version is absent;
use --format json when a deployment controller needs structured output.
Unknown Versions
If a node claims a Postgres run whose (workflow, version_hash) is not loaded
on that node, it emits [:continuum, :run, :unknown_version], releases the
lease, and leaves the run suspended. An unknown version is a fact about one
node, not the run: another node in the cluster that has the version loaded can
claim and resume it on its next dispatcher poll (the common case during rolling
deploys).
If no node has the version, the run stays suspended and the telemetry event
fires on each claim attempt. To recover, deploy the missing entrypoint again or
cancel the run if it is no longer needed. Runs marked stuck_unknown_version
by older releases are flipped back to suspended at boot when a matching
version registers (Continuum.VersionRegistry.upsert_instance/2).
Relationship To patched?/1
Versioned entrypoints are for incompatible workflow-code changes. patched?/1
is for in-place compatible branches that can safely live together in one
entrypoint until old histories drain.