Skip to content

.NET: [Bug]: Declarative Foreach collapses multi-field records to their first field in the loop value variable #6183

@yurii-beketov

Description

@yurii-beketov

Description

In declarative workflows, when a Foreach action iterates over a table whose rows are multi-field records, the loop value variable receives only the first field's value of each record instead of the full record.

For example, iterating over [{ name: "Alice", role: "Engineer" }, ...] assigns "Alice" to Local.currentItem rather than the whole { name, role } record, so Local.currentItem.role is unavailable inside the loop body.

This is distinct from #4195 (parsing produced empty records). Here the records are correctly populated, but ForeachExecutor deliberately reduces each one to its first property.

To Reproduce

1. Workflow YAML

kind: Workflow
name: repro-foreach-first-field
trigger:
  kind: OnConversationStart
  id: trigger
  actions:
    - kind: SetVariable
      id: set_people
      variable: Local.people
      value: |
        =Table(
          { name: "Alice", role: "Engineer" },
          { name: "Bob",   role: "Designer" },
          { name: "Carol", role: "PM" }
        )
    - kind: Foreach
      id: iterate_people
      items: =Local.people
      value: Local.currentItem
      index: Local.index
      actions:
        - kind: SendActivity
          id: show_item
          activity: "item={Local.currentItem} role={Local.currentItem.role}"
    - kind: EndWorkflow
      id: end

2. Expected behavior

Local.currentItem should be the full record for each iteration, so show_item prints e.g. item={ name: "Alice", role: "Engineer" } role=Engineer.

3. Actual behavior

Local.currentItem is just the first field's value ("Alice"), and Local.currentItem.role resolves to blank. The whole record is lost.

Root Cause

ForeachExecutor.ExecuteAsync collapses each row to its first property:

// ForeachExecutor.cs, line 47
this._values = [.. tableValue.Values.Select(value => value.Properties.Values.First().ToFormula())];

tableValue.Values is a sequence of RecordDataValue. Calling .Properties.Values.First() keeps only the first field's value and discards the rest of the record.

The framework already has the correct conversion elsewhere — DataValueExtensions.ToFormula maps each row through value.ToRecordValue(), preserving all fields:

TableDataValue tableValue =>
    FormulaValue.NewTable(
        tableValue.Values.FirstOrDefault()?.ParseRecordType() ?? RecordType.Empty(),
        tableValue.Values.Select(value => value.ToRecordValue())),
...
RecordDataValue recordValue => recordValue.ToRecordValue(),

ForeachExecutor simply does not use that path.

Proposed Fix

In ForeachExecutor.ExecuteAsync, convert each row as a whole value so records keep every field:

- this._values = [.. tableValue.Values.Select(value => value.Properties.Values.First().ToFormula())];
+ this._values = [.. tableValue.Values.Select(value => value.ToFormula())];

Each RecordDataValue then routes through the RecordDataValue recordValue => recordValue.ToRecordValue() branch of ToFormula, producing a full RecordValue. This makes Local.currentItem.<field> work directly and removes the need for the common user-side workaround of declaring an index and using Index(originalCollection, Local.index + 1).FieldName.

Tests Gap

The existing tests in ForeachExecutorTest.cs only build records with a single field (e.g. value/item), so .First() happens to return the correct value and the bug stays hidden. A test that iterates over multi-field records and asserts value is the full record would catch this regression.

Related

Package Versions

Reproduced against origin/main (ForeachExecutor.cs line 47 unchanged as of filing; file last modified 2026-02-12, #3835).

.NET Version

.NET 8.0

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    Status

    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions