[{"data":1,"prerenderedAt":770},["ShallowReactive",2],{"navigation":3,"\u002Fapi\u002Fworkflow-continuations":155,"\u002Fapi\u002Fworkflow-continuations-surround":765},[4,36,57,113,144],{"title":5,"icon":6,"path":7,"stem":8,"children":9,"page":35},"Getting Started","i-lucide-rocket","\u002Fgetting-started","1.getting-started",[10,15,20,25,30],{"title":11,"path":12,"stem":13,"icon":14},"Introduction","\u002Fgetting-started\u002Fintroduction","1.getting-started\u002F1.introduction","i-lucide-info",{"title":16,"path":17,"stem":18,"icon":19},"Install","\u002Fgetting-started\u002Finstall","1.getting-started\u002F2.install","i-lucide-download",{"title":21,"path":22,"stem":23,"icon":24},"Quickstart","\u002Fgetting-started\u002Fquickstart","1.getting-started\u002F3.quickstart","i-lucide-zap",{"title":26,"path":27,"stem":28,"icon":29},"Testing","\u002Fgetting-started\u002Ftesting","1.getting-started\u002F4.testing","i-lucide-flask-conical",{"title":31,"path":32,"stem":33,"icon":34},"CLI","\u002Fgetting-started\u002Fcli","1.getting-started\u002F5.cli","i-lucide-terminal",false,{"title":37,"icon":38,"path":39,"stem":40,"children":41,"page":35},"Concepts","i-lucide-boxes","\u002Fconcepts","2.concepts",[42,47,52],{"title":43,"path":44,"stem":45,"icon":46},"SQL-first contract","\u002Fconcepts\u002Fsql-first-contract","2.concepts\u002F1.sql-first-contract","i-lucide-database",{"title":48,"path":49,"stem":50,"icon":51},"Jobs and status","\u002Fconcepts\u002Fjobs-and-status","2.concepts\u002F2.jobs-and-status","i-lucide-list-checks",{"title":53,"path":54,"stem":55,"icon":56},"Fencing and settles","\u002Fconcepts\u002Ffencing-and-settles","2.concepts\u002F3.fencing-and-settles","i-lucide-shield",{"title":58,"icon":59,"path":60,"stem":61,"children":62,"page":35},"API","i-lucide-code-2","\u002Fapi","3.api",[63,68,73,78,83,88,93,98,103,108],{"title":64,"path":65,"stem":66,"icon":67},"Enqueue","\u002Fapi\u002Fenqueue","3.api\u002F1.enqueue","i-lucide-plus-circle",{"title":69,"path":70,"stem":71,"icon":72},"Trusted Effects","\u002Fapi\u002Ftrusted-effects","3.api\u002F10.trusted-effects","i-lucide-lock-keyhole",{"title":74,"path":75,"stem":76,"icon":77},"Claim and heartbeat","\u002Fapi\u002Fclaim-and-heartbeat","3.api\u002F2.claim-and-heartbeat","i-lucide-hand",{"title":79,"path":80,"stem":81,"icon":82},"Worker and handlers","\u002Fapi\u002Fworker-and-handlers","3.api\u002F3.worker-and-handlers","i-lucide-cpu",{"title":84,"path":85,"stem":86,"icon":87},"HTTP facade","\u002Fapi\u002Fhttp-facade","3.api\u002F4.http-facade","i-lucide-globe",{"title":89,"path":90,"stem":91,"icon":92},"Authorization","\u002Fapi\u002Fauthorization","3.api\u002F5.authorization","i-lucide-key-round",{"title":94,"path":95,"stem":96,"icon":97},"Follow-ups","\u002Fapi\u002Ffollow-ups","3.api\u002F6.follow-ups","i-lucide-git-branch",{"title":99,"path":100,"stem":101,"icon":102},"Workflows and schedules","\u002Fapi\u002Fworkflows-and-schedules","3.api\u002F7.workflows-and-schedules","i-lucide-workflow",{"title":104,"path":105,"stem":106,"icon":107},"Durable admission","\u002Fapi\u002Fdurable-admission","3.api\u002F8.durable-admission","i-lucide-ticket",{"title":109,"path":110,"stem":111,"icon":112},"Workflow Continuations","\u002Fapi\u002Fworkflow-continuations","3.api\u002F9.workflow-continuations","i-lucide-git-fork",{"title":114,"icon":115,"path":116,"stem":117,"children":118,"page":35},"Operations","i-lucide-activity","\u002Foperations","4.operations",[119,124,129,134,139],{"title":120,"path":121,"stem":122,"icon":123},"Standalone Scheduler","\u002Foperations\u002Fstandalone-scheduler","4.operations\u002F0.standalone-scheduler","i-lucide-calendar-clock",{"title":125,"path":126,"stem":127,"icon":128},"Housekeeping","\u002Foperations\u002Fhousekeeping","4.operations\u002F1.housekeeping","i-lucide-timer",{"title":130,"path":131,"stem":132,"icon":133},"Redrive and dead letters","\u002Foperations\u002Fredrive-and-dlq","4.operations\u002F2.redrive-and-dlq","i-lucide-rotate-ccw",{"title":135,"path":136,"stem":137,"icon":138},"Flow control","\u002Foperations\u002Fflow-control","4.operations\u002F3.flow-control","i-lucide-gauge",{"title":140,"path":141,"stem":142,"icon":143},"Queue Boundaries","\u002Foperations\u002Fqueue-boundaries","4.operations\u002F4.queue-boundaries","i-lucide-panels-top-left",{"title":145,"icon":146,"path":147,"stem":148,"children":149,"page":35},"Reference","i-lucide-book-marked","\u002Freference","5.reference",[150],{"title":151,"path":152,"stem":153,"icon":154},"Configuration","\u002Freference\u002Fconfiguration","5.reference\u002F1.configuration","i-lucide-settings",{"id":156,"title":109,"body":157,"description":758,"extension":759,"links":760,"meta":761,"navigation":762,"path":110,"seo":763,"stem":111,"__hash__":764},"docs\u002F3.api\u002F9.workflow-continuations.md",{"type":158,"value":159,"toc":750},"minimark",[160,169,197,202,205,475,482,486,578,584,588,591,659,666,669,673,713,719,723,726,730,746],[161,162,163,164,168],"p",{},"Ordinary ",[165,166,167],"a",{"href":95},"follow-ups"," create detached child jobs. A workflow\ncontinuation creates the child as a member of the parent’s already sealed\nworkflow, without reopening the graph to arbitrary mutation.",[161,170,171,172,176,177,180,181,184,185,188,189,192,193,196],{},"This surface first shipped in ",[173,174,175],"code",{},"0.1.0a25"," and remains in ",[173,178,179],{},"0.1.0a36",", Protocol\ndocument revision ",[173,182,183],{},"1.0.17",", and SQL contract ",[173,186,187],{},"0.2.5+"," (migrations\n",[173,190,191],{},"0016","–",[173,194,195],{},"0017",").",[198,199,201],"h2",{"id":200},"_1-declare-member-edges","1. Declare member edges",[161,203,204],{},"Mark the parent’s allowed target and the emitted follow-up as workflow members:",[206,207,213],"pre",{"className":208,"code":209,"filename":210,"language":211,"meta":212,"style":212},"language-python shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","from taskq import Complete, Followup, FollowupTarget, Task, TaskRegistry\n\n\nasync def discover(payload: DiscoverInput) -> Complete:\n    return Complete(\n        result={\"found\": True},\n        followups=(\n            Followup(\n                step=\"enrich\",\n                queue=\"enrichment\",\n                job_type=\"listing.enrich\",\n                payload={\"listing_id\": payload.listing_id},\n                workflow_member=True,\n            ),\n        ),\n    )\n\n\nENRICH = Task(\n    name=\"listing.enrich\",\n    queue=\"enrichment\",\n    input_model=EnrichInput,\n    output_model=EnrichOutput,\n    handler=enrich,\n)\n\nDISCOVER = Task(\n    name=\"listing.discover\",\n    queue=\"discovery\",\n    input_model=DiscoverInput,\n    output_model=DiscoverOutput,\n    followup_targets=(\n        FollowupTarget(\n            queue=ENRICH.queue,\n            job_type=ENRICH.name,\n            workflow_member=True,\n            continuation_revision=\"1\",\n        ),\n    ),\n    handler=discover,\n)\n\nregistry = TaskRegistry((DISCOVER, ENRICH))\npolicy = registry.compile_continuation_policy((DISCOVER,))\n","tasks.py","python","",[173,214,215,223,230,235,241,247,253,259,265,271,277,283,289,295,301,307,313,318,323,329,335,341,347,353,359,365,370,376,382,388,394,400,406,412,418,424,430,436,441,447,453,458,463,469],{"__ignoreMap":212},[216,217,220],"span",{"class":218,"line":219},"line",1,[216,221,222],{},"from taskq import Complete, Followup, FollowupTarget, Task, TaskRegistry\n",[216,224,226],{"class":218,"line":225},2,[216,227,229],{"emptyLinePlaceholder":228},true,"\n",[216,231,233],{"class":218,"line":232},3,[216,234,229],{"emptyLinePlaceholder":228},[216,236,238],{"class":218,"line":237},4,[216,239,240],{},"async def discover(payload: DiscoverInput) -> Complete:\n",[216,242,244],{"class":218,"line":243},5,[216,245,246],{},"    return Complete(\n",[216,248,250],{"class":218,"line":249},6,[216,251,252],{},"        result={\"found\": True},\n",[216,254,256],{"class":218,"line":255},7,[216,257,258],{},"        followups=(\n",[216,260,262],{"class":218,"line":261},8,[216,263,264],{},"            Followup(\n",[216,266,268],{"class":218,"line":267},9,[216,269,270],{},"                step=\"enrich\",\n",[216,272,274],{"class":218,"line":273},10,[216,275,276],{},"                queue=\"enrichment\",\n",[216,278,280],{"class":218,"line":279},11,[216,281,282],{},"                job_type=\"listing.enrich\",\n",[216,284,286],{"class":218,"line":285},12,[216,287,288],{},"                payload={\"listing_id\": payload.listing_id},\n",[216,290,292],{"class":218,"line":291},13,[216,293,294],{},"                workflow_member=True,\n",[216,296,298],{"class":218,"line":297},14,[216,299,300],{},"            ),\n",[216,302,304],{"class":218,"line":303},15,[216,305,306],{},"        ),\n",[216,308,310],{"class":218,"line":309},16,[216,311,312],{},"    )\n",[216,314,316],{"class":218,"line":315},17,[216,317,229],{"emptyLinePlaceholder":228},[216,319,321],{"class":218,"line":320},18,[216,322,229],{"emptyLinePlaceholder":228},[216,324,326],{"class":218,"line":325},19,[216,327,328],{},"ENRICH = Task(\n",[216,330,332],{"class":218,"line":331},20,[216,333,334],{},"    name=\"listing.enrich\",\n",[216,336,338],{"class":218,"line":337},21,[216,339,340],{},"    queue=\"enrichment\",\n",[216,342,344],{"class":218,"line":343},22,[216,345,346],{},"    input_model=EnrichInput,\n",[216,348,350],{"class":218,"line":349},23,[216,351,352],{},"    output_model=EnrichOutput,\n",[216,354,356],{"class":218,"line":355},24,[216,357,358],{},"    handler=enrich,\n",[216,360,362],{"class":218,"line":361},25,[216,363,364],{},")\n",[216,366,368],{"class":218,"line":367},26,[216,369,229],{"emptyLinePlaceholder":228},[216,371,373],{"class":218,"line":372},27,[216,374,375],{},"DISCOVER = Task(\n",[216,377,379],{"class":218,"line":378},28,[216,380,381],{},"    name=\"listing.discover\",\n",[216,383,385],{"class":218,"line":384},29,[216,386,387],{},"    queue=\"discovery\",\n",[216,389,391],{"class":218,"line":390},30,[216,392,393],{},"    input_model=DiscoverInput,\n",[216,395,397],{"class":218,"line":396},31,[216,398,399],{},"    output_model=DiscoverOutput,\n",[216,401,403],{"class":218,"line":402},32,[216,404,405],{},"    followup_targets=(\n",[216,407,409],{"class":218,"line":408},33,[216,410,411],{},"        FollowupTarget(\n",[216,413,415],{"class":218,"line":414},34,[216,416,417],{},"            queue=ENRICH.queue,\n",[216,419,421],{"class":218,"line":420},35,[216,422,423],{},"            job_type=ENRICH.name,\n",[216,425,427],{"class":218,"line":426},36,[216,428,429],{},"            workflow_member=True,\n",[216,431,433],{"class":218,"line":432},37,[216,434,435],{},"            continuation_revision=\"1\",\n",[216,437,439],{"class":218,"line":438},38,[216,440,306],{},[216,442,444],{"class":218,"line":443},39,[216,445,446],{},"    ),\n",[216,448,450],{"class":218,"line":449},40,[216,451,452],{},"    handler=discover,\n",[216,454,456],{"class":218,"line":455},41,[216,457,364],{},[216,459,461],{"class":218,"line":460},42,[216,462,229],{"emptyLinePlaceholder":228},[216,464,466],{"class":218,"line":465},43,[216,467,468],{},"registry = TaskRegistry((DISCOVER, ENRICH))\n",[216,470,472],{"class":218,"line":471},44,[216,473,474],{},"policy = registry.compile_continuation_policy((DISCOVER,))\n",[161,476,477,478,481],{},"The policy compiler validates the reachable member graph, canonicalizes it,\nand produces a SHA-256 ",[173,479,480],{},"continuation_policy_hash",". Change the continuation\nrevision when the allowed durable edge set changes.",[198,483,485],{"id":484},"_2-bind-the-workflow-to-the-policy","2. Bind the workflow to the policy",[206,487,490],{"className":208,"code":488,"filename":489,"language":211,"meta":212,"style":212},"from taskq import WorkflowKind\n\nworkflow = await tq.create_workflow(\n    \"listing-import:batch-42\",\n    WorkflowKind.DAG,\n    declared_queues=policy.reachable_queues,\n    actor=\"import-api\",\n    member_limit=10_000,\n    continuation_policy_hash=policy.continuation_policy_hash,\n)\n\nawait tq.enqueue(\n    DISCOVER,\n    {\"listing_id\": \"listing-123\"},\n    workflow_id=workflow.workflow_id,\n    step_key=\"root\",\n)\nawait tq.seal_workflow(workflow.workflow_id, actor=\"import-api\")\n","producer.py",[173,491,492,497,501,506,511,516,521,526,531,536,540,544,549,554,559,564,569,573],{"__ignoreMap":212},[216,493,494],{"class":218,"line":219},[216,495,496],{},"from taskq import WorkflowKind\n",[216,498,499],{"class":218,"line":225},[216,500,229],{"emptyLinePlaceholder":228},[216,502,503],{"class":218,"line":232},[216,504,505],{},"workflow = await tq.create_workflow(\n",[216,507,508],{"class":218,"line":237},[216,509,510],{},"    \"listing-import:batch-42\",\n",[216,512,513],{"class":218,"line":243},[216,514,515],{},"    WorkflowKind.DAG,\n",[216,517,518],{"class":218,"line":249},[216,519,520],{},"    declared_queues=policy.reachable_queues,\n",[216,522,523],{"class":218,"line":255},[216,524,525],{},"    actor=\"import-api\",\n",[216,527,528],{"class":218,"line":261},[216,529,530],{},"    member_limit=10_000,\n",[216,532,533],{"class":218,"line":267},[216,534,535],{},"    continuation_policy_hash=policy.continuation_policy_hash,\n",[216,537,538],{"class":218,"line":273},[216,539,364],{},[216,541,542],{"class":218,"line":279},[216,543,229],{"emptyLinePlaceholder":228},[216,545,546],{"class":218,"line":285},[216,547,548],{},"await tq.enqueue(\n",[216,550,551],{"class":218,"line":291},[216,552,553],{},"    DISCOVER,\n",[216,555,556],{"class":218,"line":297},[216,557,558],{},"    {\"listing_id\": \"listing-123\"},\n",[216,560,561],{"class":218,"line":303},[216,562,563],{},"    workflow_id=workflow.workflow_id,\n",[216,565,566],{"class":218,"line":309},[216,567,568],{},"    step_key=\"root\",\n",[216,570,571],{"class":218,"line":315},[216,572,364],{},[216,574,575],{"class":218,"line":320},[216,576,577],{},"await tq.seal_workflow(workflow.workflow_id, actor=\"import-api\")\n",[161,579,580,583],{},[173,581,582],{},"member_limit"," is a durable cap for the entire workflow. The workflow records\nthe policy hash at creation; a producer cannot attach one later.",[198,585,587],{"id":586},"_3-run-policy-aware-workers","3. Run policy-aware workers",[161,589,590],{},"Workers must advertise and retain every policy hash they can execute:",[206,592,595],{"className":208,"code":593,"filename":594,"language":211,"meta":212,"style":212},"from taskq import WorkerService, WorkerServiceOptions\n\nservice = WorkerService(\n    runner_transport,\n    registry,\n    worker_id=\"worker-1\",\n    options=WorkerServiceOptions(\n        queues=policy.reachable_queues,\n        listen=False,\n    ),\n    continuation_policies=(policy,),\n)\nawait service.run()\n","worker.py",[173,596,597,602,606,611,616,621,626,631,636,641,645,650,654],{"__ignoreMap":212},[216,598,599],{"class":218,"line":219},[216,600,601],{},"from taskq import WorkerService, WorkerServiceOptions\n",[216,603,604],{"class":218,"line":225},[216,605,229],{"emptyLinePlaceholder":228},[216,607,608],{"class":218,"line":232},[216,609,610],{},"service = WorkerService(\n",[216,612,613],{"class":218,"line":237},[216,614,615],{},"    runner_transport,\n",[216,617,618],{"class":218,"line":243},[216,619,620],{},"    registry,\n",[216,622,623],{"class":218,"line":249},[216,624,625],{},"    worker_id=\"worker-1\",\n",[216,627,628],{"class":218,"line":255},[216,629,630],{},"    options=WorkerServiceOptions(\n",[216,632,633],{"class":218,"line":261},[216,634,635],{},"        queues=policy.reachable_queues,\n",[216,637,638],{"class":218,"line":267},[216,639,640],{},"        listen=False,\n",[216,642,643],{"class":218,"line":273},[216,644,446],{},[216,646,647],{"class":218,"line":279},[216,648,649],{},"    continuation_policies=(policy,),\n",[216,651,652],{"class":218,"line":285},[216,653,364],{},[216,655,656],{"class":218,"line":291},[216,657,658],{},"await service.run()\n",[161,660,661,662,665],{},"The stock ",[173,663,664],{},"taskq worker run"," CLI loads a registry but does not currently accept\ncompiled continuation policies. Use a small programmatic worker entry point\nlike the one above for continuation-enabled queues.",[161,667,668],{},"At claim time the worker advertises supported hashes. It will not claim a\nworkflow member pinned to an unknown policy. If an unsupported claim reaches a\nworker through a custom transport, the worker releases it without invoking the\nhandler and begins a soft stop.",[198,670,672],{"id":671},"runtime-guarantees","Runtime guarantees",[674,675,676,680,683,698,704,710],"ul",{},[677,678,679],"li",{},"The claimed policy—not the current live registry—is the authority for that\nworkflow’s member edges.",[677,681,682],{},"Member settlement and child insertion are atomic.",[677,684,685,686,689,690,693,694,697],{},"TaskQ derives the child ",[173,687,688],{},"step_key"," and an idempotency key in the reserved\n",[173,691,692],{},"chain:"," namespace from the parent job id plus ",[173,695,696],{},"Followup.step",".",[677,699,700,701,703],{},"Producer-supplied idempotency keys beginning with ",[173,702,692],{}," are rejected.",[677,705,706,707,697],{},"A detached follow-up may still be emitted from the same handler by omitting\n",[173,708,709],{},"workflow_member=True",[677,711,712],{},"A workflow cannot exceed its durable member limit.",[714,715,716],"warning",{},[161,717,718],{},"Retain older compiled policies while any workflow pinned to them can still run.\nA deploy that removes a live policy hash strands those members until a\ncompatible worker returns.",[198,720,722],{"id":721},"when-to-use-it","When to use it",[161,724,725],{},"Use continuations when the next workflow member is discovered only while a\ncurrent member executes. Use an ordinary sealed DAG when all members and edges\nare known before sealing. Use detached follow-ups when the child should not\naffect workflow completion.",[198,727,729],{"id":728},"related","Related",[674,731,732,736,741],{},[677,733,734],{},[165,735,94],{"href":95},[677,737,738],{},[165,739,740],{"href":100},"Workflows & Schedules",[677,742,743],{},[165,744,745],{"href":54},"Fencing & Settles",[747,748,749],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":212,"searchDepth":219,"depth":225,"links":751},[752,753,754,755,756,757],{"id":200,"depth":225,"text":201},{"id":484,"depth":225,"text":485},{"id":586,"depth":225,"text":587},{"id":671,"depth":225,"text":672},{"id":721,"depth":225,"text":722},{"id":728,"depth":225,"text":729},"Grow sealed workflows through compiled, hash-pinned member policies.","md",null,{},{"icon":112},{"title":109,"description":758},"ePr36uLijKaljFn3Q-pnJm8ijJZa-khTfByyc3MsIF0",[766,768],{"title":104,"path":105,"stem":106,"description":767,"icon":107,"children":-1},"Reserve \u002F finish \u002F cancel two-phase admission for producers that must not lose intent.",{"title":120,"path":121,"stem":122,"description":769,"icon":123,"children":-1},"Bind the database target, apply paused manifests, and run one safe scheduler clock.",1787515776866]