From aa39b333e2f9deaa7bd2efd055e5fbf6e2e9ca20 Mon Sep 17 00:00:00 2001 From: os-zhuang Date: Wed, 10 Jun 2026 21:11:35 +0500 Subject: [PATCH 1/2] fix(showcase): add sync_status/sync_error fields the resilient-sync flow writes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The showcase_resilient_sync flow's try/catch demo flags push failures by writing task.sync_status / task.sync_error from its catch region — but the showcase_task object never declared those fields, so the catch itself failed with "no such column: sync_status" and the whole run failed instead of demonstrating structured error handling. Found by step-by-step browser E2E of every showcase flow; with the fields in place the flow now completes: push retries exhaust → catch flags the task → run completed with the catch step in the run log. Co-Authored-By: Claude Opus 4.8 --- examples/app-showcase/src/objects/task.object.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/examples/app-showcase/src/objects/task.object.ts b/examples/app-showcase/src/objects/task.object.ts index 85d12264f9..b486b97617 100644 --- a/examples/app-showcase/src/objects/task.object.ts +++ b/examples/app-showcase/src/objects/task.object.ts @@ -68,6 +68,17 @@ export const Task = ObjectSchema.create({ cover: Field.image({ label: 'Cover Image' }), labels: { type: 'tags', label: 'Labels' }, notes: Field.textarea({ label: 'Notes' }), + // Written by the Resilient Sync flow's try/catch region + // (showcase_resilient_sync) when the outbound push exhausts its retries — + // the catch branch flags the failure here for operator follow-up. + sync_status: Field.select({ + label: 'Sync Status', + options: [ + { label: 'Synced', value: 'synced', color: '#10B981' }, + { label: 'Failed', value: 'failed', color: '#EF4444' }, + ], + }), + sync_error: Field.textarea({ label: 'Sync Error' }), }, validations: [ From e341e28a5c0663d74d9f974e152ed9177dfc1c6c Mon Sep 17 00:00:00 2001 From: os-zhuang Date: Wed, 10 Jun 2026 21:27:18 +0500 Subject: [PATCH 2/2] =?UTF-8?q?feat(showcase):=20nested-pause=20example=20?= =?UTF-8?q?=E2=80=94=20approval=20inside=20a=20subflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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 --- examples/app-showcase/src/flows/index.ts | 131 ++++++++++++++++++++++- 1 file changed, 130 insertions(+), 1 deletion(-) diff --git a/examples/app-showcase/src/flows/index.ts b/examples/app-showcase/src/flows/index.ts index 18d9db2e51..d583c7552e 100644 --- a/examples/app-showcase/src/flows/index.ts +++ b/examples/app-showcase/src/flows/index.ts @@ -161,7 +161,11 @@ export const BudgetApprovalFlow = defineFlow({ config: { objectName: 'showcase_project', triggerType: 'record-after-update', - condition: 'budget > 100000', + // Gate on the budget CHANGING, not on every update of a large-budget + // project — otherwise any unrelated edit (status, health, …) re-opens + // an approval and collides with other approval flows on the same + // record (the approvals service dedupes pending requests per record). + condition: 'budget > 100000 && budget != previous.budget', }, }, { @@ -504,6 +508,129 @@ export const TaskDoneNotifyOwnerFlow = defineFlow({ ], }); +/** + * Closure Sign-off — a reusable **approval subflow**: pauses on a manager + * approval and reports the decision as its output. Together with + * {@link ProjectClosureFlow} this is the worked example of **nested durable + * pause** (linked-runs model): a pausing node (`approval`) inside a `subflow` + * suspends BOTH runs — the child at the approval, the parent at its subflow + * node (`correlation: subflow:`) — and the eventual decision + * bubbles back up through the chain. + */ +export const ClosureSignoffSubflow = defineFlow({ + name: 'showcase_closure_signoff', + label: 'Closure Sign-off (approval subflow)', + description: 'Reusable subflow: requests a manager sign-off and outputs the decision. Demonstrates approval inside a subflow (nested durable pause).', + type: 'autolaunched', + template: true, + variables: [ + { name: 'reason', type: 'text', isInput: true }, + { name: 'decision', type: 'text', isOutput: true }, + ], + nodes: [ + { id: 'start', type: 'start', label: 'Start' }, + { + id: 'ask_signoff', + type: 'approval', + label: 'Manager Sign-off', + config: { + approvers: [{ type: 'role', value: 'manager' }], + behavior: 'first_response', + // The parent project just hit a terminal status — no point locking it. + lockRecord: false, + }, + }, + { + id: 'mark_approved', + type: 'assignment', + label: 'Record Approval', + config: { assignments: { decision: 'approved' } }, + }, + { + id: 'mark_rejected', + type: 'assignment', + label: 'Record Rejection', + config: { assignments: { decision: 'rejected' } }, + }, + { id: 'end_ok', type: 'end', label: 'Signed Off' }, + { id: 'end_no', type: 'end', label: 'Declined' }, + ], + edges: [ + { id: 'e1', source: 'start', target: 'ask_signoff' }, + { id: 'e2', source: 'ask_signoff', target: 'mark_approved', label: 'approve' }, + { id: 'e3', source: 'ask_signoff', target: 'mark_rejected', label: 'reject' }, + { id: 'e4', source: 'mark_approved', target: 'end_ok' }, + { id: 'e5', source: 'mark_rejected', target: 'end_no' }, + ], +}); + +/** + * Project Closure with Sign-off — the worked **nested durable pause** example. + * + * When a project is marked Completed, the flow invokes + * {@link ClosureSignoffSubflow} through a `subflow` node. The child suspends on + * its `approval` node, which suspends THIS run too — both continuations are + * persisted as linked runs (`sys_automation_run`), surviving restarts. When a + * manager decides (approvals API / inbox), the child resumes down the matching + * branch, completes, and **bubbles** its `decision` output back into this run + * (`signoffResult`), which continues to notify the project owner. + * + * Observe it end-to-end: complete a project → both runs show `paused` in the + * Runs panel (parent at `signoff`, child at `ask_signoff`) → approve via + * `POST /api/v1/approvals/requests/:id/approve` → both runs complete and the + * owner's inbox gets the decision. + */ +export const ProjectClosureFlow = defineFlow({ + name: 'showcase_project_closure', + label: 'Project Closure with Sign-off (nested pause)', + description: 'On project completion, requests sign-off via an approval-inside-subflow, then notifies the owner — demonstrates nested durable pause.', + type: 'autolaunched', + nodes: [ + { + id: 'start', + type: 'start', + label: 'On Project Completed', + config: { + objectName: 'showcase_project', + triggerType: 'record-after-update', + condition: 'status == "completed" && previous.status != "completed"', + }, + }, + { + id: 'signoff', + type: 'subflow', + label: 'Request Sign-off', + config: { + flowName: 'showcase_closure_signoff', + input: { + reason: 'Project "{record.name}" was marked completed — please sign off the closure.', + }, + outputVariable: 'signoffResult', + }, + }, + { + id: 'notify_owner', + type: 'notify', + label: 'Notify Owner of Decision', + config: { + topic: 'project.closure', + recipients: ['{record.owner}'], + channels: ['inbox'], + severity: 'info', + title: 'Closure sign-off: {record.name}', + message: 'Closure sign-off decision for "{record.name}": {signoffResult.decision}.', + actionUrl: '/showcase_project/{record.id}', + }, + }, + { id: 'end', type: 'end', label: 'End' }, + ], + edges: [ + { id: 'e1', source: 'start', target: 'signoff' }, + { id: 'e2', source: 'signoff', target: 'notify_owner' }, + { id: 'e3', source: 'notify_owner', target: 'end' }, + ], +}); + /** * Batch Reminders — demonstrates the ADR-0031 **structured loop container**. * @@ -716,6 +843,8 @@ export const allFlows = [ TaskFollowUpFlow, NotifyOwnerSubflow, TaskDoneNotifyOwnerFlow, + ClosureSignoffSubflow, + ProjectClosureFlow, BatchRemindersFlow, FanOutNotifyFlow, ResilientSyncFlow,