Skip to main content
← Back to list
01Issue
FeatureShippedSwamp CLIPublic
Assigneesstack72

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:

  1. Line ~3147: Add _index: forEachIndex when building self for forEach iterations
  2. Line ~953: Preserve _index when self is rebuilt after model resolution (the existing code overwrites self with model definition fields + forEach vars, losing any extra properties)

Both changes are in DefaultStepExecutor and WorkflowExecutionService.

Alternatives

  • Use dataOutputOverrides with vary — 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.

02Bog Flow
✓OPEN✓TRIAGED✓IN PROGRESS✓SHIPPED+ 1 MOREASSIGNED+ 5 MOREREVIEW+ 10 MOREPR_MERGED+ 2 MORESESSION_SUMMARIZED

Shipped

10/2/2026, 8:19:28 PM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack7210/2/2026, 7:22:41 PM

Sign in to post a ripple.