Skip to content

Commit f5e6ebf

Browse files
committed
fix(showcase): contain the batch-reminders loop body per iteration
`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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
1 parent 3e270d4 commit f5e6ebf

1 file changed

Lines changed: 49 additions & 8 deletions

File tree

  • examples/app-showcase/src/automation/flows

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

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -852,11 +852,19 @@ export const ProjectClosureFlow = defineFlow({
852852
* variable scope, and the body sends a reminder. A hard `maxIterations` guard
853853
* keeps iteration bounded. The loop node's ordinary out-edge (`→ end`) is the
854854
* after-loop continuation — the DAG invariant for ordinary edges is preserved.
855+
*
856+
* The body also demonstrates **per-iteration containment**: the reminder runs
857+
* inside a `try_catch` whose `catch` is one bare `assignment`. A `loop` body has
858+
* no error handling of its own, so without that guard the first task with an
859+
* empty `owner` would end the whole run — later tasks never reminded, and the
860+
* run summary reporting `acted: 0` for work that did happen. With it, every
861+
* iteration is attempted and the sweep completes. See
862+
* content/docs/automation/flows.mdx §"Per-iteration containment".
855863
*/
856864
export const BatchRemindersFlow = defineFlow({
857865
name: 'showcase_batch_reminders',
858866
label: 'Batch Task Reminders (Loop)',
859-
description: 'Iterates a collection of tasks and sends a reminder for each (structured loop container, ADR-0031).',
867+
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).',
860868
type: 'autolaunched',
861869
variables: [
862870
{ name: 'tasks', type: 'list', isInput: true, isOutput: false },
@@ -874,15 +882,48 @@ export const BatchRemindersFlow = defineFlow({
874882
maxIterations: 500,
875883
body: {
876884
nodes: [
885+
// Per-iteration containment (#13681 / #14394) — a `loop` body has NO
886+
// error handling of its own: the container iterates with a bare
887+
// `await`, so a body node that returns `success: false` propagates
888+
// straight out and ends the WHOLE run. `notify` fails on an empty
889+
// resolved recipient set, so one task with a blank `owner` would
890+
// leave every later task unreminded while the run summary reports
891+
// `acted: 0` for work that did happen. The guard is a `try_catch`
892+
// INSIDE the body, one per iteration.
877893
{
878-
id: 'send_reminder',
879-
type: 'notify',
880-
label: 'Send Reminder',
894+
id: 'guard_reminder',
895+
type: 'try_catch',
896+
label: 'Guarded iteration',
881897
config: {
882-
recipients: '{task.owner}',
883-
title: 'Reminder ({taskIndex}): {task.title}',
884-
sourceObject: 'showcase_task',
885-
sourceId: '{task.id}',
898+
try: {
899+
nodes: [
900+
{
901+
id: 'send_reminder',
902+
type: 'notify',
903+
label: 'Send Reminder',
904+
config: {
905+
recipients: '{task.owner}',
906+
title: 'Reminder ({taskIndex}): {task.title}',
907+
sourceObject: 'showcase_task',
908+
sourceId: '{task.id}',
909+
},
910+
},
911+
],
912+
edges: [],
913+
},
914+
// The shortest handler that works: ONE bare `assignment` node
915+
// with no `config` at all. A `catch` region cannot be empty —
916+
// `FlowRegionSchema.nodes` is `.min(1)`, so `catch: {}` and
917+
// `catch: { nodes: [] }` are both refused by the parse, and
918+
// omitting `catch` entirely parses while containing NOTHING.
919+
// `edges` omitted; `errorVariable` omitted (defaults to
920+
// `$error`). See content/docs/automation/flows.mdx
921+
// §"Per-iteration containment".
922+
catch: {
923+
nodes: [
924+
{ id: 'reminder_failed', type: 'assignment', label: 'Reminder Failed (contained)' },
925+
],
926+
},
886927
},
887928
},
888929
],

0 commit comments

Comments
 (0)