From 9a713ff860cb2012741dcbda6a246541223718a9 Mon Sep 17 00:00:00 2001 From: Peter Ibekwe Date: Fri, 5 Jun 2026 15:32:57 -0700 Subject: [PATCH] Fix single-column value unwrap in declarative workflow --- .../ObjectModel/ForeachExecutor.cs | 11 ++++++- .../ObjectModel/ForeachExecutorTest.cs | 32 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) 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..adfa0babe2 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,38 @@ 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()); + } + [Fact] public async Task ForeachTakeLastAsync() {