Relationships
#1334 forEach leaves self.* unresolved in target
Opened by ehazlett · 7/22/2026· Shipped 7/22/2026
Using forEach to create one workflow step per worker does not resolve the iteration value inside the step-level target field.
Tested with:
swamp 20260722.020746.0-sha.5d00afa2Minimal example:
id: aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaa10
name: dynamic-worker-targeting
inputs:
properties: {}
jobs:
- name: collect
steps:
- name: collect-${{ self.worker.attributes.name }}
forEach:
item: worker
in: ${{ data.query('modelType == "swamp/worker"') }}
target: ${{ self.worker.attributes.name }}
task:
type: model_method
modelIdOrName: host-inventory
methodName: sync
dependsOn: []
weight: 0
dependsOn: []
weight: 0
version: 1Run:
swamp workflow evaluate dynamic-worker-targeting \
--repo-dir /path/to/repository \
--jsonThe evaluator expands the step name correctly:
{
"name": "collect-worker-01",
"target": "${{ self.worker.attributes.name }}"
}The same self.worker.attributes.name expression is therefore resolved in name but left as literal text in target.
When the workflow is executed through swamp serve, dispatch waits for a worker whose target is literally:
${{ self.worker.attributes.name }}The workflow then remains queued until queueTimeout expires. The resulting dispatch error does not identify the unresolved expression.
Expected behavior
Each expanded step should use its corresponding worker name:
{
"name": "collect-worker-01",
"target": "worker-01"
}Placement fields such as target, labels, and platform should resolve self.* during forEach expansion, consistently with other step fields.
At minimum, validation should reject unresolved ${{ ... }} expressions in placement fields. Treating an unresolved expression as a literal placement constraint leads to a delayed timeout instead of an actionable validation error.
Impact
This prevents the direct expression of a common fleet operation: create one step per worker and pin each step to that worker.
Without per-instance targeting, multiple expanded steps may execute on the same eligible worker while other workers are missed. The workaround is to introduce a child workflow, pass the worker name as an input, and use ${{ inputs.workerName }} as the child’s target.
Shipped
Click a lifecycle step above to view its details.