Continue-As-New and Child Workflows

On this page
Open the guided lab ↗

A million steps

Yesterday's AgentRun loops forever, and every step costs history. A completed Activity alone is three Events — ActivityTaskScheduled, ActivityTaskStarted, ActivityTaskCompleted — and the Workflow Task that reacts to it adds its own, so a step with one model call and one tool call is well over ten Events. History is not free to grow, and the Service tells the Workflow when it has grown enough: workflow.info().is_continue_as_new_suggested() is the trigger. Upstream: "Temporal will tell your Workflow when it's approaching performance or scalability problems. Find out if it's time by checking Continue-As-New Suggested in your Workflow at spots in your implementation where you are ready to checkpoint your state." Check it where the loop already turns, not in the middle of a step, and never hard-code a step count in its place.

The numbers behind the suggestion are context, not the rule, and they move. As of 2026-09 upstream states the history limit as a precaution: 51,200 Events or 50 MB, with a warning after 10,240 Events or 10 MB (see the upstream page); and a second, quieter limit of 2,000 incomplete Activities, Child Workflows, Signals or cancellation requests per execution, past which the command that would start one more fails the Workflow Task, which retries — the Workflow stalls rather than dies. Do not architect around 51,200. An agent step can produce many Events — a model call, a tool call, a Timer, a Signal or two, and the Workflow Tasks between them — so the number of steps one run can hold is not a constant you can know in advance, and it changes when you add a step. Either ask is_continue_as_new_suggested() or set a deliberately lower application threshold (every N steps, with N well below what the limit would allow), and check it at the same point in the loop. Your code should not know the limit; it should ask, or stay far from it.

Two tools today: bound the history, and split the work.

Continue-As-New

"Continue-As-New allows you to checkpoint your Workflow's state and start a fresh Workflow." You pass your latest relevant state as the arguments of a new Workflow Execution; "the new Workflow Execution has the same Workflow Id, but a different Run Id, and starts its own Event History." The runs form a chain, and from any client's point of view the Workflow ID is the stable handle for the entity's whole life.

Swipe to explore diagram →
Workflow ID agent-42 — one handle, whole life continue_as_new(AgentState) continue_as_new(AgentState) Run ID 1 Run ID 2 Run ID 3 fresh history: fresh history: fresh history: events 1..N events 1..N events 1..N closed closed open Signals in flight buffered → next run trigger: workflow.info().is_continue_as_new_suggested()
One Workflow ID, a chain of runs: each continue-as-new starts a fresh history and carries only the state you pass.

The state you pass is the checkpoint. Temporal does not snapshot your Python object: it does not serialize self, the local variables of run, or the coroutine that is awaiting something. You decide what the next run needs, put it in the arguments, and the new run receives exactly that — same Workflow ID, new Run ID, new Event History, and the state you explicitly carried forward. Nothing from the old run carries over on its own: no local variables, no pending Activities, no Timers. Upstream calls Workflows that do this indefinitely "Entity Workflows because they represent durable objects, not just processes."

Upstream gives a second reason to continue periodically even when the suggestion has not fired: a run that started on old code and keeps running under new code hits versioning problems, and a fresh run starts cleanly on the current deployment (module 9).

The agent pattern

Agent ID = Workflow ID. Process N interactions, snapshot the state that matters — not the raw transcript — and continue:

from dataclasses import dataclass, field
from temporalio import workflow

@dataclass
class AgentState:
    step: int
    goal: str
    context_summary: str = ""          # a summary, not the transcript
    attempts: int = 0
    processed_command_ids: list[str] = field(default_factory=list)  # sorted for the checkpoint payload

@dataclass
class AgentInput:
    goal: str
    state: AgentState | None = None    # None for the original caller

@workflow.defn
class AgentRun:
    @workflow.init
    def __init__(self, input: AgentInput) -> None:
        self.state = input.state or AgentState(step=0, goal=input.goal)

    @workflow.run
    async def run(self, input: AgentInput) -> str:
        while not self.done():
            await self.next_step()
            self.state.step += 1
            if workflow.info().is_continue_as_new_suggested():
                self.state.context_summary = await self.summarize_context()
                await workflow.wait_condition(workflow.all_handlers_finished)
                workflow.continue_as_new(AgentInput(goal=self.state.goal, state=self.state))
        return self.state.context_summary

The lab keeps processed_command_ids as a sorted list in the checkpoint payload: when handling a command, convert it to a set for membership checks, then write it back with sorted(...) before continuing-as-new. This preserves the dedupe set from module 5 without letting Python's process-dependent set iteration order change the serialized payload when you diff histories across runs.

continue_as_new "stops the Workflow right away and starts a new one"; the line after it never runs. Three rules around that line:

Drain handlers first. "If you use Updates or Signals, don't call Continue-as-New from the handlers. Instead, wait for your handlers to finish in your main Workflow before you run continue_as_new." An async handler mid-Activity when the run ends is cut off, and its caller gets NOT_FOUND. This is module 5's interleaving scenario: two change_goal Updates and a pause Signal arrive together; the first Update's handler is awaiting an Activity, the second is waiting on the lock, the main loop sees the pause, reaches its checkpoint and continues-as-new. Without the wait, both handlers die with the run, their callers get an error, and the new run's processed_command_ids does not contain their ids — so a client that retries them is not deduplicated either. The all_handlers_finished wait above is the guard, and it belongs in the main method, never in a handler.

Carry your own queue. Signals sent during the transition are safe: "Signals sent during the transition are not lost; the Temporal Service buffers them for the new execution." But a Signal your handler already accepted into an in-memory list that the main loop has not processed yet is your state, and it goes into the snapshot like everything else — upstream's loyalty-points guide carries pending_events in exactly this way. So does processed_command_ids: leave it behind and the first retried Signal after the new run starts is an undetected duplicate (module 5). Keep the payload well under the 2 MB limit (as of 2026-09; see upstream guide).

Children do not carry over. "Child Workflows do not carry over when the Parent uses Continue-As-New." A child still running when the parent continues is handled by its Parent Close Policy (below), so either await it before continuing or set the policy deliberately.

Update IDs are deduplicated per run, so dedupe re-sent Updates across the chain yourself (module 5).

Activity or Child Workflow?

This is the question that decides agent architecture. An Activity is "a single operation on the external world" with one recorded result. A Child Workflow "is a Workflow Execution that is spawned from within another Workflow in the same Namespace" — it has its own history, its own steps, Timers, Signals, retries and state, and it shows up on its own in the UI.

Activity Child Workflow
Models one external effect or computation a composite of Activities and other children
Code constraints none — may be non-deterministic full Workflow API, full determinism rules
What history records input, output, retry attempts every state change, in the child's own history
Visible on its own? as a pending Activity of the parent as its own execution with its own history
Can receive Signals? no (heartbeats can receive Cancellation) yes
On parent cancel always cancelled governed by Parent Close Policy; can outlive the parent with ABANDON
Retry unit the whole Activity each Activity inside it
Cost in the parent 3 Events 3 Events, plus the child's own history elsewhere

call_llm → Activity. ResearchAgent → Child Workflow. A model call is one effect with one answer; retrying it is retrying the whole thing, which is correct. A research task — thirty sources, a summary per source, a human review, a merge — is a lifecycle. As one Activity it has no progress visibility, one giant timeout, and all-or-nothing retry: source 29 fails and you re-fetch 28. As a child it has a history you can open, a step you can retry, and a Signal you can send.

Upstream's advice: "There is no reason to use Child Workflows just for code organization ... When in doubt, use an Activity." A child costs more Events overall than the Activities inside it would in the parent, so reach for one when the delegated work has a lifecycle someone would want to inspect on its own, when it must be partitioned to stay under history limits (a parent should not spawn more than 1,000 children), or when it should run on a separate set of Workers as its own service.

Child mechanics

from temporalio import workflow
from temporalio.exceptions import ChildWorkflowError
from temporalio.workflow import ParentClosePolicy

try:
    report = await workflow.execute_child_workflow(
        ResearchAgent.run, task,
        id=f"{workflow.info().workflow_id}/research/{n}",   # derive from the parent's for traceability
        parent_close_policy=ParentClosePolicy.REQUEST_CANCEL,
    )
except ChildWorkflowError as e:
    self.state.attempts += 1
    report = None                                          # decide, as with an ActivityError

execute_child_workflow is start_child_workflow plus await handle. Use the two-step form when you want the child's handle first — to Signal it, or to run several children in parallel. Awaiting either call resolves only once ChildWorkflowExecutionStarted is in the parent's history, so the child is guaranteed to exist when the call returns. The parent's history records StartChildWorkflowExecutionInitiated, ChildWorkflowExecutionStarted and ChildWorkflowExecutionCompleted (or ...Failed); a failure reaches the parent as ChildWorkflowError with the original cause inside. The child's own steps live in the child's history, not the parent's — which is the point.

Parent and child share no local state; they communicate only through arguments, the result, and Signals. If the child continues-as-new, the parent sees the whole chain as one execution.

Parent Close Policy

"A Parent Close Policy determines what happens to a Child Workflow Execution if its Parent changes to a Closed status (Completed, Failed, or Timed out)." There are three values:

Each child may have its own policy, set per call with parent_close_policy=. The default bites when the parent ends without awaiting a child — including by continue-as-new — so a child meant to keep running needs ABANDON explicitly. A deployment monitor that should watch for a week after the agent that started it has finished is the clear case.

Ask a second question of such a child, though: does the parent ever need its result? If not, it is not a child at all but a sibling — start it as a top-level Workflow from an Activity using the Client, and let it own its own lifecycle. ABANDON is the right tool when the parent wants the child's result and wants it to survive the parent's failure.

Six delegations, sorted

Task Choice Why
Fetch a URL Activity one effect, one result
Summarize a document with the model Activity one call; retry the whole thing
Run a test suite in a container Activity with heartbeats long, but one result; progress via heartbeat details
Research a company across 30 sources with human review Child Workflow multi-step, Signals in, inspectable on its own
Monitor a deployment for a week Child (ABANDON) or sibling outlives the parent; needs its own history
Send a notification Activity one effect; make it idempotent

The test to apply: does it have a lifecycle someone would want to inspect on its own? If not, it is an Activity.

When the child belongs to another team's service and Namespace, a Child Workflow is the wrong boundary. Appendix A2 covers Nexus, the tool for that case.

What the lab will test

List the state a new Run needs to continue the agent correctly. In Lab 6, compare that list with the Continue-As-New snapshot, then trace the separate Child Workflow history.

Try it

Lab 6 is in labs/06-continue-as-new-and-child-workflows/:

Sources and license

This page adapts material from Temporal's MIT-licensed documentation and samples (© Temporal Technologies Inc.; © Uber Technologies, Inc.). Adapted text is rewritten for this course; the upstream pages are the reference of record and may have changed since the commit linked here. Temporal is a trademark of Temporal Technologies; this course is independent and not endorsed by Temporal.