Description
InvokeAzureAgentExecutor (the executor backing the declarative
InvokeAzureAgent action) accepts a .NET-style input.arguments mapping,
as documented in its own class docstring and used by the repo's own
customer_support sample (ticket_agent, escalate_agent, resolution_agent
actions in
python/samples/getting_started/workflows/declarative/customer_support/workflow.yaml,
which invoke TicketingAgent / TicketEscalationAgent / TicketResolutionAgent
with only input.arguments, no input.messages).
In _build_input_text()
(agent_framework_declarative/_workflows/_executors_agents.py), every entry
of input.arguments is evaluated into a local evaluated_args dict — and
then never used again:
async def _build_input_text(self, state, arguments, messages_expr) -> str:
# Evaluate arguments
evaluated_args: dict[str, Any] = {}
for key, value in arguments.items():
evaluated_args[key] = state.eval_if_expression(value)
# Evaluate messages/input
if messages_expr:
...
# evaluated_args is not read from this point on
...
return input_text if input_text else ""
evaluated_args is dropped: it isn't concatenated into the returned text,
and isn't stored anywhere in workflow state. Note that options/run_kwargs
is not a viable channel for it either — that dict is populated from the
kwargs passed to workflow.run() (via WORKFLOW_RUN_KWARGS_KEY) so that
tools can see them; it has nothing to do with input.arguments, and
SupportsAgentRun.run() has no separate "arguments" parameter at all, only
messages. So messages and arguments are two distinct inputs on the
declarative-action side (_build_input_text(state, arguments, messages_expr)),
but only one channel — the returned text, which becomes the message sent to
agent.run() — actually reaches the agent. The fix has to fold
evaluated_args into that returned text (the way the existing
Workflow.Inputs fallback already formats multiple values as key: value
lines), not thread them through options. As a result, any
InvokeAzureAgent action that relies on input.arguments without also
specifying input.messages sends the agent no information about those
arguments at all — in the reproduction below, the agent is invoked with an
empty string.
The equivalent .NET implementation
(InvokeAzureAgentExecutor.cs, InvokeAgentAsync, lines 63-64) evaluates the
structured inputs via GetStructuredInputs() into inputParameters and
threads them into agentProvider.InvokeAgentAsync(...), so this is a
Python-only regression relative to .NET feature parity, not an intentional
behavior difference.
The same "evaluate into evaluated_args, then never use it" pattern also
exists in the older interpreter-based action handler,
_actions_agents.py::_run_invoke_azure_agent (not currently wired into
WorkflowFactory, but shipped in the package) — so this isn't isolated to
one code path.
Expected behavior: the evaluated input.arguments values are included in
(or otherwise made available to) the input sent to the agent, matching the
documented schema and the .NET behavior.
Actual behavior: input.arguments are evaluated and silently discarded;
the agent receives none of that data.
Steps to reproduce:
- Define a declarative workflow with an
InvokeAzureAgent action that uses
input.arguments and no input.messages (see the workflow definition
below).
- Run the workflow with
WorkflowFactory, using an agent that records what
it's asked to run.
- Observe that the agent receives an empty string instead of the arguments.
A minimal, self-contained repro (using a stub agent, no Azure credentials
required) is included below.
Code Sample
Workflow definition:
kind: Workflow
trigger:
kind: OnConversationStart
id: main
actions:
- kind: InvokeAzureAgent
id: ticket_agent
agent:
name: TicketingAgent
input:
arguments:
IssueDescription: "The printer on the 3rd floor is jammed."
AttemptedResolutionSteps: "Restarted the printer twice."
output:
resultProperty: Local.AgentReply
Reproduction script:
import asyncio
from pathlib import Path
from typing import Any
from agent_framework.declarative import WorkflowFactory
class RecordingAgent:
"""Duck-typed stand-in for a real agent (see `SupportsAgentRun` protocol).
Just records what it was asked to run instead of calling a model.
"""
def __init__(self, name: str) -> None:
self.id = f"{name}-001"
self.name = name
self.description = "Records the input it receives; does not call a model."
self.received_input: Any = "<agent.run() was never called>"
async def run(self, received_input: Any = None, *, options: Any = None, **kwargs: Any) -> Any:
self.received_input = received_input
class _Result:
text = "(stub response)"
return _Result()
def get_new_thread(self, **kwargs: Any) -> dict[str, Any]:
return {"id": "stub-thread", "messages": []}
async def main() -> None:
agent = RecordingAgent("TicketingAgent")
factory = WorkflowFactory(agents={"TicketingAgent": agent})
workflow = factory.create_workflow_from_yaml_path(Path(__file__).parent / "workflow.yaml")
async for _event in workflow.run({}, stream=True):
pass
defined_arguments = {
"IssueDescription": "The printer on the 3rd floor is jammed.",
"AttemptedResolutionSteps": "Restarted the printer twice.",
}
agent_was_invoked = agent.received_input != "<agent.run() was never called>"
received_text = str(agent.received_input).lower()
missing_arguments = [
name for name, value in defined_arguments.items() if value.lower() not in received_text
]
print("input.arguments defined in workflow.yaml:")
print(f" {defined_arguments}")
print()
print("What TicketingAgent.run() actually received:")
print(f" {agent.received_input!r}")
print()
if not agent_was_invoked:
print("BUG: TicketingAgent.run() was never even called.")
elif missing_arguments:
print(
"BUG CONFIRMED: TicketingAgent.run() WAS called (the workflow -> agent "
"wiring works), but what it received does not include the values from "
f"input.arguments: {missing_arguments}. _build_input_text() evaluates "
"them into `evaluated_args` and then never reads that variable again, "
"so the arguments are silently dropped before the agent is invoked."
)
else:
print("OK: input.arguments made it to the agent. The bug appears fixed.")
if __name__ == "__main__":
asyncio.run(main())
Error Messages / Stack Traces
None — this is a silent data-loss bug, not an exception. Running the script
above prints:
input.arguments defined in workflow.yaml:
{'IssueDescription': 'The printer on the 3rd floor is jammed.', 'AttemptedResolutionSteps': 'Restarted the printer twice.'}
What TicketingAgent.run() actually received:
''
BUG CONFIRMED: TicketingAgent.run() WAS called (the workflow -> agent wiring works), but what it received does not include the values from input.arguments: ['IssueDescription', 'AttemptedResolutionSteps']. _build_input_text() evaluates them into `evaluated_args` and then never reads that variable again, so the arguments are silently dropped before the agent is invoked.
i.e. `TicketingAgent.run()` is called (the workflow → agent wiring works),
but what it receives is `''` instead of something that reflects
`IssueDescription` / `AttemptedResolutionSteps`.
Package Versions
agent-framework-declarative: 1.0.3, agent-framework: 1.15.0, agent-framework-core: 1.15.0 (also reproduces on agent-framework-declarative 1.0.0rc2 / agent-framework 1.11.0 / agent-framework-core 1.11.0, and on microsoft/agent-framework's main branch as of 2026-08-19)
Python Version
Python 3.13.1
Additional Context
Workaround: pass the same values via input.messages instead of
input.arguments (e.g. compose a single message string yourself), or set
Local.input / Local.userInput directly before the action runs.
Description
InvokeAzureAgentExecutor(the executor backing the declarativeInvokeAzureAgentaction) accepts a.NET-styleinput.argumentsmapping,as documented in its own class docstring and used by the repo's own
customer_supportsample (ticket_agent,escalate_agent,resolution_agentactions in
python/samples/getting_started/workflows/declarative/customer_support/workflow.yaml,which invoke
TicketingAgent/TicketEscalationAgent/TicketResolutionAgentwith only
input.arguments, noinput.messages).In
_build_input_text()(
agent_framework_declarative/_workflows/_executors_agents.py), every entryof
input.argumentsis evaluated into a localevaluated_argsdict — andthen never used again:
evaluated_argsis dropped: it isn't concatenated into the returned text,and isn't stored anywhere in workflow state. Note that
options/run_kwargsis not a viable channel for it either — that dict is populated from the
kwargs passed to
workflow.run()(viaWORKFLOW_RUN_KWARGS_KEY) so thattools can see them; it has nothing to do with
input.arguments, andSupportsAgentRun.run()has no separate "arguments" parameter at all, onlymessages. Somessagesandargumentsare two distinct inputs on thedeclarative-action side (
_build_input_text(state, arguments, messages_expr)),but only one channel — the returned text, which becomes the message sent to
agent.run()— actually reaches the agent. The fix has to foldevaluated_argsinto that returned text (the way the existingWorkflow.Inputsfallback already formats multiple values askey: valuelines), not thread them through
options. As a result, anyInvokeAzureAgentaction that relies oninput.argumentswithout alsospecifying
input.messagessends the agent no information about thosearguments at all — in the reproduction below, the agent is invoked with an
empty string.
The equivalent .NET implementation
(
InvokeAzureAgentExecutor.cs,InvokeAgentAsync, lines 63-64) evaluates thestructured inputs via
GetStructuredInputs()intoinputParametersandthreads them into
agentProvider.InvokeAgentAsync(...), so this is aPython-only regression relative to .NET feature parity, not an intentional
behavior difference.
The same "evaluate into
evaluated_args, then never use it" pattern alsoexists in the older interpreter-based action handler,
_actions_agents.py::_run_invoke_azure_agent(not currently wired intoWorkflowFactory, but shipped in the package) — so this isn't isolated toone code path.
Expected behavior: the evaluated
input.argumentsvalues are included in(or otherwise made available to) the input sent to the agent, matching the
documented schema and the .NET behavior.
Actual behavior:
input.argumentsare evaluated and silently discarded;the agent receives none of that data.
Steps to reproduce:
InvokeAzureAgentaction that usesinput.argumentsand noinput.messages(see the workflow definitionbelow).
WorkflowFactory, using an agent that records whatit's asked to run.
A minimal, self-contained repro (using a stub agent, no Azure credentials
required) is included below.
Code Sample
Workflow definition: kind: Workflow trigger: kind: OnConversationStart id: main actions: - kind: InvokeAzureAgent id: ticket_agent agent: name: TicketingAgent input: arguments: IssueDescription: "The printer on the 3rd floor is jammed." AttemptedResolutionSteps: "Restarted the printer twice." output: resultProperty: Local.AgentReply Reproduction script: import asyncio from pathlib import Path from typing import Any from agent_framework.declarative import WorkflowFactory class RecordingAgent: """Duck-typed stand-in for a real agent (see `SupportsAgentRun` protocol). Just records what it was asked to run instead of calling a model. """ def __init__(self, name: str) -> None: self.id = f"{name}-001" self.name = name self.description = "Records the input it receives; does not call a model." self.received_input: Any = "<agent.run() was never called>" async def run(self, received_input: Any = None, *, options: Any = None, **kwargs: Any) -> Any: self.received_input = received_input class _Result: text = "(stub response)" return _Result() def get_new_thread(self, **kwargs: Any) -> dict[str, Any]: return {"id": "stub-thread", "messages": []} async def main() -> None: agent = RecordingAgent("TicketingAgent") factory = WorkflowFactory(agents={"TicketingAgent": agent}) workflow = factory.create_workflow_from_yaml_path(Path(__file__).parent / "workflow.yaml") async for _event in workflow.run({}, stream=True): pass defined_arguments = { "IssueDescription": "The printer on the 3rd floor is jammed.", "AttemptedResolutionSteps": "Restarted the printer twice.", } agent_was_invoked = agent.received_input != "<agent.run() was never called>" received_text = str(agent.received_input).lower() missing_arguments = [ name for name, value in defined_arguments.items() if value.lower() not in received_text ] print("input.arguments defined in workflow.yaml:") print(f" {defined_arguments}") print() print("What TicketingAgent.run() actually received:") print(f" {agent.received_input!r}") print() if not agent_was_invoked: print("BUG: TicketingAgent.run() was never even called.") elif missing_arguments: print( "BUG CONFIRMED: TicketingAgent.run() WAS called (the workflow -> agent " "wiring works), but what it received does not include the values from " f"input.arguments: {missing_arguments}. _build_input_text() evaluates " "them into `evaluated_args` and then never reads that variable again, " "so the arguments are silently dropped before the agent is invoked." ) else: print("OK: input.arguments made it to the agent. The bug appears fixed.") if __name__ == "__main__": asyncio.run(main())Error Messages / Stack Traces
None — this is a silent data-loss bug, not an exception. Running the script above prints: input.arguments defined in workflow.yaml: {'IssueDescription': 'The printer on the 3rd floor is jammed.', 'AttemptedResolutionSteps': 'Restarted the printer twice.'} What TicketingAgent.run() actually received: '' BUG CONFIRMED: TicketingAgent.run() WAS called (the workflow -> agent wiring works), but what it received does not include the values from input.arguments: ['IssueDescription', 'AttemptedResolutionSteps']. _build_input_text() evaluates them into `evaluated_args` and then never reads that variable again, so the arguments are silently dropped before the agent is invoked. i.e. `TicketingAgent.run()` is called (the workflow → agent wiring works), but what it receives is `''` instead of something that reflects `IssueDescription` / `AttemptedResolutionSteps`.Package Versions
agent-framework-declarative: 1.0.3, agent-framework: 1.15.0, agent-framework-core: 1.15.0 (also reproduces on agent-framework-declarative 1.0.0rc2 / agent-framework 1.11.0 / agent-framework-core 1.11.0, and on
microsoft/agent-framework'smainbranch as of 2026-08-19)Python Version
Python 3.13.1
Additional Context
Workaround: pass the same values via
input.messagesinstead ofinput.arguments(e.g. compose a single message string yourself), or setLocal.input/Local.userInputdirectly before the action runs.