From f5e6ebf2065b1c7ca5fc0a824508feed2e831a9e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 14:47:29 +0000 Subject: [PATCH] fix(showcase): contain the batch-reminders loop body per iteration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `showcase_batch_reminders` ran its `notify` node (`send_reminder`) bare inside the `loop_tasks` body with no `try_catch` between the loop and it. A `loop` body has no error handling of its own — the container iterates with a bare `await` — and `notify` returns `success: false` when every resolved recipient template is empty. One task with a blank `owner` therefore ended the whole run: every later task was never reminded, and the run summary reported `acted: 0` for work that had happened. Adopt the documented containment spelling from content/docs/automation/flows.mdx §"Per-iteration containment": a `try_catch` inside the body with `send_reminder` moved into its `try` region, and a `catch` of one bare `assignment` node (the shortest handler that parses — `catch.nodes` is `.min(1)`, and an omitted `catch` contains nothing). This is the showcase app's first in-repo hit of the `flow-loop-body-uncontained` warning. Example apps are what AI authors copy from, so the flow now demonstrates the loop container AND the per-iteration guard. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y --- .../src/automation/flows/index.ts | 57 ++++++++++++++++--- 1 file changed, 49 insertions(+), 8 deletions(-) diff --git a/examples/app-showcase/src/automation/flows/index.ts b/examples/app-showcase/src/automation/flows/index.ts index 34dc8f8927..40dc48db0a 100644 --- a/examples/app-showcase/src/automation/flows/index.ts +++ b/examples/app-showcase/src/automation/flows/index.ts @@ -852,11 +852,19 @@ export const ProjectClosureFlow = defineFlow({ * variable scope, and the body sends a reminder. A hard `maxIterations` guard * keeps iteration bounded. The loop node's ordinary out-edge (`→ end`) is the * after-loop continuation — the DAG invariant for ordinary edges is preserved. + * + * The body also demonstrates **per-iteration containment**: the reminder runs + * inside a `try_catch` whose `catch` is one bare `assignment`. A `loop` body has + * no error handling of its own, so without that guard the first task with an + * empty `owner` would end the whole run — later tasks never reminded, and the + * run summary reporting `acted: 0` for work that did happen. With it, every + * iteration is attempted and the sweep completes. See + * content/docs/automation/flows.mdx §"Per-iteration containment". */ export const BatchRemindersFlow = defineFlow({ name: 'showcase_batch_reminders', label: 'Batch Task Reminders (Loop)', - description: 'Iterates a collection of tasks and sends a reminder for each (structured loop container, ADR-0031).', + description: 'Iterates a collection of tasks and sends a reminder for each, each iteration contained by a try_catch so one bad task cannot end the sweep (structured loop container, ADR-0031).', type: 'autolaunched', variables: [ { name: 'tasks', type: 'list', isInput: true, isOutput: false }, @@ -874,15 +882,48 @@ export const BatchRemindersFlow = defineFlow({ maxIterations: 500, body: { nodes: [ + // Per-iteration containment (#13681 / #14394) — a `loop` body has NO + // error handling of its own: the container iterates with a bare + // `await`, so a body node that returns `success: false` propagates + // straight out and ends the WHOLE run. `notify` fails on an empty + // resolved recipient set, so one task with a blank `owner` would + // leave every later task unreminded while the run summary reports + // `acted: 0` for work that did happen. The guard is a `try_catch` + // INSIDE the body, one per iteration. { - id: 'send_reminder', - type: 'notify', - label: 'Send Reminder', + id: 'guard_reminder', + type: 'try_catch', + label: 'Guarded iteration', config: { - recipients: '{task.owner}', - title: 'Reminder ({taskIndex}): {task.title}', - sourceObject: 'showcase_task', - sourceId: '{task.id}', + try: { + nodes: [ + { + id: 'send_reminder', + type: 'notify', + label: 'Send Reminder', + config: { + recipients: '{task.owner}', + title: 'Reminder ({taskIndex}): {task.title}', + sourceObject: 'showcase_task', + sourceId: '{task.id}', + }, + }, + ], + edges: [], + }, + // The shortest handler that works: ONE bare `assignment` node + // with no `config` at all. A `catch` region cannot be empty — + // `FlowRegionSchema.nodes` is `.min(1)`, so `catch: {}` and + // `catch: { nodes: [] }` are both refused by the parse, and + // omitting `catch` entirely parses while containing NOTHING. + // `edges` omitted; `errorVariable` omitted (defaults to + // `$error`). See content/docs/automation/flows.mdx + // §"Per-iteration containment". + catch: { + nodes: [ + { id: 'reminder_failed', type: 'assignment', label: 'Reminder Failed (contained)' }, + ], + }, }, }, ],