July 31, 2026
Microsoft Just Moved Multi-Agent Orchestration Out of C# - Here's the Review Gate I'd Add First
Agent Framework declarative workflows are now 1.0. The YAML is readable, versionable, and far too easy to mistake for harmless…

By Mikhail Petrusheuski
5 min read
Agent Framework declarative workflows are now 1.0. The YAML is readable, versionable, and far too easy to mistake for harmless configuration.
For years, .NET teams treated workflow orchestration as application logic.
The sequence lived in C#.
The branches lived in C#.
The agent handoffs lived in C#.
And changing the workflow meant changing, reviewing, building, and deploying the application.
Microsoft Agent Framework just changed that boundary.
Declarative workflows have reached 1.0 across the .NET and Python SDKs. In .NET, you can now describe agent coordination, state changes, branching, tool calls, and human intervention in YAML, then load that definition as a standard Workflow.
The obvious interpretation is that Microsoft made orchestration easier to read.
I think the more important change is this:
Your production agent behavior can now change without anyone touching the C# call graph.
That is useful.
It is also exactly why I would make workflow YAML harder to merge, not easier.
This Is Not Just Configuration
A normal configuration file selects values for behavior implemented somewhere else.
A declarative agent workflow can define the behavior itself.
It can choose which agent runs.
It can branch on an agent result.
It can invoke a function, MCP tool, or HTTP endpoint.
It can pause for human input.
It can jump, loop, end a conversation, and resume from a checkpoint.
That is executable control flow.
The file extension is .yaml, but the operational impact is much closer to code.
Here is the basic .NET setup from the current documentation:
dotnet add package Microsoft.Agents.AI.Workflows.Declarativedotnet add package Microsoft.Agents.AI.Workflows.DeclarativeA small support router can then move the decision tree out of C#:
kind: Workflow
trigger:
kind: OnConversationStart
id: support_router
actions:
- kind: InvokeAzureAgent
id: triage
conversationId: =System.ConversationId
agent:
name: TriageAgent
output:
responseObject: Local.Triage
- kind: If
id: route
condition: =Local.Triage.Category = "Billing"
then:
- kind: InvokeAzureAgent
id: billing
agent:
name: BillingAgent
else:
- kind: InvokeAzureAgent
id: support
agent:
name: SupportAgentkind: Workflow
trigger:
kind: OnConversationStart
id: support_router
actions:
- kind: InvokeAzureAgent
id: triage
conversationId: =System.ConversationId
agent:
name: TriageAgent
output:
responseObject: Local.Triage
- kind: If
id: route
condition: =Local.Triage.Category = "Billing"
then:
- kind: InvokeAzureAgent
id: billing
agent:
name: BillingAgent
else:
- kind: InvokeAzureAgent
id: support
agent:
name: SupportAgentThis is easier to scan than a graph assembled through builders and delegates.
A product owner can see the route.
An architect can see the handoff.
A reviewer can see which agent receives the request.
That is a real improvement.
But readability is not safety.
The condition still trusts Local.Triage.Category, which came from an agent response. If that output is malformed, ambiguous, or manipulated by hostile input, the workflow can take the wrong branch with perfect YAML syntax.
Readable control flow can still encode a bad decision boundary.
The C# Gets Smaller, Not Less Important
The .NET host now loads the definition and runs it through the same workflow runtime used by code-first orchestration:
DeclarativeWorkflowOptions options = new(agentProvider)
{
Configuration = configuration,
LoggerFactory = loggerFactory
};
Workflow workflow = DeclarativeWorkflowBuilder.Build<string>(
"support-router.yaml",
options);
CheckpointManager checkpoints = CheckpointManager.CreateInMemory();
StreamingRun run = await InProcessExecution.RunStreamingAsync(
workflow,
request,
checkpoints);DeclarativeWorkflowOptions options = new(agentProvider)
{
Configuration = configuration,
LoggerFactory = loggerFactory
};
Workflow workflow = DeclarativeWorkflowBuilder.Build<string>(
"support-router.yaml",
options);
CheckpointManager checkpoints = CheckpointManager.CreateInMemory();
StreamingRun run = await InProcessExecution.RunStreamingAsync(
workflow,
request,
checkpoints);This separation is clean.
The YAML owns the graph.
The host owns credentials, providers, handlers, telemetry, persistence, and the execution environment.
I would keep that boundary strict.
Do not put secrets in the workflow.
Do not let arbitrary YAML select arbitrary endpoints.
Do not treat a successful build as proof that the graph is authorized to do what it describes.
The host should supply capabilities.
The workflow should only reference capabilities it has explicitly been allowed to use.
Protect Workflow YAML Like Source Code
My first review gate would be repository ownership.
Put production workflow definitions under protected paths.
Require an owner from the application team.
Require an owner from the domain that the workflow affects.
Block direct pushes.
Show the rendered graph or a structured diff in the pull request.
A change from BillingAgent to RefundAgent may be one line.
It may also change who can move money.
Line count is not a useful risk metric.
I would also separate workflow updates from free-form prompt edits. They can influence each other, but they deserve different review questions:
- Did the graph change?
- Did a capability change?
- Did an approval disappear?
- Did an external endpoint change?
- Did the agent output become a new control-flow input?
That review is much stronger than "the YAML parses."
Make Sensitive Tools Approval-Gated
The declarative action model exposes approval directly.
Use it.
For example, a refund function should not silently execute just because the router selected a billing path:
- kind: InvokeFunctionTool
id: issue_refund
displayName: Issue approved refund
functionName: IssueRefund
conversationId: =System.ConversationId
requireApproval: true
arguments:
orderId: =Local.OrderId
amount: =Local.RefundAmount
output:
result: Local.RefundResult- kind: InvokeFunctionTool
id: issue_refund
displayName: Issue approved refund
functionName: IssueRefund
conversationId: =System.ConversationId
requireApproval: true
arguments:
orderId: =Local.OrderId
amount: =Local.RefundAmount
output:
result: Local.RefundResultThis is the kind of line I want reviewers to find immediately:
requireApproval: true
But I would not stop there.
The application still needs to define who can approve, what evidence they see, how long the decision remains valid, and what gets recorded.
Agent Framework surfaces external input as a workflow event. Your host still owns the decision path:
await foreach (WorkflowEvent workflowEvent in run.WatchStreamAsync())
{
switch (workflowEvent)
{
case RequestInfoEvent requestInfo:
var answer = await approvalService.ResolveAsync(requestInfo);
await run.SendResponseAsync(
requestInfo.Request.CreateResponse(answer));
break;
case WorkflowErrorEvent error:
logger.LogError("Workflow failed: {Error}", error.Data);
break;
}
}await foreach (WorkflowEvent workflowEvent in run.WatchStreamAsync())
{
switch (workflowEvent)
{
case RequestInfoEvent requestInfo:
var answer = await approvalService.ResolveAsync(requestInfo);
await run.SendResponseAsync(
requestInfo.Request.CreateResponse(answer));
break;
case WorkflowErrorEvent error:
logger.LogError("Workflow failed: {Error}", error.Data);
break;
}
}The approvalService here is your integration, not magic supplied by the model.
That distinction matters.
The workflow can request approval.
Your system must enforce it.
Validate Capabilities, Not Only Syntax
A schema validator can tell you that serverUrl is a string.
It cannot tell you whether the workflow should call that server.
Before deployment, I would extract and compare at least:
- agent names
- function names
- MCP server URLs and tool names
- HTTP destinations and methods
- actions with requireApproval disabled
- loops, jumps, and maximum iteration paths
- external input points
- checkpoint strategy
Then compare that capability manifest with an allowlist owned by the host application.
This is especially important because declarative workflows support direct function, MCP, and HTTP actions.
Moving an operation out of an agent does not make it harmless.
It makes it deterministic.
A deterministic call to the wrong endpoint is still the wrong call.
Test the Decision Boundaries
Most teams will test the happy path:
Billing request -> BillingAgent
Support request -> SupportAgent
That is necessary, but weak.
The more valuable cases are at the branch boundary:
- missing category
- unexpected category
- conflicting fields
- prompt-injected classification
- agent timeout
- tool refusal
- rejected approval
- resume after host failure
For each case, define the expected next action and the actions that must never occur.
I would rather have 20 deterministic workflow cases than another broad demo conversation that "looks right."
The YAML makes this easier because the graph is now an artifact you can version and test independently.
Use that advantage.
Persist Checkpoints Deliberately
The quick sample uses an in-memory checkpoint manager.
That is fine for a console demo.
It is not a recovery strategy.
For a long-running workflow, persist checkpoints and make the serialization choice explicit:
var checkpointFolder = Directory.CreateDirectory("./checkpoints");
var store = new FileSystemJsonCheckpointStore(checkpointFolder);
CheckpointManager checkpoints = CheckpointManager.CreateJson(
store,
DeclarativeWorkflowJsonOptions.Default);var checkpointFolder = Directory.CreateDirectory("./checkpoints");
var store = new FileSystemJsonCheckpointStore(checkpointFolder);
CheckpointManager checkpoints = CheckpointManager.CreateJson(
store,
DeclarativeWorkflowJsonOptions.Default);Microsoft recommends the source-generated DeclarativeWorkflowJsonOptions.Default for AOT-safe checkpoint serialization, and notes that it is safe outside AOT as well.
The larger operational question is not the serializer.
It is compatibility.
If a workflow pauses under version 12 and resumes after version 13 is deployed, which definition owns the checkpoint?
I would persist the workflow version or content digest beside every checkpoint and resume against the same definition by default.
Otherwise, a harmless-looking YAML deployment can change the meaning of an in-flight process.
The Review Gate I Would Actually Ship
My minimum production gate would look like this:
- Parse and build every workflow in CI.
- Generate a capability manifest from agents, functions, MCP tools, and HTTP endpoints.
- Fail when a sensitive action lacks approval.
- Run fixed branch and refusal tests.
- Store a version digest with checkpoints and traces.
- Require domain and application owners for workflow changes.
- Roll out to one tenant or traffic slice before broad activation.
None of this removes the benefit of declarative orchestration.
It is what makes the benefit usable.
The workflow becomes readable to more people.
The deployment becomes independent from application code.
The runtime remains the same standard Workflow abstraction.
Those are strong improvements.
But they move responsibility into a new artifact.
Final Thoughts
I like this direction.
Multi-agent orchestration should not require every reviewer to mentally execute a C# builder graph.
A clear YAML definition can make branches, handoffs, approvals, and state transitions visible to the whole team.
But I would not call it "configuration-driven agents."
That phrase makes the file sound passive.
The more accurate model is:
Agent Framework now lets .NET teams deploy executable orchestration as a document.
Treat the document like code.
Treat its capabilities like permissions.
Treat its checkpoints like versioned state.
And treat every one-line routing change as a production behavior change.
The teams that build that review gate will get faster workflow iteration without losing control.
The teams that do not may discover that they made agent behavior easier to edit and much harder to govern.