Declarative Workflows¶
Foundry can run Microsoft Agent Framework declarative workflows — orchestration authored in YAML rather than code — against agents your application already owns.
This lives in the optional
NexusLabs.Foundry.MicrosoftAgentFramework.Workflows.Declarative package. The core
package never references it, and no other Foundry package acquires it transitively.
Why this package exists¶
Upstream ships a declarative runtime, but the only agent provider it ships resolves agent names against a deployed Azure AI Foundry project, and that companion package is pre-release. Every upstream sample assumes it.
The InvokeAzureAgent action is not actually Azure-specific: it resolves through an
abstract ResponseAgentProvider. This package supplies one backed by your own agents,
so a declarative workflow can drive in-process agents with no remote project involved.
Running a workflow¶
Agents are declared the same way as anywhere else in Foundry and resolved through
IAgentFactory, so a declarative workflow needs no registration code of its own:
[FoundryAgent(Description = "...", Instructions = "...")]
public sealed class ClassifierAgent { }
// composition root
services.AddFoundryAgentFramework(builder => builder
.UsingChatClient(chatClient)
.AddAgent<ClassifierAgent>()
.AddAgent<ResponderAgent>());
var agentFactory = serviceProvider.GetRequiredService<IAgentFactory>();
Workflow workflow = agentFactory.CreateDeclarativeWorkflow(
File.ReadAllText("triage-workflow.yaml"));
StreamingRun run = await InProcessExecution.RunStreamingAsync(
workflow,
"I cannot log in to my account",
checkpointManager: CheckpointManager.CreateInMemory());
await foreach (var workflowEvent in run.WatchStreamAsync().ReportProgressTo(reporter, ct))
{
// The upstream stream is forwarded unchanged; progress is reported as a side effect.
}
CreateDeclarativeWorkflow and ValidateDeclarativeWorkflow are extension methods on
IAgentFactory rather than members of IWorkflowFactory. Agent resolution is the only
thing declarative composition needs from Foundry, and IWorkflowFactory lives in the
core package, which cannot depend on this one without pulling an interpreted expression
engine into every consumer.
The document names agents by type name:
kind: Workflow
trigger:
kind: OnConversationStart
id: triage_workflow
actions:
- kind: SetVariable
id: capture_report
variable: Local.report
value: =System.LastMessage.Text
- kind: InvokeAzureAgent
id: classify
agent:
name: ClassifierAgent
input:
messages: =Local.report
output:
autoSend: true
responseObject: Local.Classification
Agents are addressed by their published name, matched exactly as
IAgentFactory.CreateAgent(string) matches them. That is the simple class name by
default, or [FoundryAgent(Name = "...")] when one is declared:
[FoundryAgent(
Name = "Classifier",
Instructions = "Classify the report.")]
public sealed class ClassifierAgent { }
Declaring a name matters here because a workflow document is hand-edited and is not compiled. Left as the class name, the document is coupled to a type name, and renaming the class breaks the workflow with no compile-time error — there is no schema to validate the document against either. Declaring the name instead lets the class be renamed freely, and moves the thing that must not change into a place an author can see. Renaming the published name still breaks the document, so pick it deliberately.
Either way the failure is loud rather than silent. An unresolved name surfaces at the action that named it:
DeclarativeActionException: Unhandled workflow failure - #classify (InvokeAzureAgent)
---> InvalidOperationException: No agent named 'Clasifier' is registered. ...
Registered names: 'Classifier', 'ResponderAgent'.
Names must be unique across all declared agents; a collision is reported by FDRYMAF031 at compile time and rejected when the agent factory is built. Because the name also reaches the model as part of a handoff tool name, prefer characters a provider accepts in a function name — letters, digits, hyphens, and underscores. Nothing in Foundry or the Agent Framework enforces that, so a name containing other characters may be rejected by the provider instead.
The fully-qualified type name always resolves as well, and is the way to disambiguate if you ever need it.
A complete runnable example is in
src/Examples/AgentFramework/DeclarativeWorkflowApp. It runs entirely offline.
Calling out to MCP tools and HTTP endpoints¶
A document can reach outside the workflow three ways: an agent, an MCP server tool, or an HTTP endpoint. Agents are always wired — that is what this package is for. The other two are not, because each reaches a network endpoint the host has to decide to trust, not the document and not Foundry.
Supply them explicitly:
var workflow = agentFactory.CreateDeclarativeWorkflow(
workflowYaml,
new DeclarativeWorkflowHandlers
{
McpToolHandler = new DefaultMcpToolHandler(/* … */),
HttpRequestHandler = null,
});
Both members are required, so "documents may not do this" is stated as null rather
than left unanswered. If your documents call out to neither, use the overloads that take
no handlers at all.
Foundry ships no implementation of either interface. An implementation decides which servers may be reached, what credentials are attached, and what timeout and retry policy applies — none of which is knowable here. Upstream provides:
DefaultMcpToolHandlerinMicrosoft.Agents.AI.Workflows.Declarative.Mcp(stable at 1.17.0, tracks the same version as the base package). Adding it pulls inModelContextProtocol, which is why Foundry does not take the dependency on your behalf.DefaultHttpRequestHandler, already in the base package. Note it is unrestricted — it sends whatever the document asks, wherever the document names. That suits a trusted document and is a poor default for one that is not.
A runnable example against a real MCP server is in
src/Examples/AgentFramework/DeclarativeMcpWorkflowApp. Start a server first:
The tool's result surfaces both in the responseObject variable and as an
AgentResponseEvent on the workflow stream, so a caller watching the stream sees it
without reading workflow state.
The InvokeMcpTool action¶
There is no published schema for this action. The shape below was established by running candidate documents and observing which reached the handler, and is pinned by tests:
- kind: InvokeMcpTool
id: call_tool
serverUrl: https://example.test/mcp # required
serverLabel: example # optional
toolName: search # required
arguments: # optional
query: =System.LastMessage.Text # values are Power Fx expressions
output:
responseObject: Local.ToolResult
Argument values are expressions, not literals — they are evaluated against workflow
state before the handler is called, so a handler receives resolved values. Nesting the
server under a server: key or the tool under a tool: key does not work.
Validation¶
Declarative workflows have no published JSON Schema, so parsing is the only
validation available. Validate makes it deliberate:
var validation = agentFactory.ValidateDeclarativeWorkflow(workflowYaml);
if (!validation.IsValid)
{
Console.WriteLine(validation.ErrorMessage);
}
It reports what upstream's builder rejects, which is structural malformation. That includes two MCP faults, both caught at build rather than at invocation:
- an
InvokeMcpToolaction missingserverUrlortoolName; - an
InvokeMcpToolaction in a document built without anMcpToolHandler.
It does not catch every authoring mistake:
- an unrecognized action
kindis accepted without complaint; - an expression that will fail to evaluate cannot be detected until it runs;
- an agent name that is not registered cannot be detected until the action runs.
Progress reporting¶
ReportProgressTo bridges the upstream event stream onto Foundry progress. Action
lifecycle is taken from upstream's declarative events rather than the generic executor
events, because only those carry the action id written in the document:
It is a pass-through: the original stream is yielded unchanged, so a caller can report progress and still do its own handling.
Limitations¶
These are properties of the upstream feature, measured rather than assumed.
Not NativeAOT compatible¶
The package is deliberately excluded from Foundry's NativeAOT profile. A probe that
parsed a declarative document and published with PublishAot=true produced 44 trim
warnings and 41 AOT warnings, and the resulting native binary failed at startup:
NotSupportedException: 'Microsoft.Agents.ObjectModel.RecurrenceFrequency[]'
is missing native code or metadata.
That is a static type initializer, so nothing declarative runs at all under AOT. The cause is upstream-owned — Power Fx is an interpreter doing reflective function binding, and the Power Platform object model maps types reflectively — so it is not fixable from Foundry. Do not reference this package from a project you intend to publish AOT.
No fan-out / fan-in¶
Declarative YAML has no parallel execution construct
(upstream #2765). If you need
concurrent branches, joins, or reducers, use Foundry's GraphWorkflowRunner, which is
what it exists for. This package sits beside it rather than replacing it.
No declarative retry, timeout, or error handling¶
There are no retry, timeout, or onError constructs in the document format. Handle
failures in code around the run, or in a custom IHttpRequestHandler for HTTP actions.
Documentation defects to be aware of¶
Two things in Microsoft's published guidance do not reproduce:
- The greeting sample calls
Concatwith four arguments. Power Fx'sConcatis the table-aggregation function and accepts two to three, so the sample as published fails withInvalid number of arguments: received 4, expected 2-3. String concatenation isConcatenateor the&operator. SendActivitywithactivity: =SomeExpressionemits the raw template rather than the evaluated text, despite the documented sample output showing otherwise.
Expressions do evaluate — SetVariable values and agent inputs both work — so prefer
carrying computed values into an agent input or variable rather than relying on
SendActivity to render an expression.
Package boundaries¶
- Optional: the core
NexusLabs.Foundry.MicrosoftAgentFrameworkpackage never references it. - Excluded from the NativeAOT example and profile.
- Pinned to
Microsoft.Agents.AI.Workflows.Declarative1.15.0, matching the rest of the Agent Framework pin. No version bump was required to adopt it.