Skip to content

Commit e341e28

Browse files
os-zhuangclaude
andcommitted
feat(showcase): nested-pause example — approval inside a subflow
Adds the worked example for nested durable pause (linked runs, #1693): - showcase_closure_signoff: reusable approval subflow (manager sign-off, approve/reject branches recorded into its `decision` output) - showcase_project_closure: on project completion, invokes the sign-off subflow — the child suspends on its approval node, suspending the parent at the subflow node too; the decision bubbles back up and the owner is notified with the outcome Also gates showcase_budget_approval on the budget actually CHANGING (`budget != previous.budget`) — it previously fired on every update of a large-budget project, colliding with any other approval flow on the same record (approvals dedupe pending requests per record), which broke the new example and was wrong on its own terms. Verified live in the browser: complete a project → parent run paused at `signoff` (correlation subflow:<child>) + child paused at `ask_signoff` → approve via /api/v1/approvals → child completes down the approve edge and bubbles: parent resumes, notify_owner runs, both runs completed; the Studio Runs panel shows the same run transitioning paused → completed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent aa39b33 commit e341e28

1 file changed

Lines changed: 130 additions & 1 deletion

File tree

examples/app-showcase/src/flows/index.ts

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,11 @@ export const BudgetApprovalFlow = defineFlow({
161161
config: {
162162
objectName: 'showcase_project',
163163
triggerType: 'record-after-update',
164-
condition: 'budget > 100000',
164+
// Gate on the budget CHANGING, not on every update of a large-budget
165+
// project — otherwise any unrelated edit (status, health, …) re-opens
166+
// an approval and collides with other approval flows on the same
167+
// record (the approvals service dedupes pending requests per record).
168+
condition: 'budget > 100000 && budget != previous.budget',
165169
},
166170
},
167171
{
@@ -504,6 +508,129 @@ export const TaskDoneNotifyOwnerFlow = defineFlow({
504508
],
505509
});
506510

511+
/**
512+
* Closure Sign-off — a reusable **approval subflow**: pauses on a manager
513+
* approval and reports the decision as its output. Together with
514+
* {@link ProjectClosureFlow} this is the worked example of **nested durable
515+
* pause** (linked-runs model): a pausing node (`approval`) inside a `subflow`
516+
* suspends BOTH runs — the child at the approval, the parent at its subflow
517+
* node (`correlation: subflow:<childRunId>`) — and the eventual decision
518+
* bubbles back up through the chain.
519+
*/
520+
export const ClosureSignoffSubflow = defineFlow({
521+
name: 'showcase_closure_signoff',
522+
label: 'Closure Sign-off (approval subflow)',
523+
description: 'Reusable subflow: requests a manager sign-off and outputs the decision. Demonstrates approval inside a subflow (nested durable pause).',
524+
type: 'autolaunched',
525+
template: true,
526+
variables: [
527+
{ name: 'reason', type: 'text', isInput: true },
528+
{ name: 'decision', type: 'text', isOutput: true },
529+
],
530+
nodes: [
531+
{ id: 'start', type: 'start', label: 'Start' },
532+
{
533+
id: 'ask_signoff',
534+
type: 'approval',
535+
label: 'Manager Sign-off',
536+
config: {
537+
approvers: [{ type: 'role', value: 'manager' }],
538+
behavior: 'first_response',
539+
// The parent project just hit a terminal status — no point locking it.
540+
lockRecord: false,
541+
},
542+
},
543+
{
544+
id: 'mark_approved',
545+
type: 'assignment',
546+
label: 'Record Approval',
547+
config: { assignments: { decision: 'approved' } },
548+
},
549+
{
550+
id: 'mark_rejected',
551+
type: 'assignment',
552+
label: 'Record Rejection',
553+
config: { assignments: { decision: 'rejected' } },
554+
},
555+
{ id: 'end_ok', type: 'end', label: 'Signed Off' },
556+
{ id: 'end_no', type: 'end', label: 'Declined' },
557+
],
558+
edges: [
559+
{ id: 'e1', source: 'start', target: 'ask_signoff' },
560+
{ id: 'e2', source: 'ask_signoff', target: 'mark_approved', label: 'approve' },
561+
{ id: 'e3', source: 'ask_signoff', target: 'mark_rejected', label: 'reject' },
562+
{ id: 'e4', source: 'mark_approved', target: 'end_ok' },
563+
{ id: 'e5', source: 'mark_rejected', target: 'end_no' },
564+
],
565+
});
566+
567+
/**
568+
* Project Closure with Sign-off — the worked **nested durable pause** example.
569+
*
570+
* When a project is marked Completed, the flow invokes
571+
* {@link ClosureSignoffSubflow} through a `subflow` node. The child suspends on
572+
* its `approval` node, which suspends THIS run too — both continuations are
573+
* persisted as linked runs (`sys_automation_run`), surviving restarts. When a
574+
* manager decides (approvals API / inbox), the child resumes down the matching
575+
* branch, completes, and **bubbles** its `decision` output back into this run
576+
* (`signoffResult`), which continues to notify the project owner.
577+
*
578+
* Observe it end-to-end: complete a project → both runs show `paused` in the
579+
* Runs panel (parent at `signoff`, child at `ask_signoff`) → approve via
580+
* `POST /api/v1/approvals/requests/:id/approve` → both runs complete and the
581+
* owner's inbox gets the decision.
582+
*/
583+
export const ProjectClosureFlow = defineFlow({
584+
name: 'showcase_project_closure',
585+
label: 'Project Closure with Sign-off (nested pause)',
586+
description: 'On project completion, requests sign-off via an approval-inside-subflow, then notifies the owner — demonstrates nested durable pause.',
587+
type: 'autolaunched',
588+
nodes: [
589+
{
590+
id: 'start',
591+
type: 'start',
592+
label: 'On Project Completed',
593+
config: {
594+
objectName: 'showcase_project',
595+
triggerType: 'record-after-update',
596+
condition: 'status == "completed" && previous.status != "completed"',
597+
},
598+
},
599+
{
600+
id: 'signoff',
601+
type: 'subflow',
602+
label: 'Request Sign-off',
603+
config: {
604+
flowName: 'showcase_closure_signoff',
605+
input: {
606+
reason: 'Project "{record.name}" was marked completed — please sign off the closure.',
607+
},
608+
outputVariable: 'signoffResult',
609+
},
610+
},
611+
{
612+
id: 'notify_owner',
613+
type: 'notify',
614+
label: 'Notify Owner of Decision',
615+
config: {
616+
topic: 'project.closure',
617+
recipients: ['{record.owner}'],
618+
channels: ['inbox'],
619+
severity: 'info',
620+
title: 'Closure sign-off: {record.name}',
621+
message: 'Closure sign-off decision for "{record.name}": {signoffResult.decision}.',
622+
actionUrl: '/showcase_project/{record.id}',
623+
},
624+
},
625+
{ id: 'end', type: 'end', label: 'End' },
626+
],
627+
edges: [
628+
{ id: 'e1', source: 'start', target: 'signoff' },
629+
{ id: 'e2', source: 'signoff', target: 'notify_owner' },
630+
{ id: 'e3', source: 'notify_owner', target: 'end' },
631+
],
632+
});
633+
507634
/**
508635
* Batch Reminders — demonstrates the ADR-0031 **structured loop container**.
509636
*
@@ -716,6 +843,8 @@ export const allFlows = [
716843
TaskFollowUpFlow,
717844
NotifyOwnerSubflow,
718845
TaskDoneNotifyOwnerFlow,
846+
ClosureSignoffSubflow,
847+
ProjectClosureFlow,
719848
BatchRemindersFlow,
720849
FanOutNotifyFlow,
721850
ResilientSyncFlow,

0 commit comments

Comments
 (0)