forEach.in with data.latest() throws misleading 'got: object' error for unresolved Promise
Opened by stack72 · 4/11/2026· Shipped 4/13/2026
Summary
forEach.in is evaluated synchronously in expandForEachSteps(), so CEL expressions that return a Promise — data.latest(), data.findByTag(), data.findBySpec() — never resolve in that position. The step then fails with:
forEach.in must evaluate to an array or object, got: objectThe "object" is the unresolved Promise. The error message is actively misleading: the user reads "got: object" and assumes they returned the wrong shape from the model, when the real problem is that the CEL function is async and forEach.in cannot await it.
Steps to Reproduce
Create a workflow with a forEach step that uses
data.latest()inforEach.in:jobs: - name: download steps: - name: download-${{ self.ep.show }} forEach: item: ep in: ${{ data.latest(\"dedup\", \"current\").attributes.episodes }} task: type: model_method modelIdOrName: transmission methodName: add inputs: uri: ${{ self.ep.magnet }} protocol: torrent
Run the workflow.
Observe the error:
forEach.in must evaluate to an array or object, got: object.
Expected Behavior
The error should identify the actual cause (unresolved Promise from an async CEL function) and point at the workaround — resolve the async call in a parent workflow's task.inputs and pass the array as a child workflow input.
Suggested error message:
forEach.in received an unresolved Promise from '\${{ data.latest(...) }}'.
forEach.in is evaluated synchronously and cannot await async CEL functions
(data.latest, data.findByTag, data.findBySpec).
Fix: move the async call into a parent workflow's task.inputs (which IS
awaited) and have the child iterate over inputs.<name>. See:
.claude/skills/swamp-workflow/references/nested-workflows.md#when-to-use-nested-workflowsEnvironment
- Location: `src/domain/workflows/execution_service.ts:1543` (sync `celEvaluator.evaluate` call) and `:1640` (the misleading `UserError`)
- Related: `expandForEachSteps` is not async, so it cannot switch to `evaluateAsync` without propagating async through the `runJob` call stack. A smaller fix is to detect `items instanceof Promise` before the typeof check and throw a specific `UserError`.
- Documented workaround: parent/child workflow split with `task.inputs` resolving the async call — see systeminit/swamp#1165 which adds this to the `swamp-workflow` skill.
- swamp version: 20260411.204833.0-sha.44d3107f
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.