Skip to main content
← Back to list
01Issue
BugShippedSwamp CLI
Assigneesstack72

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: object

The "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

  1. Create a workflow with a forEach step that uses data.latest() in forEach.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
  2. Run the workflow.

  3. 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-workflows

Environment

  • 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
02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPEDTRIAGE+ 4 MOREREVIEW+ 3 MOREPR_MERGEDSHIPPED

Shipped

4/13/2026, 5:56:47 PM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack724/11/2026, 11:27:31 PM

Sign in to post a ripple.