You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
.NET: Python: [Bug]: Declarative custom-function regexes swallow the formula tail, silently emitting formula source or failing compilation #9173
(.+) is greedy and \)$ only requires the formula to end with some ) — not that the paren closes the call being matched. So any formula that starts with one of these names and ends with ) is captured whole, and everything after the call's own closing paren is swallowed into the argument list.
eval() consults _eval_custom_function before PowerFx and returns on any non-None result, so the mis-parse wins and the formula never reaches the engine that would have handled it correctly.
Two symptoms follow, depending on what the swallowed tail looks like:
Formula
Expected
Actual
Concat("a") & Lower("B")
ab
a") & Lower("B
Concatenate("x") & Upper("y")
xY
x") & Upper("y
MessageText("hi") & Upper("there")
hiTHERE
ValueError: Power Fx failed compilation: Error 4 - 5 : Unexpected characters.
Concat("a", "b")(control, no trailing operator)
ab
ab ✔
For Concat/Concatenate the swallowed tail still starts and ends with ", so the literal branch strips the outer quotes and emits the formula's own source text as data — silently wrong output, no error. For MessageText the tail is re-evaluated as a PowerFx fragment (="hi") & Upper("there"), producing a spurious compilation error for a formula that is valid PowerFx.
A related consequence: because the alias only applies when Concat( is at position 0, Concat resolves to two different functions depending on where it sits in the formula. =Concat("a") is intercepted as the Copilot Studio string alias, while =Lower("B") & Concat("a") reaches real PowerFx and fails with Invalid number of arguments: received 1, expected 2-3 — the table-aggregator signature.
What I expected: a custom-function regex to match only when the call spans the entire formula, so that a formula merely beginning with Concat(/Concatenate(/UserMessage(/AgentMessage(/MessageText( falls through to PowerFx instead of being mis-parsed.
What happens instead: the tail after the call's closing paren is absorbed into the arguments, yielding either formula source text as output or a compilation error.
Steps to reproduce
Install agent-framework-declarative with a working PowerFx engine (dotnet + powerfx).
Define a declarative workflow whose SendActivity.activity.text is a formula that starts with one of the five custom-function names and continues with any operator, e.g. =Concat(Local.First, " ") & Upper(Local.Last).
A matching-paren scan (or requiring the capture to be balanced before accepting the match) would confine each handler to formulas that are entirely one custom call, which is what the surrounding _preprocess_custom_functions path already assumes for the nested case (Upper(MessageText(...)) works today).
Separately, _powerfx_functions.concat_text implements the standard PowerFx table Concat(table, expression, separator) but is registered nowhere — CUSTOM_FUNCTIONS["Concat"] points at the varargs string concat_strings, and concat_text is referenced only from tests. Worth deciding whether the table form should be reachable at all, given the positional split described above.
No model or network access is needed for this reproduction.
changed the title [-]Python: [Bug]: Declarative custom-function regexes swallow the formula tail, silently emitting formula source or failing compilation[/-][+].NET: Python: [Bug]: Declarative custom-function regexes swallow the formula tail, silently emitting formula source or failing compilation[/+]on Oct 7, 2026
The greedy (.+)\)$ capture is the whole story: anything after the call's own closing paren is absorbed into the argument list, and since eval() consults _eval_custom_function before PowerFx, the mis-parse always wins.
I'd like to take this. Proposed fix: replace the four end-anchored regexes with one shared matcher that finds the call's own closing paren via a balanced scan (strings and nested parens respected) and accepts the match only when that paren is the last non-space character of the formula. Formulas that merely begin with Concat( / MessageText( / etc. then fall through to PowerFx, which both fixes the swallowed tail and resolves the positional duality you flagged: mid-formula Concat keeps its engine meaning. Regression coverage would be your four cases plus Lower("B") & Concat("a") and a string literal containing ").
One scoping question: your third bullet (the unregistered table-form concat_text) reads as a separate design decision. I'd leave it out of this fix unless you'd rather settle it at the same time.
Description
DeclarativeWorkflowState._eval_custom_functionmatches its custom-function dialect with regexes anchored to the end of the whole formula:(.+)is greedy and\)$only requires the formula to end with some)— not that the paren closes the call being matched. So any formula that starts with one of these names and ends with)is captured whole, and everything after the call's own closing paren is swallowed into the argument list.eval()consults_eval_custom_functionbefore PowerFx and returns on any non-Noneresult, so the mis-parse wins and the formula never reaches the engine that would have handled it correctly.Two symptoms follow, depending on what the swallowed tail looks like:
Concat("a") & Lower("B")aba") & Lower("BConcatenate("x") & Upper("y")xYx") & Upper("yMessageText("hi") & Upper("there")hiTHEREValueError: Power Fx failed compilation: Error 4 - 5 : Unexpected characters.Concat("a", "b")(control, no trailing operator)abab✔For
Concat/Concatenatethe swallowed tail still starts and ends with", so the literal branch strips the outer quotes and emits the formula's own source text as data — silently wrong output, no error. ForMessageTextthe tail is re-evaluated as a PowerFx fragment (="hi") & Upper("there"), producing a spurious compilation error for a formula that is valid PowerFx.A related consequence: because the alias only applies when
Concat(is at position 0,Concatresolves to two different functions depending on where it sits in the formula.=Concat("a")is intercepted as the Copilot Studio string alias, while=Lower("B") & Concat("a")reaches real PowerFx and fails withInvalid number of arguments: received 1, expected 2-3— the table-aggregator signature.What I expected: a custom-function regex to match only when the call spans the entire formula, so that a formula merely beginning with
Concat(/Concatenate(/UserMessage(/AgentMessage(/MessageText(falls through to PowerFx instead of being mis-parsed.What happens instead: the tail after the call's closing paren is absorbed into the arguments, yielding either formula source text as output or a compilation error.
Steps to reproduce
agent-framework-declarativewith a working PowerFx engine (dotnet+powerfx).SendActivity.activity.textis a formula that starts with one of the five custom-function names and continues with any operator, e.g.=Concat(Local.First, " ") & Upper(Local.Last).Code Sample
Output:
Error Messages / Stack Traces
The
Concat/Concatenatecases raise nothing — the workflow emits incorrect text. TheMessageTextcase raises:Package Versions
Source checkout
40763bed1a5e94a1698ba9ebf009e5622eb099aa: agent-framework-core 1.20.0, agent-framework-declarative 1.2.0, powerfx 0.0.34, dotnet 10.0.303.Python Version
Python 3.13.2 on Windows x64.
Additional Context
arg.startswith('"') and arg.endswith('"')) inside an otherwise correctly-delimited call. This report is about the outer regex selecting the wrong extent of the formula in the first place; the control rowConcat("a", "b")evaluates correctly, so argument handling is not what fails here. The two interact: once the extent is fixed, Python: Declarative concatenation treats quoted expressions as literal text #9072's fix governs the arguments; while the extent is wrong, a Python: Declarative concatenation treats quoted expressions as literal text #9072 fix would change these cases from literal garbage to evaluating a malformed fragment rather than making them correct._preprocess_custom_functionspath already assumes for the nested case (Upper(MessageText(...))works today)._powerfx_functions.concat_textimplements the standard PowerFx tableConcat(table, expression, separator)but is registered nowhere —CUSTOM_FUNCTIONS["Concat"]points at the varargs stringconcat_strings, andconcat_textis referenced only from tests. Worth deciding whether the table form should be reachable at all, given the positional split described above.