Skip to content

Feat/foreach step - #6473

Merged
rsareddy0329 merged 7 commits into
aws:masterfrom
nayan3107:feat/foreach-step
Oct 20, 2026
Merged

rsareddy0329 merged 7 commits into
aws:masterfrom
nayan3107:feat/foreach-step

Conversation

@nayan3107

@nayan3107 nayan3107 commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

What: Add ForEachStep to sagemaker.mlops.workflow. A ForEach step runs its body step(s) once per item of a list resolved at execution time, with a concurrency cap of 1-50.

iterable_items: a property reference that resolves to a JSON list, or a literal list of up to 100 scalars/property references.
for_each_body: one or more steps (Condition, Fail and nested ForEach are not allowed). Body steps read the current item via CurrentItem(), including deep paths and bracket indexing (CurrentItem().a.b, CurrentItem()["score"]).
ForEachSelector(pick="MaxBy"|"MinBy", field=IterationOutput(body_step).) picks one iteration.
Downstream references: properties.IterationOutputs[i].., properties.Selected., and the aggregates Status, InputItemCount, TotalStepCount, SucceededStepCount, FailedStepCount, StoppedStepCount.
Run conditions: depends_on on the ForEach step and on its body steps accepts StepDependency(step, TriggerCondition.<...>), serialized the same way as for other steps.
StepsCompiler and PipelineGraph compile ForEach bodies and enforce unique step names across bodies.
Why: SageMaker Pipelines adds a ForEach step type; this exposes it in the PySDK.

Test: Unit tests for the new step, compiler paths and pipeline wire format, including full Pipeline.definition() checks and run-condition triggers on the ForEach step, its body and downstream steps; all 441 tests in sagemaker-mlops/tests/unit/workflow pass.

flake8 and pylint still haven't been run locally, so CI will be the first lint check.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI code review

This PR adds a new ForEachStep workflow step (plus ForEachSelector, CurrentItem, IterationOutput, and supporting references) to sagemaker.mlops.workflow, and wires it into StepsCompiler and PipelineGraph following the established ConditionStep compound-step pattern. The implementation is consistent with existing conventions: it overrides step_only_arguments so body references are not treated as top-level dependencies, mirrors _flatten_compound_step/_build_*/_generate_step_map handling, and interpolates correctly as a PipelineVariable. Validation is comprehensive and the test suite is thorough (wire format, downstream references, run conditions, nesting in ConditionStep, duplicate-name enforcement). I found no correctness or security issues; only one low-severity edge case worth a glance.

Reviewed commit 8dd22ec. Automated review; verify before acting.

f"IterationOutputs of ForEach step '{self._step.name}' must be indexed "
f"with an integer, e.g. properties.IterationOutputs[0].<bodyStepName>."
)
return _ForEachPropertyReference(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Low: __getitem__ uses isinstance(item, int) to decide between list-index and map-key formatting. Because bool is a subclass of int, IterationOutputs[True] (or ref[False]) is silently treated as index 1/0 rather than rejected. The _IterationOutputsAccessor.__getitem__ already explicitly guards against non-int keys; you may want the same care here (e.g. isinstance(item, int) and not isinstance(item, bool)) so booleans don't accidentally produce an integer index. Minor, since passing a bool here is unusual.

Daniel Castro and others added 7 commits October 19, 2026 19:15
Adds a new ForEachStep to sagemaker.mlops.workflow that iterates a
body of steps over a collection at pipeline execution time. Supports
CurrentItem / ChildOutput references, a Selector for picking values
from prior child outputs, and a configurable max_concurrency in the
range [1, 50].

- New for_each_step.py exposing ForEachStep, ForEachSelector,
  ForEachSelectorPickType, CurrentItem, and ChildOutput.
- Add FOR_EACH to StepTypeEnum.
- Extend StepsCompiler to compile ForEachStep bodies and share
  duplicate-name detection with ConditionStep by generalizing
  _flatten_condition_step into _flatten_compound_step.
- Track for_each_body dependencies in PipelineGraph and enforce
  unique step names across ForEachStep bodies.
- Re-export the new symbols from sagemaker.mlops.workflow.
- Unit tests covering the new step type, compiler paths, and
  pipeline wire format.
Remove Status, InputItemCount, TotalStepCount, SucceededStepCount,
FailedStepCount, and StoppedStepCount from ForEachStep.properties.
Only ChildOutputs[i] and Selected remain as downstream-referenceable
properties.

This aligns with the backend policy change: any child failure now
causes the entire ForEach step to fail (no partial success). The
PartiallySucceeded status is removed, making aggregate counters
no longer meaningful to downstream steps.
Remove _TerminalPropertyReference class and CurrentItem overrides.
CurrentItem now inherits full drilling from _ForEachPropertyReference:
- Deep dot paths: CurrentItem().a.b.c
- Bracket indexing: CurrentItem()['score'], CurrentItem()[0]

All forms produce valid {"Get": "ForEach.CurrentItem..."} expressions.
The service resolves these at expansion time for each child lane.
Remove the "exactly one body step" restriction so ForEach bodies can
contain any number of steps. Each iteration spawns one child instance
of every body step. Rename ChildOutput / ChildOutputs to
IterationOutput / IterationOutputs across the public API.

Property reference changes:
- properties.IterationOutputs[i].<bodyStepName>.<member> now requires
  a body step name qualifier, since an iteration may span multiple
  body steps.
- properties.Selected.<member> resolves directly to the selector-
  picked iteration's output — no body step name qualifier, because
  the selector's field already identifies the target body step.
The service still resolves the ForEach aggregate members (Status,
InputItemCount and the step counts) as downstream references, and
accepts a literal IterableItems list of up to 100 scalars or property
references. Expose both and add ForEach to the step-type guard test.
---
X-AI-Prompt: Fix the ForEachStep gaps against the Tioga backend
X-AI-Tool: kiro-cli
ForEach compiled DependsOn as plain names, so a StepDependency trigger
on the ForEach step was silently dropped. Use the shared run-condition
serializer like regular and Condition steps.
---
X-AI-Prompt: Rebase the ForEach PR onto the merged Run Conditions PR
X-AI-Tool: kiro-cli
@nayan3107
nayan3107 deployed to manual-approval October 20, 2026 02:23 — with GitHub Actions Active
@nayan3107
nayan3107 deployed to manual-approval October 20, 2026 02:23 — with GitHub Actions Active
@nayan3107
nayan3107 deployed to manual-approval October 20, 2026 02:23 — with GitHub Actions Active

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI code review

This PR adds ForEachStep (plus ForEachSelector, ForEachSelectorPickType, CurrentItem, IterationOutput) to sagemaker.mlops.workflow, wires it into the public __init__ exports, the StepsCompiler, and PipelineGraph, and adds a new ForEach StepTypeEnum. The implementation closely mirrors the existing ConditionStep compound-step patterns (flattening, step-map recursion, JSON-serialization flag propagation, adjacency edges) and is backed by a thorough unit test suite covering validation, wire format, downstream references, and run-condition triggers. The code is well-structured and I found no correctness or compatibility bugs; only minor points below.

Reviewed commit 18a890d. Automated review; verify before acting.

root_prop.__dict__["Selected"] = _ForEachPropertyReference(f"Steps.{name}.Selected", [self])
self._properties = root_prop

@staticmethod

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Low: _validate_selector indexes selector.field.expr.get("Get"), but field is only checked to be a PipelineVariable in ForEachSelector.__init__, not an IterationOutput. A PipelineVariable subclass whose expr is not a dict (e.g. returns a list or string) would raise AttributeError here instead of the intended ValueError. Consider guarding with isinstance(selector.field.expr, dict) before .get, or validating the field type up front. Low severity since the public IterationOutput always returns a dict expr.

@staticmethod
def _validate_selector(
name: str, selector: Optional[ForEachSelector], for_each_body: List[Step]
):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Low: arguments and step_only_arguments duplicate the IterableItems/MaxConcurrency/Selector construction. step_only_arguments could be built once and arguments extend it with ForEachBody, which keeps the two in sync (compare ConditionStep.step_only_arguments reusing Conditions). Minor maintenance nit.

@rsareddy0329
rsareddy0329 merged commit d75056d into aws:master Oct 20, 2026
20 of 21 checks passed

This branch was successfully deployed

1 active deployment
manual-approval — 18a890d9 Deployed Oct 20, 2026 by nayan3107 via wait-for-approval #1785
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants