Skip to content

Continue as new ​

Every workflow event is recorded in its history. A workflow that loops forever — a subscription poller, a long-lived state machine, a per-entity actor — grows its history without bound until Temporal refuses to continue.

continueAsNew ends the current run and atomically starts a fresh one with new arguments and an empty history.

The basic pattern ​

typescript
import { declareWorkflow, propagateFailure } from "@temporal-contract/worker/workflow";
import { sleep } from "@temporalio/workflow";

export const pollSubscription = declareWorkflow({
  workflowName: "pollSubscription",
  contract: billingContract,
  activityOptions: { startToCloseTimeout: "1 minute", retry: { maximumAttempts: 3 } },
  implementation: async (context, args) => {
    for (let i = 0; i < 100; i += 1) {
      await propagateFailure(
        context.activities.chargeSubscription({ subscriptionId: args.subscriptionId }),
      );
      await sleep("30 days");
    }

    // 100 cycles is enough history. Start over with a clean slate.
    return context.continueAsNew({
      subscriptionId: args.subscriptionId,
      cycle: args.cycle + 100,
    });
  },
});

continueAsNew never returns — its type is Promise<never>. Returning it is the idiomatic way to make that obvious and to satisfy the implementation's return type.

Arguments are validated against the destination workflow's input schema before Temporal is called. An invalid payload throws WorkflowInputValidationError rather than starting a broken run.

Decide when to roll over ​

Base the decision on something deterministic. context.info exposes Temporal's WorkflowInfo:

typescript
implementation: async (context, args) => {
  let processed = args.processed;
  let cursor = args.cursor;

  while (true) {
    const batch = await propagateFailure(context.activities.fetchBatch({ cursor }));
    if (batch.items.length === 0) {
      return { processed };
    }

    await propagateFailure(context.activities.processBatch({ items: batch.items }));
    processed += batch.items.length;
    cursor = batch.nextCursor; // advance, so the next fetch makes progress

    // Temporal's own signal that history is getting long.
    if (context.info.continueAsNewSuggested) {
      return context.continueAsNew({ cursor, processed });
    }
  }
};

continueAsNewSuggested is set by the server based on real history size. It is a better trigger than a hand-tuned iteration count.

Carry state forward ​

The new run starts with empty memory. Anything that must survive has to travel in the arguments — which means the contract's input schema has to accommodate it:

typescript
const pollSubscription = defineWorkflow({
  input: z.object({
    subscriptionId: z.string(),
    // Continuation state, defaulted so the first run can omit it.
    cycle: z.number().int().nonnegative().default(0),
    lastChargeId: z.string().optional(),
  }),
  output: z.object({ cycles: z.number() }),
  // `continueAsNew` keeps the SAME workflow ID for the whole polling chain,
  // and this mode never fires between one cycle and the next — not because
  // the previous run is still open (at the moment of a continuation it is
  // Closed, with status `CONTINUED_AS_NEW`), but because a continuation is
  // not a start request: the continue-as-new command carries no
  // `workflowIdReusePolicy` field at all, so the server never consults one.
  // This mode instead governs an external start under this
  // subscription's ID after the whole chain eventually ends (cancellation,
  // or the subscription itself closing) — e.g. a customer resubscribing —
  // which is the expected, harmless case here, so `allow-duplicate` is
  // fine. It does NOT guard against double-charging a cycle: that's
  // `chargeSubscription`'s job (an idempotency key derived from
  // `lastChargeId`/`cycle`), independent of this field.
  startPolicy: "allow-duplicate",
});

Keep it small. These arguments are serialized into the new run's history on every rollover.

Continue into a different workflow ​

The four-argument form takes a contract and a workflow name, so a run can hand off to a different workflow type — including one on another contract and task queue:

typescript
implementation: async (context, args) => {
  if (args.phase === "collection") {
    // Hand off to the dunning workflow on the collections contract.
    return context.continueAsNew(collectionsContract, "dunningProcess", {
      accountId: args.accountId,
      overdueSince: args.overdueSince,
    });
  }
  // ...
};

Arguments are validated against the destination workflow's schema.

Options ​

typescript
return context.continueAsNew(
  { subscriptionId: args.subscriptionId, cycle: args.cycle + 1 },
  {
    workflowRunTimeout: "7 days",
    workflowTaskTimeout: "10 seconds",
    memo: { tenant: args.tenantId },
  },
);

TypedContinueAsNewOptions is Temporal's ContinueAsNewOptions minus workflowType and taskQueue (derived from the contract, and ignored if you try to set them). There is no retry option — a continued run inherits the chain's retry policy; use activity retry policies for step-level retries.

Drain handlers first ​

Rolling over while a signal or update handler is mid-flight drops that work:

typescript
import { allHandlersFinished, condition } from "@temporalio/workflow";

if (context.info.continueAsNewSuggested) {
  await condition(() => allHandlersFinished());
  return context.continueAsNew({ cursor, processed });
}

What callers see ​

The workflow id stays the same; the run id changes. A client that holds a handle and awaits result() transparently follows the chain and receives the value returned by the final run.

Signals sent during the rollover window are delivered to the new run. Queries against a completed run see that run's final state — bind by workflow id rather than run id to always reach the current one.

Do not use it for ​

  • Retrying a failed step. That is what activity retry policies are for.
  • Splitting unrelated work. Use child workflows.
  • Short workflows. If history will not grow unbounded, the extra complexity buys nothing.

Next ​

Released under the MIT License.