@@ -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