Skip to content

.NET: [Bug]: Declarative workflows fail or give wrong values on de-DE and fr-FR hosts #10033

Description

Observed Behavior

Declarative workflows parse expressions and values with the host's culture.
On a host whose culture uses , as the decimal separator, such as de-DE or fr-FR:

  • =Max(1, 2) doesn't parse, and the workflow fails.
  • ParseValue turns the string "3.14" into the number 314 on de-DE, and fails on fr-FR.

The same YAML works on en-US, so a workflow can pass on one machine and fail or give wrong values on another.

Expected Behavior

The same YAML gives the same result on every host, whatever its culture.

Steps to Reproduce

  1. Build the console app below against Microsoft.Agents.AI.Workflows.Declarative.
  2. Run it with LC_ALL=en_US.UTF-8: it prints Price is 3.14 and Max is 2.
  3. Run it with LC_ALL=de_DE.UTF-8: it prints Price is 314, then =Max(1, 2) fails to parse.
  4. Run it with LC_ALL=fr_FR.UTF-8: ParseValue fails with The input string '3.14' was not in a correct format.

Minimal Reproduction

using System.Globalization;
using System.Runtime.CompilerServices;
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Agents.AI.Workflows.Declarative;
using Microsoft.Extensions.AI;

const string Yaml = """
    kind: Workflow
    trigger:
      kind: OnConversationStart
      id: repro
      actions:
        - kind: ParseValue
          id: parse_price
          variable: Local.Price
          value: ="3.14"
          valueType: Number
        - kind: SendActivity
          id: send_price
          activity: "Price is {Local.Price}"
        - kind: SetVariable
          id: set_max
          variable: Local.Max
          value: =Max(1, 2)
        - kind: SendActivity
          id: send_max
          activity: "Max is {Local.Max}"
    """;

Console.WriteLine($"Culture: {CultureInfo.CurrentCulture.Name}");

Workflow workflow = DeclarativeWorkflowBuilder.Build<string>(new StringReader(Yaml), new DeclarativeWorkflowOptions(new NoAgentProvider()));
await using StreamingRun run = await InProcessExecution.RunStreamingAsync(workflow, "start");
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
    switch (evt)
    {
        case MessageActivityEvent message:
            Console.WriteLine($"Activity: {message.Message}");
            break;
        case ExecutorFailedEvent failed:
            Console.WriteLine($"Executor failed: {failed.Data?.InnerException?.Message ?? failed.Data?.Message}");
            break;
        case WorkflowErrorEvent error:
            Console.WriteLine($"Workflow error: {(error.Data as Exception)?.Message}");
            break;
    }
}

/// <summary>A provider for workflows that never call an agent.</summary>
internal sealed class NoAgentProvider : ResponseAgentProvider
{
    public override Task<string> CreateConversationAsync(CancellationToken cancellationToken = default) =>
        Task.FromResult(Guid.NewGuid().ToString("N"));

    public override Task<ChatMessage> CreateMessageAsync(string conversationId, ChatMessage conversationMessage, CancellationToken cancellationToken = default) =>
        Task.FromResult(conversationMessage);

    public override Task<ChatMessage> GetMessageAsync(string conversationId, string messageId, CancellationToken cancellationToken = default) =>
        throw new NotSupportedException();

    public override IAsyncEnumerable<AgentResponseUpdate> InvokeAgentAsync(string agentId, string? agentVersion, string? conversationId, IEnumerable<ChatMessage>? messages, IDictionary<string, object?>? inputArguments, CancellationToken cancellationToken = default) =>
        throw new NotSupportedException();

    public override async IAsyncEnumerable<ChatMessage> GetMessagesAsync(string conversationId, int? limit = null, string? after = null, string? before = null, bool newestFirst = false, [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        await Task.CompletedTask.ConfigureAwait(false);
        yield break;
    }
}

Error Messages and Stack Traces

Culture: de-DE
Activity: Price is 314
Executor failed: Errors: Error 7-8: Operator erwartet. An dieser Stelle in der Formel wird ein Operator wie beispielsweise +, *, oder & erwartet.
Error 8-9: Unerwartete Zeichen. In der Formel werden Zeichen nicht erwartungsgemäß verwendet.
Error 9-9: Unerwartete Zeichen. Die Formel enthält „Eof“, es wird aber „ParenClose“ erwartet.
Workflow error: Error invoking handler for Microsoft.Agents.AI.Workflows.Declarative.Kit.ActionExecutorResult

Culture: fr-FR
Executor failed: The input string '3.14' was not in a correct format.
Workflow error: Error invoking handler for Microsoft.Agents.AI.Workflows.Declarative.Kit.ActionExecutorResult

Package Versions

Microsoft.Agents.AI.Workflows.Declarative: built from main (fad6929)

.NET Version

.NET 10.0

Operating System

macOS

Regression

Unknown

Additional Context

WorkflowExpressionEngine doesn't set ParserOptions.Culture, so Power Fx parses with the current culture.
ObjectExtensions.ConvertType, which ParseValue uses, calls double.Parse(s) and the like without a format provider.
TemplateExtensions.Format doesn't pass parser options either, but only tests call it (see #9495).

I tried setting ParserOptions.Culture to CultureInfo.InvariantCulture in WorkflowExpressionEngine and TemplateExtensions, and passing CultureInfo.InvariantCulture to those Parse calls.
With that, the repro prints Price is 3,14 and Max is 2 on de-DE and fr-FR.

The Declarative unit tests depend on the culture too.
On main, 72 of 1305 fail with LC_ALL=de_DE.UTF-8, and all pass with LC_ALL=en_US.UTF-8.
With the change above, en-US still passes and 20 still fail on de-DE:

  • 11 EditTableExecutorTest cases call RecalcEngine.Eval directly in the test.
  • 7 tests expect US number or date formats, in EntityExtractorTest, FormulaValueExtensionsTests, and ObjectExtensionsTests.
  • 2 InvokeAzureAgentExecutorTest cases fail because IsBlankResponseMemberAccessFailure looks for the English error text Deprecated use of '.'.
    Power Fx localizes its errors, so on de-DE an externalLoop.when like =Local.Result.IsResolved throws after a non-JSON reply instead of evaluating to false.

The Python package already runs Power Fx in en-US whatever the host culture (_POWERFX_EVAL_LOCALE in _declarative_base.py).
#4321 was the same English error-text problem there.

EntityExtractor also parses dates with DateTime.TryParse in the current culture, so on en-IN 12/25/2023 is invalid and 3 EntityExtractorTest cases fail.
That may be intended for user input, but the tests assume a US culture.

AI assistance: I used an AI coding assistant for the analysis, the repro, and this write-up.

Acknowledgements

  • I searched existing issues and did not find a duplicate.
  • I personally verified this behavior and the reproduction details are authentic.
  • I will wait for explicit maintainer agreement before starting implementation of a non-trivial change.

Activity

  1. added
    .NETUsage: [Issues, PRs], Target: .Net
    pythonUsage: [Issues, PRs], Target: Python
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Nov 3, 2026
  2. changed the title [-].NET: [Bug]: Declarative workflows fail or give wrong values on de-DE and fr-FR hosts[/-] [+]Python: .NET: [Bug]: Declarative workflows fail or give wrong values on de-DE and fr-FR hosts[/+] on Nov 3, 2026
  3. added
    reproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow
    on Nov 3, 2026
  4. github-actions commented on Nov 3, 2026

    @github-actions
    Contributor

    🤖 Automated triage reproduction notes (agent-authored — trust but verify)

    Agent analysis

    Repro: WorkflowExpressionEngine.cs::EvaluateScope/GetSensitiveReferences rejects Max(1, 2) under de-DE, while ObjectExtensions.cs::ConvertType converts "3.14" to 314. The trigger is evaluating declarative expressions or parsing numeric strings when the host uses comma decimal separators. Minimal repro: set CultureInfo.CurrentCulture to de-DE, evaluate NumberExpression.Expression("Max(1, 2)"), and convert "3.14" to double.

    • Failing test: dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs
    • Files examined: dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/PowerFx/WorkflowExpressionEngine.cs, dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Extensions/ObjectExtensions.cs, dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/PowerFx/WorkflowExpressionEngineTests.cs, dotnet/tests/Microsoft.Agents.AI.Workflows.Declarative.UnitTests/Extensions/ObjectExtensionsTests.cs
    • Tests run: WorkflowExpressionEngineTests.NumberExpressionGetValueFormulaUsesInvariantCulture, ObjectExtensionsTests.ConvertNumberFromStringUsesInvariantCulture
    • Reported version: main@fad6929c
  5. added
    declarativeUsage: [Issues, PRs], Target: declarative agents and workflows
    workflowsUsage: [Issues, PRs], Target: Workflows
    and removed
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Nov 3, 2026
  6. changed the title [-]Python: .NET: [Bug]: Declarative workflows fail or give wrong values on de-DE and fr-FR hosts[/-] [+].NET: [Bug]: Declarative workflows fail or give wrong values on de-DE and fr-FR hosts[/+] on Nov 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

.NETUsage: [Issues, PRs], Target: .NetdeclarativeUsage: [Issues, PRs], Target: declarative agents and workflowspythonUsage: [Issues, PRs], Target: PythonreproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflowworkflowsUsage: [Issues, PRs], Target: Workflows

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions