Relationships
#2348 Add self._index to forEach expression context
Opened by stack72 · 9/22/2026· Shipped 10/2/2026
Problem
Workflow forEach steps that iterate over file paths cannot use the factory pattern (one model definition per iteration) because file paths contain slashes, which are rejected by both model names and dataOutputOverrides vary values. There is no way to generate a unique, slug-safe identifier per iteration in CEL or template expressions.
The forEach index is already tracked internally (forEachIndex in
execution_service.ts) but is not exposed to the expression context.
Proposed Solution
Expose the zero-based forEach iteration index as self._index in both
template expressions (${{ self._index }}) and CEL expressions. The
underscore prefix avoids collision with user-defined forEach item
variables.
This enables the factory pattern for forEach steps:
- name: process-${{ self.file }}
forEach:
item: file
in: "${{ data.latest('repo', 'diff').attributes.files }}"
task:
type: model_method
modelType: "@some/extension"
modelName: processor-${{ run.id }}-${{ self._index }}
methodName: run
inputs:
data: "${{ data.latest('processor-' + run.id + '-' + string(self._index), 'result').attributes.value }}"Each iteration gets its own model definition — no shared datastore lock, full parallelism.
Implementation
A prototype is working on branch typesafe-triage-integration. The change
is ~3 lines in execution_service.ts:
- Line ~3147: Add
_index: forEachIndexwhen buildingselffor forEach iterations - Line ~953: Preserve
_indexwhenselfis rebuilt after model resolution (the existing code overwritesselfwith model definition fields + forEach vars, losing any extra properties)
Both changes are in DefaultStepExecutor and WorkflowExecutionService.
Alternatives
- Use
dataOutputOverrideswithvary— rejected: vary values cannot contain path separators - Use per-file model names with file paths — rejected: model names become filesystem paths, slashes create nested directories
- Use shared models with concurrency limits — works but causes datastore lock contention warnings at high concurrency
- Add a CEL
replace()function for string sanitization — heavier change, less general
Use Case
The immediate use case is the @swamp/typesafe-ai triage integration in
the verification workflow, where per-file diffs are sent to TypeSafe's Jev
model for noul evaluation. Each file needs its own model definition to
avoid lock contention when running 64+ concurrent iterations.
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.