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`):