Repository navigation
Description
Description
In declarative workflows, a PowerFx default for an input the caller did not pass never takes effect. The simple_workflow sample sets
value: =If(IsBlank(inputs.name), "World", inputs.name)but workflow.run({}) prints Hello, ! instead of Hello, World!.
The cause: when the inputs record has no name field, PowerFx fails to compile inputs.name (and Workflow.Inputs.name) with Name isn't valid. 'name' isn't recognized. DeclarativeWorkflowState.eval() treats that as an undefined variable and returns None for the whole expression, so the IsBlank default is never evaluated. The input only counts as blank when it is present with a blank value:
workflow.run(...) |
output |
|---|---|
{} |
Hello, ! |
{"other": 1} |
Hello, ! |
{"name": None} |
Hello, World! |
{"name": ""} |
Hello, World! |
{"name": "Alice"} |
Hello, Alice! |
invoke_function_tool/main.py works around this with the comment "both location and unit must be provided". The existing test test_if_with_isblank avoids the case by initializing with {"name": ""}.
Separately, the simple_workflow sample discards the result of its second ("Alice") run, so it prints nothing for that run.
Reproduction
With the .NET runtime available for PowerFx, run python/samples/03-workflows/declarative/simple_workflow/main.py, or:
state = DeclarativeWorkflowState(state_store)
state.initialize({"other": 1})
state.eval('=If(IsBlank(inputs.name), "World", inputs.name)') # None, expected "World"Expected behavior
An input the workflow references but the caller omitted behaves as blank, the same as passing it as None, so IsBlank-based defaults apply.
Proposed fix
In DeclarativeWorkflowState.eval(), find inputs.<name> / Workflow.Inputs.<name> references in the formula. Bind any the caller did not supply to blank in the PowerFx symbols for that evaluation only, without writing them to workflow state. Other undefined names keep today's None behavior. I have a PR ready, with a regression test that fails on main.
Package Versions
agent-framework-declarative from main at 1168c00, powerfx with .NET 10.0.12 runtime.
Python Version
3.13
AI Assistance
Claude Code helped find this by running the declarative samples locally, trace it to DeclarativeWorkflowState.eval(), and draft this report. I verified the behavior locally.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsNo status
Activity
[-]Python: [Bug]: Declarative workflow IsBlank defaults ignore inputs the caller omitted[/-][+].NET: Python: [Bug]: Declarative workflow IsBlank defaults ignore inputs the caller omitted[/+]eavanvalkenburg commentedon Nov 2, 2026
Thanks for the reproduction. The team will consider owning omitted-input handling that lets declarative workflows apply useful blank/default expressions. This changes the currently documented legacy undefined-reference fallback, so the exact input-binding and compatibility contract must be agreed first. This is consideration, not an implementation or timing commitment. We will use existing #9730 and the namespace/lexical-scope review as planning input rather than start competing work.
eavanvalkenburg commentedon Nov 2, 2026
/plan Plan the accepted core ownership of omitted-input blank/default binding, using existing #9730 and the current maintainer scope/alias findings. Reconcile this with eval documented legacy None-on-undefined-reference behavior: decide declared optional inputs versus referenced-but-undeclared inputs, missing/null/empty distinctions and compatibility for unrelated undefined names. Prefer supported engine/schema binding information over maintaining an approximate PowerFx parser. Preserve true global aliases versus With-local bindings/member names, independently resolved inputs and Workflow.Inputs records, quoted identifiers/comments/interpolated code, and no workflow-state mutation. Cover the two existing review regressions through native PowerFx and the public YAML path, plus missing/null/empty/provided values and unchanged namespace behavior. Do not blanket-rebind both aliases from one record. Keep the simple sample second-run output omission separate from the expression-policy change. Scope is the Python declarative wrapper; the .NET runtime dependency alone does not establish a .NET SDK bug. This is not minor-ready: retain both triage labels, stay unassigned and settle semantics before implementation approval.