diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ForeachExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ForeachExecutor.cs index e88cd25ef6..f154ad7f97 100644 --- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ForeachExecutor.cs +++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/ForeachExecutor.cs @@ -49,7 +49,7 @@ internal sealed class ForeachExecutor : DeclarativeActionExecutor EvaluationResult expressionResult = this.Evaluator.GetValue(this.Model.Items); if (expressionResult.Value is TableDataValue tableValue) { - this._values = [.. tableValue.Values.Select(value => value.ToFormula())]; + this._values = [.. tableValue.Values.Select(ToLoopValue)]; } else { @@ -99,6 +99,15 @@ internal sealed class ForeachExecutor : DeclarativeActionExecutor } } + // Power Fx wraps scalar array literals (`=[1, 2, 3]`) as `Table({Value: 1}, ...)`. Unwrap that single-column + // `Value`-record shape so `Local.LoopValue` is the scalar; multi-field and other shapes pass through unchanged. + private static FormulaValue ToLoopValue(DataValue value) => + value is RecordDataValue record + && record.Properties.Count == 1 + && record.Properties.TryGetValue("Value", out DataValue? singleColumn) + ? singleColumn.ToFormula() + : value.ToFormula(); + /// /// /// Persists the iteration cursor (), the materialized item snapshot diff --git a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ForeachExecutorTest.cs b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ForeachExecutorTest.cs index 1eaf8a00e0..b3b16f149b 100644 --- a/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ForeachExecutorTest.cs +++ b/dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/ObjectModel/ForeachExecutorTest.cs @@ -170,6 +170,67 @@ public sealed class ForeachExecutorTest(ITestOutputHelper output) : WorkflowActi Assert.Equal("Engineer", currentValue.GetField("role").ToObject()); } + /// + /// Power Fx wraps scalar array literals such as =[1, 2, 3] as Table({Value: 1}, ...); + /// the loop value must expose the bare scalar, not the single-column wrapper record. + /// + [Fact] + public async Task ForeachTakeNextWithSingleColumnValueRecordAsync() + { + // Arrange + const string CurrentValueName = "CurrentValue"; + this.SetVariableState(CurrentValueName); + + TableDataValue tableValue = DataValue.TableFromRecords( + DataValue.RecordFromFields(new KeyValuePair("Value", new NumberDataValue(1))), + DataValue.RecordFromFields(new KeyValuePair("Value", new NumberDataValue(2))), + DataValue.RecordFromFields(new KeyValuePair("Value", new NumberDataValue(3)))); + + Foreach model = this.CreateModel( + displayName: nameof(ForeachTakeNextWithSingleColumnValueRecordAsync), + items: ValueExpression.Literal(tableValue), + valueName: CurrentValueName, + indexName: null); + ForeachExecutor action = new(model, this.State); + + // Act + await this.ExecuteAsync(action, ForeachExecutor.Steps.Next(action.Id), action.TakeNextAsync); + + // Assert + FormulaValue currentValue = this.State.Get(CurrentValueName); + Assert.IsNotType(currentValue, exactMatch: false); + Assert.Equal(1m, currentValue.ToObject()); + } + + /// + /// Single-field records whose only field is NOT named Value are not Power Fx auto-wraps; + /// they are preserved as records so the field name remains accessible inside the loop body. + /// + [Fact] + public async Task ForeachTakeNextWithSingleFieldNonValueRecordAsync() + { + // Arrange + const string CurrentValueName = "CurrentValue"; + this.SetVariableState(CurrentValueName); + + TableDataValue tableValue = DataValue.TableFromRecords( + DataValue.RecordFromFields(new KeyValuePair("name", new StringDataValue("Alice")))); + + Foreach model = this.CreateModel( + displayName: nameof(ForeachTakeNextWithSingleFieldNonValueRecordAsync), + items: ValueExpression.Literal(tableValue), + valueName: CurrentValueName, + indexName: null); + ForeachExecutor action = new(model, this.State); + + // Act + await this.ExecuteAsync(action, ForeachExecutor.Steps.Next(action.Id), action.TakeNextAsync); + + // Assert + RecordValue currentValue = Assert.IsType(this.State.Get(CurrentValueName), exactMatch: false); + Assert.Equal("Alice", currentValue.GetField("name").ToObject()); + } + [Fact] public async Task ForeachTakeLastAsync() {