I Tried Microsoft Agent Framework’s New Declarative Workflows (1.0) — Here’s Where It Actually Broke
Microsoft just pushed Agent Framework’s declarative workflows to 1.0 across both the Python and .NET SDKs. The pitch in the announcement is simple and honestly pretty appealing: stop wiring multi-agent orchestration in code, describe it in YAML instead, and let the framework turn that YAML into a normal Workflow object you run like any other.
I didn’t want to just rephrase the blog post. So I actually pip install-ed the package, wrote a couple of workflows, and tried to run them in a clean Linux sandbox. It did not go the way the "five-minute quickstart" implies. I hit a very specific, very undocumented wall about ninety seconds in, and once I understood why, it changed how I read the rest of the announcement.
This is the write-up of that process: what worked immediately, what silently depends on a runtime nobody mentions in the prerequisites, and what I’d actually tell a team evaluating this for production.
What declarative workflows are, in one paragraph
Instead of writing Python or C# code that calls agents in sequence, checks conditions, and routes to the next step, you write a YAML file. Each step is an “action” (SetVariable, If, InvokeAzureAgent, Foreach, and so on). The framework parses that YAML and builds a real Workflow graph out of it — the same execution engine that powers code-first workflows, complete with streaming, checkpointing, and human-in-the-loop pauses. Product folks can edit the YAML; you don't have to touch Python for a routing change.
That’s a genuinely useful idea. The question I wanted answered was: how solid is the 1.0 in practice?
Step 1: the install looks normal, until you actually read the output
pip install agent-framework-declarative --pre
This is the exact command from the Microsoft Learn prerequisites page. It pulled in agent-framework-core, httpx, pyyaml, and two packages I did not expect for what's advertised as a Python library: pythonnet and clr_loader.
If those names don’t ring a bell — they’re the standard bridge for calling .NET assemblies from Python (CoreCLR hosting via pythonnet). A "pure Python" declarative workflow package was quietly pulling in a CLR bridge. I didn't think much of it until the very next command.
The moment I imported anything from the package, this printed to stdout, completely unprompted:
Patch applied successfully
✓ Applied clr_loader patch for .NET 10+ compatibility
That’s debug output baked into a dependency’s module-level code (powerfx/_loader.py, which agent-framework-declarative requires), firing on every single import, in every environment, whether or not you ever touch an expression. It's monkey-patching clr_loader's DotnetCoreRuntimeSpec because the original version-parsing logic breaks on double-digit .NET major versions ("10.0.0" was being sliced into "10..0"). Harmless, but it's the kind of thing that makes you go "wait, why does this package need a CoreCLR runtime patch at all?"
I kept going.
Step 2: the wall
I followed the docs’ own “Your First Declarative Workflow” example almost verbatim:
# greeting-workflow.yaml
name: greeting-workflow
description: A simple workflow that greets the user
inputs:
name:
type: string
description: The name of the person to greet
actions:
- kind: SetVariable
id: set_greeting
displayName: Set greeting prefix
variable: Local.greeting
value: Hello
- kind: SetVariable
id: build_message
displayName: Build greeting message
variable: Local.message
value: =Concat(Local.greeting, ", ", Workflow.Inputs.name, "!")
- kind: SendActivity
id: send_greeting
displayName: Send greeting to user
activity:
text: =Local.message
- kind: SetVariable
id: set_output
displayName: Store result in outputs
variable: Workflow.Outputs.greeting
value: =Local.message
# run.py
import asyncio
from pathlib import Path
from agent_framework.declarative import WorkflowFactory
async def main() -> None:
factory = WorkflowFactory()
workflow = factory.create_workflow_from_yaml_path(Path( __file__ ).parent / "greeting-workflow.yaml")
print(f"Loaded workflow: {workflow.name}")
result = await workflow.run({"name": "Ali"})
for output in result.get_outputs():
print(f"Output: {output}")
asyncio.run(main())
Loading the workflow worked fine. Running it did not:
RuntimeError: PowerFx is not available (dotnet runtime not installed).
Expression '=Local.greeting' cannot be evaluated. Install dotnet and the
powerfx package for full PowerFx support.
Every value in that workflow that starts with = is a PowerFx expression — the expression language declarative workflows use for state and conditions. And PowerFx here isn't a Python reimplementation; the powerfx package literally describes itself in its own metadata as a "Power Fx python bridge to invoke c# implementation." It's the real .NET Power Fx engine, loaded through pythonnet, over CoreCLR.
Here’s the part that actually annoyed me: the Python “Prerequisites” section on Microsoft Learn lists exactly two requirements — Python 3.10–3.13, and pip install agent-framework-declarative --pre. No .NET runtime anywhere on that list. You find out about the hidden dependency the hard way, at runtime, the first time you write anything more interesting than a static string.
I want to be fair about what I could and couldn’t verify here. In my sandbox I don’t have root and outbound access to dotnet.microsoft.com / dot.net / builds.dotnet.microsoft.com is blocked, so I genuinely could not install a .NET runtime to get past this and confirm the happy path end-to-end. What I could confirm is that Ubuntu's own apt repositories carry dotnet-sdk-8.0 out of the box (apt-cache search dotnet-sdk found it immediately), so on a normal dev machine or CI image with sudo, this is a one-line fix:
# Debian/Ubuntu
sudo apt-get update && sudo apt-get install -y dotnet-sdk-8.0
# or the official cross-platform installer
curl -sSL https://dot.net/v1/dotnet-install.sh | bash -s -- --channel 8.0
If you’re building this into a container image, the cleanest path is honestly to just start from Microsoft’s own SDK base image and add Python on top, rather than the other way around:
FROM mcr.microsoft.com/dotnet/sdk:8.0
RUN apt-get update && apt-get install -y python3 python3-pip \
&& pip3 install --break-system-packages agent-framework-declarative
WORKDIR /app
COPY . .
CMD ["python3", "run.py"]
That’s a completely free, local, self-hosted way to satisfy the dependency — no Azure subscription required for this part. It’s just not something the “pip install and go” framing prepares you for.
Step 3: what actually works with zero dependencies
Out of curiosity, I stripped every = expression out of the workflow to see how much you can do with pure literals:
# literal_only.yaml
name: literal-only-workflow
description: No PowerFx expressions at all, just literals
actions:
- kind: SetVariable
id: set_greeting
variable: Local.greeting
value: Hello there
- kind: SendActivity
id: send_greeting
activity:
text: Hello there, static message only
$ python3 run_literal.py
Loaded workflow: literal-only-workflow
Output: Hello there, static message only
This ran cleanly, no .NET, no errors. Which tells you something useful: SetVariable, SendActivity, and the workflow graph machinery itself don't need PowerFx. But this is basically a party trick — the second you need Concat, If, a comparison operator, IsBlank, or to reference Workflow.Inputs.anything, you're back to needing the .NET runtime. And since conditions and dynamic values are the entire reason you'd reach for a workflow engine over a static script, in practice "no PowerFx" isn't a real deployment option for anything beyond a demo.
The action vocabulary, condensed
Once you get past the runtime requirement, the action set itself is genuinely broad. Here’s the reference table from the docs, reformatted so you can paste it somewhere useful:
ACTION CATEGORY PY C# NOTES
------------------------ -------------------- --- --- --------------------------------
SetVariable Variable yes yes single value, literal or =expr
SetMultipleVariables Variable yes yes map of path -> value
ResetVariable Variable yes yes clears a variable
ClearAllVariables Variable - yes C# only
ParseValue Variable - yes C# only
EditTableV2 Variable - yes C# only
If Control Flow yes yes condition / then / else
ConditionGroup Control Flow yes yes switch-like, first match wins
Foreach Control Flow yes yes itemName / indexName
BreakLoop / ContinueLoop Control Flow yes yes standard loop control
GotoAction Control Flow yes yes jump to an action id
SendActivity Output yes yes message to the user
InvokeAzureAgent Agent yes yes calls a registered/Foundry agent
InvokeFunctionTool Tool yes yes calls a local function directly
InvokeMcpTool Tool yes yes calls an MCP server tool
HttpRequestAction HTTP yes yes GET/POST/etc, JSON auto-parsed
Question Human-in-the-Loop yes yes ask + store response
RequestExternalInput Human-in-the-Loop yes yes pause for external system
EndWorkflow / EndConv. Workflow Control yes yes terminate execution
CreateConversation Workflow Control yes yes new conversation context
AddConversationMessage Conversation - yes C# only
CopyConversationMessages Conversation - yes C# only
RetrieveConversationMsg* Conversation - yes C# only
Notice the asymmetry: conversation-thread manipulation actions exist only in C#. If your team is Python-first and wants fine-grained control over conversation history inside the YAML itself, you’re currently more limited than the .NET side.
Variable namespaces, also condensed:
NAMESPACE PYTHON C# ACCESS EXAMPLE
---------------------- ------ --- ----------- --------------------------
Local.* yes yes read/write Local.message
Workflow.Inputs.* yes - read-only Workflow.Inputs.name
Workflow.Outputs.* yes - read/write Workflow.Outputs.result
System.* yes yes read-only System.ConversationId
Agent.* yes - read-only results of agent calls
Worth flagging: C# doesn’t use Workflow.Inputs/Workflow.Outputs at all. Input arrives via System.LastMessage, output goes out via SendActivity. That's not a small stylistic difference — it means a YAML file written for the Python runtime is not portable to the .NET runtime without rewriting the input/output plumbing. "Declarative" here means declarative-per-language, not a shared, language-agnostic format. That surprised me; I'd assumed one YAML dialect for both.
A free, local alternative to Azure AI Foundry agents
Every InvokeAzureAgent example in the docs assumes a Foundry project with a deployed agent. If you just want to prototype the orchestration logic without an Azure subscription, agent-framework-core ships an Ollama integration you can register into the same WorkflowFactory the exact same way:
pip install agent-framework-ollama
from agent_framework.declarative import WorkflowFactory
from agent_framework.ollama import OllamaChatClient
# Requires a local Ollama daemon: `ollama serve` + `ollama pull llama3.2`
client = OllamaChatClient(host="http://localhost:11434", model="llama3.2")
local_agent = client.as_agent(
name="LocalAssistant",
instructions="You are a concise, helpful assistant.",
)
factory = WorkflowFactory()
factory.register_agent("AssistantAgent", local_agent)
workflow = factory.create_workflow_from_yaml_path("support_router.yaml")
# any `agent.name: AssistantAgent` action in the YAML now hits your local model
I verified the real method signatures (OllamaChatClient.__init__, .as_agent(...), WorkflowFactory.register_agent) directly against the installed package, so this is accurate to 1.0.1/1.13.0. I didn't have a running Ollama daemon in my network-restricted sandbox to do a full end-to-end call, so I can't show you real model output — but the wiring is exactly this, and it's the cheapest way to sanity-check a workflow's routing logic before you touch Foundry or pay for API calls.
The .NET side, honestly
I don’t have the .NET SDK in this environment either (no root, and the usual dotnet.microsoft.com/NuGet install domains aren't reachable from my sandbox), so I'm not going to pretend I compiled and ran the C# samples. What I can tell you, from reading the source-level docs closely:
- The YAML shape is different (kind: Workflow + trigger.actions, vs Python's name + actions), as noted above.
- DeclarativeWorkflowBuilder.Build() loads the YAML into a Workflow, same conceptual shape as Python's WorkflowFactory.
- There’s a real, specific gotcha called out in the docs for Native AOT / trimmed publishes: the default CheckpointManager.CreateJson(store) breaks under PublishAot=true because it relies on JSON reflection. You need DeclarativeWorkflowJsonOptions.Default, a source-generated JsonSerializerOptions, passed explicitly — and it's marked [Experimental("MAAI001")], so you'll eat a compiler warning unless you suppress MAAI001. If your team ships AOT-published services (which is an increasingly common .NET 8+ pattern for cold-start-sensitive workloads), this isn't optional reading.
- Under the hood, both languages sit on the same “Pregel-like” superstep execution model — executors exchange messages, supersteps run until the graph goes idle, and you can export the graph as Mermaid or Graphviz DOT for visualization/debugging. That part of the architecture is genuinely shared; it’s the declarative authoring layer on top that diverges by language.
Where this sits next to LangGraph and Semantic Kernel’s process framework
Agent Framework is Microsoft’s convergence point for AutoGen and Semantic Kernel — AutoGen is explicitly in maintenance mode now, with an official migration guide pointing people here. So this is the strategic successor, not a side experiment.
Compared to LangGraph: LangGraph’s graph-as-code model is more mature, has a much bigger third-party integration catalog, and is the safer bet if your team is Python/JS-only and doesn’t want anything Azure-shaped in the stack. Agent Framework’s declarative layer is the more natural fit if you’re already committed to Azure AI Foundry and want non-engineers editing orchestration logic without shipping code changes.
Compared to Semantic Kernel’s older process framework: this feels like the more coherent, better-documented successor — checkpointing, human-in-the-loop, and MCP tool support are first-class here in a way they weren’t consistently across SK’s various process APIs.
My honest read after actually running it: the orchestration model is solid and the action vocabulary is more complete than I expected for a 1.0. But “declarative” is doing some marketing work here — you still need to understand PowerFx syntax, still need a .NET runtime present even in the Python SDK, and the YAML isn’t portable between the two language runtimes. If you’re Azure-native already, none of that matters much. If you were hoping this would let a Python-only team avoid .NET entirely, it won’t — you’re just going to meet .NET at runtime instead of at compile time.
Should you use it?
If you’re already on Azure AI Foundry, want non-developers (PMs, support leads) to be able to tweak routing logic without a PR, and you’re fine with a .NET runtime somewhere in your deployment: yes, this is a well-built 1.0, and the checkpoint/resume story alone is worth it for long-running workflows.
If you’re a Python-only shop hoping to avoid .NET, or you need heavy custom logic that goes beyond what If/ConditionGroup/Foreach can express cleanly, you'll fight the expression language more than you'll benefit from the YAML — at which point the code-first API in the same framework is probably the better starting point, declarative workflow or not.
Tags: microsoft-agent-framework, ai-agents, python, dotnet, yaml-workflows, llm-orchestration, ollama
Top comments (0)