From c0be0a8883b60365131fa734adb05d45b2833196 Mon Sep 17 00:00:00 2001
From: stack72
Date: Sun, 12 Apr 2026 00:15:40 +0100
Subject: [PATCH] docs(skills/swamp-workflow): document when to use nested
workflows
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Add a "When to Use Nested Workflows" section to the swamp-workflow skill
covering the three cases where a child workflow is the right shape —
most importantly, forEach over an async list (`data.latest()`,
`data.findByTag()`, etc.). `forEach.in` is evaluated synchronously so
promise-returning CEL functions never resolve in that position and the
step fails with a misleading "got: object" error. Task `inputs:` ARE
awaited for both model_method and workflow tasks, so the canonical fix
is to resolve the async call in the parent's task.inputs and let the
child iterate over a plain `inputs.`.
Also cross-references the new section from expressions-and-foreach.md
so readers landing there find the escape hatch, and updates the
SKILL.md reference pointer so the topic is discoverable from the index.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.claude/skills/swamp-workflow/SKILL.md | 7 +-
.../references/expressions-and-foreach.md | 18 +++++
.../references/nested-workflows.md | 77 +++++++++++++++++++
3 files changed, 99 insertions(+), 3 deletions(-)
diff --git a/.claude/skills/swamp-workflow/SKILL.md b/.claude/skills/swamp-workflow/SKILL.md
index ed510cb8..8ce0faf7 100644
--- a/.claude/skills/swamp-workflow/SKILL.md
+++ b/.claude/skills/swamp-workflow/SKILL.md
@@ -541,9 +541,10 @@ End-to-end workflow creation:
[references/ci-integration.md](../swamp-repo/references/ci-integration.md) for
installing swamp in CI and GitHub Actions examples
- **Nested workflows**: See
- [references/nested-workflows.md](references/nested-workflows.md) for full
- examples of workflows calling other workflows, forEach with workflows, and
- nesting limitations
+ [references/nested-workflows.md](references/nested-workflows.md) for when to
+ split a workflow into parent + child (including the async-list-into-forEach
+ pattern), full examples of workflows calling other workflows, forEach with
+ workflows, and nesting limitations
- **Expressions, forEach, and data tracking**: See
[references/expressions-and-foreach.md](references/expressions-and-foreach.md)
for forEach iteration patterns, CEL expressions, environment variables, and
diff --git a/.claude/skills/swamp-workflow/references/expressions-and-foreach.md b/.claude/skills/swamp-workflow/references/expressions-and-foreach.md
index de6b8a38..f7b7305b 100644
--- a/.claude/skills/swamp-workflow/references/expressions-and-foreach.md
+++ b/.claude/skills/swamp-workflow/references/expressions-and-foreach.md
@@ -74,6 +74,24 @@ With `--input '{"tags": {"env": "prod", "team": "platform"}}'`, creates steps:
| `self.{item}.key` | Key name (object iteration) |
| `self.{item}.value` | Value (object iteration) |
+### forEach.in Cannot Await Promises
+
+`forEach.in` is evaluated **synchronously**. Async CEL functions that return a
+Promise — `data.latest()`, `data.findByTag()`, `data.findBySpec()` — do not
+resolve in this position and the step fails with
+`forEach.in must evaluate to an array or object, got: object`.
+
+Only these shapes work directly in `forEach.in`:
+
+- `${{ inputs. }}` — workflow inputs (pre-resolved before expansion)
+- Static literals
+
+To iterate over a list produced by `data.latest()` (or any async source), split
+into a parent + child workflow and let the parent's `task.inputs` resolve the
+list — task inputs ARE awaited. See
+[nested-workflows.md § When to Use Nested Workflows](nested-workflows.md#when-to-use-nested-workflows)
+for the canonical pattern.
+
### forEach with Vary Dimensions
Use `vary` on `dataOutputOverrides` to isolate data per forEach iteration:
diff --git a/.claude/skills/swamp-workflow/references/nested-workflows.md b/.claude/skills/swamp-workflow/references/nested-workflows.md
index 4036c5a9..6c2046af 100644
--- a/.claude/skills/swamp-workflow/references/nested-workflows.md
+++ b/.claude/skills/swamp-workflow/references/nested-workflows.md
@@ -2,6 +2,7 @@
## Table of Contents
+- [When to Use Nested Workflows](#when-to-use-nested-workflows)
- [Basic Nested Workflow](#basic-nested-workflow)
- [Workflow Task Fields](#workflow-task-fields)
- [Nested Workflow with forEach](#nested-workflow-with-foreach)
@@ -11,6 +12,82 @@
Steps can invoke another workflow using `type: workflow`. The parent step waits
for the child workflow to complete before continuing.
+## When to Use Nested Workflows
+
+Reach for a child workflow when a flat workflow can't express the shape you
+need. The cases that come up in practice:
+
+### 1. forEach over an async list (`data.latest()`, `data.findByTag()`, etc.)
+
+`forEach.in` is evaluated **synchronously** at expansion time. CEL expressions
+that return a `Promise` — `data.latest()`, `data.findByTag()`,
+`data.findBySpec()` — never resolve in this position, and forEach fails with
+`forEach.in must evaluate to an array or object, got: object` (the "object" is
+the unresolved Promise).
+
+Task `inputs:` ARE awaited for both `model_method` and `workflow` tasks. Move
+the async call into the parent's `task.inputs` and let the child iterate over a
+plain `inputs.`:
+
+```yaml
+# parent — task.inputs awaits data.latest() before invoking child
+- name: download
+ task:
+ type: workflow
+ workflowIdOrName: download-episodes
+ inputs:
+ episodes: ${{ data.latest("dedup", "current").attributes.episodes }}
+```
+
+```yaml
+# child — declares episodes as an array input
+inputs:
+ properties:
+ episodes:
+ type: array
+ items: { type: object }
+ required: ["episodes"]
+
+jobs:
+ - name: download
+ steps:
+ - name: download-${{ self.ep.show }}
+ forEach:
+ item: ep
+ in: ${{ inputs.episodes }} # already resolved — sync eval is fine
+ task:
+ type: model_method
+ modelIdOrName: transmission
+ methodName: add
+ inputs:
+ uri: ${{ self.ep.magnet }}
+ protocol: torrent
+```
+
+The child's input schema validates the boundary, so shape drift between producer
+and consumer is caught at invoke time.
+
+### 2. Reusable sub-process invoked from multiple parents
+
+When the same sequence of steps runs from a cron parent, a manual run, and
+another workflow, extract it into a child workflow with a typed input schema.
+Duplicating steps across workflows is the wrong trade — the child gives you one
+validated entry point.
+
+### 3. Independent cadence or isolation
+
+A child workflow can carry its own `trigger.schedule` and still be invoked by a
+parent. Splitting lets the child run independently — useful for backfill, manual
+replays, and tests — without dragging the parent's prelude along.
+
+### When NOT to nest
+
+- **Pure ordering within a single run** → use `dependsOn` between jobs or steps.
+ A workflow boundary is not an ordering primitive.
+- **Sharing a single resolved value across steps** → reference it directly via
+ CEL in each step's inputs; don't pay the boundary cost.
+- **Nesting depth pressure** — the cap is 10. Each level should earn its keep.
+
## Basic Nested Workflow
**Child workflow** (`notify-team`):