Skip to content

Python: [Bug]: Foreach loop body: exit edge wired to first action instead of last, causing parallel execution #5520

@corradopignato

Description

@corradopignato

Description

When a Foreach loop body contains multiple sequential actions (e.g., SendActivityInvokeAzureAgentInvokeAzureAgentSendActivity), the loop advances to the next iteration as soon as the first action completes, instead of waiting for all actions to finish. This effectively causes loop body actions to execute in parallel across iterations.

Root cause

In agent_framework_declarative/_workflows/_declarative_builder.py, method _create_foreach_structure(), around line 776:

# Body exit -> Next (get all exits from body and wire to next_executor)
body_exits = self._get_source_exits(body_entry)
for body_exit in body_exits:
    builder.add_edge(source=body_exit, target=next_executor)

body_entry is the first executor returned by _create_executors_for_actions(). _get_source_exits() checks for branch_exits (not present on a regular executor), then falls back to _exit_executor (not set), and finally returns the executor itself — i.e., the first action in the body.

The existing _get_branch_exit() method correctly uses _chain_executors[-1] to find the last executor in a chain, but _create_foreach_structure doesn't use it.

Suggested fix

# Before (buggy):
body_exits = self._get_source_exits(body_entry)
for body_exit in body_exits:
    builder.add_edge(source=body_exit, target=next_executor)

# After (fixed):
chain = getattr(body_entry, "_chain_executors", [body_entry])
last_in_chain = chain[-1]
body_exits = self._get_source_exits(last_in_chain)
for body_exit in body_exits:
    builder.add_edge(source=body_exit, target=next_executor)

Reproduction

Workflow YAML

- kind: Foreach
  id: my_loop
  source: =Local.items
  itemName: item
  indexName: idx
  actions:
    - kind: SendActivity
      id: step_1
      activity:
        text: '="Step 1 for: " & Local.item'

    - kind: InvokeAzureAgent
      id: step_2
      agent:
        name: my_agent
      input:
        messages: =Local.item
      output:
        responseObject: Local.result
        autoSend: false

    - kind: SendActivity
      id: step_3
      activity:
        text: '="Done: " & Local.item'

Expected behavior

For a list of 2 items, execution should be:

step_1 (item 0) → step_2 (item 0) → step_3 (item 0) → step_1 (item 1) → step_2 (item 1) → step_3 (item 1)

Actual behavior

step_1 (item 0) → step_1 (item 1) → step_2 (item 0) → step_2 (item 1) → ...

The loop advances to item 1 as soon as step_1 for item 0 completes, because the exit edge is wired from step_1 (first action) to ForeachNextExecutor instead of from step_3 (last action).

Code Sample

Error Messages / Stack Traces

Package Versions

agent-framework-declarative 1.0.0b260424

Python Version

Python 3.11.2

Additional Context

No response

Metadata

Metadata

Assignees

Type

No fields configured for Bug.

Projects

Status

In Progress

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions