Skip to content

Proposal: for step type for basic loop support #21

Description

@tbrandenburg

Motivation

Workflows often need to iterate over a list of items, such as processing multiple files, running agent tasks per item, or collecting results. Today users have to inline bash for or while loops inside a single bash step, which breaks the flowsh step model and makes it impossible to use per-item vars, agent steps, or structured logging.

Proposed Design: for wrapper step type

Add a new for step type that wraps a sequence of child steps and iterates over a variable from a vars step.

YAML shape

steps:
  - type: vars
    name: Collect items
    values:
      ITEMS: |
        printf "file-a\nfile-b\nfile-c"

  - type: for
    name: Process each item
    in: ITEMS
    item: ITEM
    steps:
      - type: bash
        name: Process
        run: |
          echo "Processing $ITEM"

      - type: agent
        name: Review
        expandPrompt: true
        prompt: |
          Review the file $ITEM and suggest improvements.

How it generates

Inner steps are rendered as top-level bash functions (once, outside the loop), then called via run_step inside a while read loop:

# Inner step -- defined once outside the loop
for_0_process() {
  bash -euo pipefail <<'BASH_EOF'
echo "Processing $ITEM"
BASH_EOF
}

# Loop step
step_process_for_item() {
  while IFS= read -r ITEM; do
    export ITEM
    run_step for_0_process
    run_step for_0_review
  done <<< "$ITEMS"
}
run_stateful_step step_process_for_item

This gives every inner step:

  • Per-iteration logging via run_step
  • Failure catching via catch() with the correct exit code
  • Dry-run support
  • State preservation via export ITEM

Models change

Add ForStep to models.py:

class ForStep(BaseStep):
    type: Literal["for"]
    in_: str = Field(..., validation_alias="in")
    item: str = Field(...)
    steps: list[Step] = Field(min_length=1)

Add to Step union.

Renderer change

In render.py:render_step, add a new elif isinstance(step, ForStep) branch:

  1. Emit each inner step as its own function using the existing step_function_name and used_function_names for unique names.
  2. Emit the loop function body with run_step <name> calls inside while IFS= read -r ....
  3. Use run_stateful_step for the outer for function.

Accepted restrictions / downsides

  • Items are newline-delimited
  • Loop variable must match [A-Z_][A-Z0-9_]*
  • agent inner steps need expandPrompt: true to see the loop variable
  • No nested for loops in v1
  • Flat function namespace

Decisions

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions