Continue-as-new closes one active workflow run and starts one fresh successor with explicit durable lineage. Use it for recurring or paginated workflows that would otherwise accumulate an unbounded run thread.

The predecessor becomes terminal with status :continued. The successor has a new run id, fresh run history, the selected current workflow definition, and only the declared successor input. Jizoku does not copy accumulated workflow context into the successor.

Native Step Return

A native Jizoku.Step can request continuation at its execution boundary:

defmodule Billing.Steps.AdvanceCursor do
  use Jizoku.Step, name: :advance_cursor

  @impl Jizoku.Step
  def run(%{cursor: cursor}, _context) when cursor < 100 do
    {:continue_as_new, %{cursor: cursor + 1},
     key: "cursor-#{cursor + 1}", definition: :current}
  end

  def run(%{cursor: cursor}, _context) do
    {:ok, %{completed_cursor: cursor}}
  end
end

The continuation key must be a stable, non-empty string. definition: :current is required. Input must be a storage-safe map accepted by the selected trigger's payload contract. The native control result is distinct from ordinary success; do not place :continue_as_new inside {:ok, output, opts}.

When a worker receives the native result, Jizoku durably completes the source attempt and fences the predecessor in one dispatch append. Recovery then applies the source, records the continuation intent, terminalizes the predecessor, starts or repairs the deterministic successor, and records repair completion. Crashes and duplicate delivery converge on the same successor without rerunning an already committed source action.

Public Command

Host code can continue a quiescent active run directly:

{:ok, successor} =
  Jizoku.continue_as_new(run_id,
    input: %{cursor: next_cursor},
    continuation_key: "cursor-#{next_cursor}"
  )

The public command is appropriate when host-owned control logic chooses the boundary after all planned work has applied. It rejects unsafe states such as active or pending attempts, manual gates, compensation or recovery work, dynamic work, graph mutation, and unresolved child starts. The native return is the usual choice when the workflow step itself decides to recur.

Exact retries return the same successor. Reusing the predecessor continuation with a different key or input fails closed. Queue, partition, trace, trigger, definition version, and definition fingerprint are preserved or validated from durable state rather than accepted as caller-controlled lineage.

Choosing The Right Primitive

NeedUseRun/history behavior
Bound one recurring workflow's historyContinue as newPredecessor becomes :continued; one fresh linked successor starts.
Recheck the same step later without consuming retry budget{:defer, reason, schedule_in: seconds}Same run and logical step continue after durable delayed visibility.
Start separately managed work discovered by a stepstart_child_run/4 or /5Independent child lifecycle with parent-child lineage.
Re-run prior workflow history for operator recoveryreplay/2New replay run linked to a source run under replay safety rules.
Start a workflow on a scheduleCron trigger and host schedulerIndependent scheduled runs with host-owned delivery and idempotency.
Add bounded executable nodes to the active runschedule_dynamic_work/3Same run gains durable dynamic nodes and graph overlays.

Continue-as-new is not a retry, replay, child start, or graph mutation. It is a terminal lifecycle transition followed by a fresh run with one explicit continuation edge.

Inspection And History Bounds

Single-run read models expose immediate lineage without recursively loading the chain:

  • inspect_run/2 and list_runs/2 include continuation.continued_from and continuation.continued_to plus a history size classification.
  • inspect_run_graph/2 includes explicit :continuation links.
  • inspect_run_timeline/2 includes :run_continued_from and :run_continued_to events.
  • explain_run/2 identifies :continued terminal runs and points operators to the immediate successor.

Traverse more than one edge only through the bounded chain API:

{:ok, chain} =
  Jizoku.inspect_continuation_chain(successor_run_id,
    direction: :backward,
    max_hops: 25
  )

chain.runs
chain.hops
chain.truncated?
chain.warnings

Traversal follows continuation edges only. It never follows child or replay lineage and loads at most max_hops + 1 run projections.

Hosts can tune warnings and the default traversal limit:

config :jizoku, :continuation_history,
  run_warning_threshold: 5_000,
  run_critical_threshold: 20_000,
  chain_warning_hops: 25,
  max_chain_hops: 100

These thresholds classify durable thread size for operator tooling; they do not delete or compact history.

Activation And Rollout

Fence emission is disabled by default because old workers do not understand the new dispatch control facts. Roll out in two phases:

  1. Deploy a continuation-aware Jizoku release to every worker, recovery loop, and scheduler that can read the affected queues. Drain or replace older workers and prevent an older image from returning.

  2. Enable emission at the trusted host configuration boundary:

    config :jizoku, continuation_fences: :enabled

The flag is a host readiness assertion, not automatic cluster discovery. Do not expose it as a request option. After the first continuation fence is written, rollback may disable new emission, but workers must remain continuation-aware until every durable fence has been repaired or aborted.

The minimal host app enables the flag in development and test because those smoke paths run one coherent application version. Its production configuration keeps emission disabled; multi-node activation remains subject to the all-workers rollout barrier.