Skip to content

Python: fix(declarative): treat omitted workflow inputs as blank in PowerFx - #9730

Draft
Naveen Chatlapalli (1aifanatic) wants to merge 3 commits into
microsoft:mainfrom
1aifanatic:contrib/python-declarative-missing-inputs-blank
Draft

Naveen Chatlapalli (1aifanatic) wants to merge 3 commits into
microsoft:mainfrom
1aifanatic:contrib/python-declarative-missing-inputs-blank

Conversation

@1aifanatic

Copy link
Copy Markdown
Contributor

Motivation & Context

=If(IsBlank(inputs.name), "World", inputs.name) never applies its default when the caller omits name, so the simple_workflow sample prints Hello, ! for workflow.run({}). PowerFx fails to compile inputs.name when the inputs record has no such field, and DeclarativeWorkflowState.eval() returns None for the whole expression. Details and an input matrix are in #9729.

Description & Review Guide

  • What are the major changes?
    • _bind_missing_input_references() finds inputs.<name> / Workflow.Inputs.<name> references in the formula. Any input the caller did not supply is bound to blank (None) in that evaluation's PowerFx symbols, for both aliases. Workflow state is not modified, and other undefined names keep the existing None behavior. The regex follows the existing _ENV_REFERENCE_RE approach for Env.<name>.
    • New parametrized test test_if_with_isblank_when_input_is_missing, next to test_if_with_isblank, which only covered {"name": ""}. It checks both aliases and that the missing key is not written to state.
    • The simple_workflow sample now prints the outputs of its second ("Alice") run, which were discarded.
  • What is the impact of these changes? An omitted input now evaluates the same as an input passed as None. Expressions that previously evaluated to None only because an input was absent now evaluate normally with that input blank.
  • What do you want reviewers to focus on? Whether blank is the semantics you want for omitted inputs. An alternative is binding only inputs declared in the workflow's inputs: schema, but simple_workflow does not declare its inputs.

Verification, with the .NET 10 runtime for PowerFx:

main this PR
new test (both aliases) assert None == 'World' passes
simple_workflow sample Hello, !, then no output for Alice Hello, World!, Hello, Alice!
packages/declarative/tests all pass

Pyright on _declarative_base.py reports 0 errors. Mypy reports the same 5 errors before and after (none on the changed lines). ruff check and ruff format --check pass.

Related Issue

Fixes #9729

AI Assistance

  • No material AI assistance was used.
  • This is an AI-assisted contribution. I reviewed, understood, and verified all submitted content and accept responsibility for it.

AI assistance details: Claude Code helped find the bug by running the declarative samples locally, trace it to DeclarativeWorkflowState.eval(), implement the fix and test, and draft this description and the linked issue.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR links to an agreed issue with no competing open PR, or the Related Issue section documents a trivial-change or repository-automation exception.
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

🤖 Generated with Claude Code

…owerFx

When a caller omits an input that a formula references, PowerFx fails to
compile inputs.<name> / Workflow.Inputs.<name> ("Name isn't valid") and
eval() returns None for the whole expression. That breaks the documented
default pattern =If(IsBlank(inputs.name), "World", inputs.name): the
simple_workflow sample printed "Hello, !" for workflow.run({}).

Bind referenced-but-missing inputs to blank for that evaluation only, the
same result as passing the input as None, without writing them to state.
Also print the simple_workflow sample's second run, which was discarded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 26, 2026 18:36
@agent-framework-automation agent-framework-automation Bot added the python Usage: [Issues, PRs], Target: Python label Oct 26, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Reference discovery matches literal and comment text while missing valid quoted and whitespace-separated references.

2 open issues
What changed in this PR

This PR addresses #9729 by binding omitted workflow inputs as blank during PowerFx evaluation, allowing default expressions to run without modifying workflow state.

Changes:

  • Adds evaluation-only blank bindings for references through both input aliases.
  • Tests omitted-input defaults and verifies state remains unchanged.
  • Prints the custom-name run’s outputs in the simple workflow sample.
File Description
python/​samples/​03-workflows/​declarative/​simple_workflow/​main.py Prints outputs from the “Alice” run.
python/​packages/​declarative/​tests/​test_powerfx_yaml_compatibility.py Adds missing-input regression tests for both aliases.
python/​packages/​declarative/​agent_framework_declarative/​_workflows/​_declarative_base.py Discovers missing input references and binds evaluation-only blanks.

Review effort: Balanced

Skip comments and string literals when looking for inputs.<name> and
Workflow.Inputs.<name>, and accept single-quoted field names and
whitespace or comments around the dots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
if member is None or member[0] != "Inputs":
continue
after_root = member[1]
elif root_name != "inputs":

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naveen Chatlapalli (@1aifanatic) This can treat a member name or locally bound inputs as the global workflow alias. After state.initialize({"other": 1}) and state.set("Local.inputs", {"name": "local"}), =If(Local . inputs . name = "local", inputs, inputs) returns {"other": 1.0, "name": None} instead of the base result {"other": 1.0}.

With({inputs: {name: "local"}}, {Chosen: inputs.name, Received: Workflow.Inputs}) likewise adds a global blank field solely from its local reference. Please distinguish actual global aliases from member names and local bindings before adding missing fields.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — both reproduce. I changed the approach so this can't affect formulas that already work:

  • The formula is evaluated with the unmodified symbols first. Blank inputs are only bound when that fails with PowerFx's unrecognized-name error (the case that used to return None), and then the formula is retried once. If(Local . inputs . name = "local", inputs, inputs) and the With({inputs: ...}, {Chosen: inputs.name, Received: Workflow.Inputs}) example both compile against the real symbols, so they now return exactly the base result ({"other": 1} and {"Chosen": "local", "Received": {"other": 1}}).
  • Reference detection now reads whole member-access chains, so inputs after a dot (including Local . inputs and Local./* c */inputs) is a member name, not a root.

Both of your cases, plus the comment-separated variant, are in test_member_names_and_local_bindings_are_not_workflow_inputs; they fail on the previous commit and pass now.

if not missing:
return symbols
bound_inputs: dict[str, Any] = {**input_values, **dict.fromkeys(sorted(missing))}
return {**symbols, "Workflow": {**symbols["Workflow"], "Inputs": bound_inputs}, "inputs": bound_inputs}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naveen Chatlapalli (@1aifanatic) The inputs symbol can be a custom namespace rather than the canonical input record. Replacing both bindings from it loses supplied values: after state.initialize({"name": "Alice"}) and state.set("inputs.other", 1), state.eval("=Workflow.Inputs.name") now returns None; base returns "Alice".

The public YAML path also reproduces this: SetValue at inputs.other, followed by SendActivity reading Workflow.Inputs.name, loses the message. Please preserve independently resolved records and synchronize aliases only when they still refer to the same canonical input namespace.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. inputs and Workflow.Inputs are now kept separate when a custom inputs namespace exists: only Workflow.Inputs.<name> references complete the workflow record, and the custom namespace is passed through unchanged. They're only synchronized when inputs is still the canonical alias. Combined with the retry-on-name-error change, state.eval("=Workflow.Inputs.name") after state.set("inputs.other", 1) returns "Alice" again.

Tests: test_custom_inputs_namespace_keeps_workflow_inputs covers the state-level case, and test_custom_inputs_namespace_does_not_hide_workflow_inputs runs your YAML (SetValue at inputs.other, then SendActivity with =Workflow.Inputs.name) and checks the message is sent. Both fail on the previous commit. The full declarative suite passes (1684 tests), and pyright is clean.

Formulas that already compile keep their base result, so member names and
local bindings called inputs are unaffected. A custom inputs namespace is
no longer replaced by the workflow inputs record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch was previously deployed

1 inactive deployment
github-app-auth — df27c137 Deployed Nov 4, 2026 by 1aifanatic via add_label #27039
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

.NET: Python: [Bug]: Declarative workflow IsBlank defaults ignore inputs the caller omitted

3 participants