Namespaces are soft tenancy inside one Continuum instance and one repo. They are row metadata on continuum_runs, not a separate process tree.

{:ok, run_id} =
  Continuum.start(MyApp.OrderFlow, input,
    namespace: "tenant-a",
    attributes: %{region: "eu"}
  )

Continuum.query/1 and Continuum.Observer.list_runs/1 default to the "default" namespace. Pass namespace: "tenant-a" to list or search another namespace.

Run-id keyed operations remain global when :namespace is omitted because run ids are UUIDs. Service layers can add an expected-value guard to every access:

Continuum.get_run(run_id, namespace: "tenant-a")
Continuum.await(run_id, 5_000, namespace: "tenant-a")
Continuum.signal(run_id, :approved, %{}, namespace: "tenant-a")
Continuum.cancel(run_id, namespace: "tenant-a")
Continuum.set_attributes(run_id, %{region: "eu"}, namespace: "tenant-a")

Every guarded operation returns {:error, :not_found} when the run does not exist or its namespace differs, without revealing which case occurred and without mutating the run. Signal, cancel, and await retain their existing behavior of following continue_as_new to the live tip.

The host application still owns authentication and authorization; the guard prevents an already-authorized service path from accidentally crossing tenant boundaries. For hard isolation, use separate Continuum instances and repos; namespaces are a softer filter inside one instance.