Skip to content

Commit f7912a6

Browse files
authored
Merge pull request #1695 from objectstack-ai/fix-showcase-task-sync-fields
fix(showcase): flow example fixes + nested-pause (approval-in-subflow) example
2 parents 2bf40bf + e341e28 commit f7912a6

2 files changed

Lines changed: 141 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,

examples/app-showcase/src/objects/task.object.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,17 @@ export const Task = ObjectSchema.create({
6868
cover: Field.image({ label: 'Cover Image' }),
6969
labels: { type: 'tags', label: 'Labels' },
7070
notes: Field.textarea({ label: 'Notes' }),
71+
// Written by the Resilient Sync flow's try/catch region
72+
// (showcase_resilient_sync) when the outbound push exhausts its retries —
73+
// the catch branch flags the failure here for operator follow-up.
74+
sync_status: Field.select({
75+
label: 'Sync Status',
76+
options: [
77+
{ label: 'Synced', value: 'synced', color: '#10B981' },
78+
{ label: 'Failed', value: 'failed', color: '#EF4444' },
79+
],
80+
}),
81+
sync_error: Field.textarea({ label: 'Sync Error' }),
7182
},
7283

7384
validations: [

0 commit comments

Comments
 (0)