Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Declarative workflows allow you to define workflow logic using YAML configuration files instead of writing programmatic code. This approach makes workflows easier to read, modify, and share across teams.
Overview
With declarative workflows, you describe what your workflow should do rather than how to implement it. The framework handles the underlying execution, converting your YAML definitions into executable workflow graphs.
Key benefits:
- Readable format: YAML syntax is easy to understand, even for non-developers
- Portable: Workflow definitions can be shared, versioned, and modified without code changes
- Rapid iteration: Modify workflow behavior by editing configuration files
- Consistent structure: Predefined action types ensure workflows follow best practices
When to Use Declarative vs. Programmatic Workflows
| Scenario | Recommended Approach |
|---|---|
| Standard orchestration patterns | Declarative |
| Workflows that change frequently | Declarative |
| Non-developers need to modify workflows | Declarative |
| Complex custom logic | Programmatic |
| Maximum flexibility and control | Programmatic |
| Integration with existing Python code | Programmatic |
Basic YAML Structure
The YAML structure differs slightly between C# and Python implementations. See the language-specific sections below for details.
Action Types
Declarative workflows support a wide range of action kinds covering variable management, control flow, agent and tool invocation, HTTP and MCP integration, human-in-the-loop, and conversation control. The complete language-specific reference appears in each zone below; for an at-a-glance availability matrix across both languages, see Actions Quick Reference at the bottom of this article.
C# YAML Structure
C# declarative workflows use a trigger-based structure:
#
# Workflow description as a comment
#
kind: Workflow
trigger:
kind: OnConversationStart
id: my_workflow
actions:
- kind: ActionType
id: unique_action_id
displayName: Human readable name
# Action-specific properties
Structure Elements
| Element | Required | Description |
|---|---|---|
kind |
Yes | Must be Workflow |
trigger.kind |
Yes | Trigger type (typically OnConversationStart) |
trigger.id |
Yes | Unique identifier for the workflow |
trigger.actions |
Yes | List of actions to execute |
Python YAML Structure
Python declarative workflows use a name-based structure with optional inputs:
name: my-workflow
description: A brief description of what this workflow does
inputs:
parameterName:
type: string
description: Description of the parameter
actions:
- kind: ActionType
id: unique_action_id
displayName: Human readable name
# Action-specific properties
Structure Elements
| Element | Required | Description |
|---|---|---|
name |
Yes | Unique identifier for the workflow |
description |
No | Human-readable description |
inputs |
No | Input parameters the workflow accepts |
actions |
Yes | List of actions to execute |
Prerequisites
Before you begin, ensure you have:
- .NET 8.0 or later
- A Microsoft Foundry project with at least one deployed agent
- The following NuGet packages installed:
dotnet add package Microsoft.Agents.AI.Workflows.Declarative --prerelease
dotnet add package Microsoft.Agents.AI.Workflows.Declarative.AzureAI --prerelease
- If you intend to add MCP tool invocation action to your workflow, also install the following NuGet package:
dotnet add package Microsoft.Agents.AI.Workflows.Declarative.Mcp --prerelease
- Basic familiarity with YAML syntax
- Understanding of workflow concepts
Your First Declarative Workflow
Let's create a simple workflow that greets a user based on their input.
Step 1: Create the YAML File
Create a file named greeting-workflow.yaml:
#
# This workflow demonstrates a simple greeting based on user input.
# The user's message is captured via System.LastMessage.
#
# Example input:
# Alice
#
kind: Workflow
trigger:
kind: OnConversationStart
id: greeting_workflow
actions:
# Capture the user's input from the last message
- kind: SetVariable
id: capture_name
displayName: Capture user name
variable: Local.userName
value: =System.LastMessage.Text
# Set a greeting prefix
- kind: SetVariable
id: set_greeting
displayName: Set greeting prefix
variable: Local.greeting
value: Hello
# Build the full message using an expression
- kind: SetVariable
id: build_message
displayName: Build greeting message
variable: Local.message
value: =Concat(Local.greeting, ", ", Local.userName, "!")
# Send the greeting to the user
- kind: SendActivity
id: send_greeting
displayName: Send greeting to user
activity: =Local.message
Step 2: Configure the Agent Provider
Create a C# console application to execute the workflow. First, configure the agent provider that connects to Foundry:
using Azure.Identity;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Agents.AI.Workflows.Declarative;
using Microsoft.Extensions.Configuration;
// Load configuration (endpoint should be set in user secrets or environment variables)
IConfiguration configuration = new ConfigurationBuilder()
.AddUserSecrets<Program>()
.AddEnvironmentVariables()
.Build();
string foundryEndpoint = configuration["FOUNDRY_PROJECT_ENDPOINT"]
?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT not configured");
// Create the agent provider that connects to Foundry
// WARNING: DefaultAzureCredential is convenient for development but requires
// careful consideration in production environments.
AzureAgentProvider agentProvider = new(
new Uri(foundryEndpoint),
new DefaultAzureCredential());
Step 3: Build and Run the Workflow
// Define workflow options with the agent provider
DeclarativeWorkflowOptions options = new(agentProvider)
{
Configuration = configuration,
// LoggerFactory = loggerFactory, // Optional: Enable logging
// ConversationId = conversationId, // Optional: Continue existing conversation
};
// Build the workflow from the YAML file
string workflowPath = Path.Combine(AppContext.BaseDirectory, "greeting-workflow.yaml");
Workflow workflow = DeclarativeWorkflowBuilder.Build<string>(workflowPath, options);
Console.WriteLine($"Loaded workflow from: {workflowPath}");
Console.WriteLine(new string('-', 40));
// Create a checkpoint manager (in-memory for this example)
CheckpointManager checkpointManager = CheckpointManager.CreateInMemory();
// Execute the workflow with input
string input = "Alice";
StreamingRun run = await InProcessExecution.RunStreamingAsync(
workflow,
input,
checkpointManager);
// Process workflow events
await foreach (WorkflowEvent workflowEvent in run.WatchStreamAsync())
{
switch (workflowEvent)
{
case MessageActivityEvent activityEvent:
Console.WriteLine($"Activity: {activityEvent.Message}");
break;
case AgentResponseEvent responseEvent:
Console.WriteLine($"Response: {responseEvent.Response.Text}");
break;
case WorkflowErrorEvent errorEvent:
Console.WriteLine($"Error: {errorEvent.Data}");
break;
}
}
Console.WriteLine("Workflow completed!");
Expected Output
Loaded workflow from: C:\path\to\greeting-workflow.yaml
----------------------------------------
Activity: Hello, Alice!
Workflow completed!
Core Concepts
Variable Namespaces
Declarative workflows in C# use namespaced variables to organize state:
| Namespace | Description | Example |
|---|---|---|
Local.* |
Variables local to the workflow | Local.message |
System.* |
System-provided values | System.ConversationId, System.LastMessage |
Note
C# declarative workflows do not use Workflow.Inputs or Workflow.Outputs namespaces. Input is received via System.LastMessage and output is sent via SendActivity actions.
System Variables
| Variable | Description |
|---|---|
System.ConversationId |
Current conversation identifier |
System.LastMessage |
The most recent user message |
System.LastMessage.Text |
Text content of the last message |
Expression Language
Values prefixed with = are evaluated as expressions using the PowerFx expression language:
# Literal value (no evaluation)
value: Hello
# Expression (evaluated at runtime)
value: =Concat("Hello, ", Local.userName)
# Access last message text
value: =System.LastMessage.Text
Common functions include:
Concat(str1, str2, ...)- Concatenate stringsIf(condition, trueValue, falseValue)- Conditional expressionIsBlank(value)- Check if value is emptyUpper(text)/Lower(text)- Case conversionFind(searchText, withinText)- Find text within stringMessageText(message)- Extract text from a message objectUserMessage(text)- Create a user message from textAgentMessage(text)- Create an agent message from text
Configuration Options
The DeclarativeWorkflowOptions class provides configuration for workflow execution:
DeclarativeWorkflowOptions options = new(agentProvider)
{
// Application configuration for variable substitution
Configuration = configuration,
// Continue an existing conversation (optional)
ConversationId = "existing-conversation-id",
// Enable logging (optional)
LoggerFactory = loggerFactory,
// MCP tool handler for InvokeMcpTool actions (optional)
McpToolHandler = mcpToolHandler,
// HTTP request handler for HttpRequestAction actions (optional)
HttpRequestHandler = new DefaultHttpRequestHandler(),
// PowerFx expression limits (optional)
MaximumCallDepth = 50,
MaximumExpressionLength = 10000,
// Telemetry configuration (optional)
ConfigureTelemetry = opts => { /* configure telemetry */ },
TelemetryActivitySource = activitySource,
};
Agent Provider Setup
The AzureAgentProvider connects your workflow to Foundry agents:
using Azure.Identity;
using Microsoft.Agents.AI.Workflows.Declarative;
// Create the agent provider with Azure credentials
AzureAgentProvider agentProvider = new(
new Uri("https://your-project.api.azureml.ms"),
new DefaultAzureCredential())
{
// Optional: Define functions that agents can automatically invoke
Functions = [
AIFunctionFactory.Create(myPlugin.GetData),
AIFunctionFactory.Create(myPlugin.ProcessItem),
],
// Optional: Allow concurrent function invocation
AllowConcurrentInvocation = true,
// Optional: Allow multiple tool calls per response
AllowMultipleToolCalls = true,
};
Workflow Execution
Use InProcessExecution to run workflows and handle events:
using Microsoft.Agents.AI.Workflows;
using Microsoft.Agents.AI.Workflows.Checkpointing;
// Create checkpoint manager (choose in-memory or file-based)
CheckpointManager checkpointManager = CheckpointManager.CreateInMemory();
// Or persist to disk:
// var checkpointFolder = Directory.CreateDirectory("./checkpoints");
// var checkpointManager = CheckpointManager.CreateJson(
// new FileSystemJsonCheckpointStore(checkpointFolder));
// Start workflow execution
StreamingRun run = await InProcessExecution.RunStreamingAsync(
workflow,
input,
checkpointManager);
// Process events as they occur
await foreach (WorkflowEvent workflowEvent in run.WatchStreamAsync())
{
switch (workflowEvent)
{
case MessageActivityEvent activity:
Console.WriteLine($"Message: {activity.Message}");
break;
case AgentResponseUpdateEvent streamEvent:
Console.Write(streamEvent.Update.Text); // Streaming text
break;
case AgentResponseEvent response:
Console.WriteLine($"Agent: {response.Response.Text}");
break;
case RequestInfoEvent request:
// Handle external input requests (human-in-the-loop)
var userInput = await GetUserInputAsync(request);
await run.SendResponseAsync(request.Request.CreateResponse(userInput));
break;
case SuperStepCompletedEvent checkpoint:
// Checkpoint created - can resume from here if needed
var checkpointInfo = checkpoint.CompletionInfo?.Checkpoint;
break;
case WorkflowErrorEvent error:
Console.WriteLine($"Error: {error.Data}");
break;
}
}
Resuming from Checkpoints
Workflows can be resumed from checkpoints for fault tolerance:
// Save checkpoint info when workflow yields
CheckpointInfo? lastCheckpoint = null;
await foreach (WorkflowEvent workflowEvent in run.WatchStreamAsync())
{
if (workflowEvent is SuperStepCompletedEvent checkpointEvent)
{
lastCheckpoint = checkpointEvent.CompletionInfo?.Checkpoint;
}
}
// Later: Resume from the saved checkpoint
if (lastCheckpoint is not null)
{
// Recreate the workflow (can be on a different machine)
Workflow workflow = DeclarativeWorkflowBuilder.Build<string>(workflowPath, options);
StreamingRun resumedRun = await InProcessExecution.ResumeStreamingAsync(
workflow,
lastCheckpoint,
checkpointManager);
// Continue processing events...
}
AOT and Trim-Aggressive Checkpointing
When you publish with Native AOT (dotnet publish -p:PublishAot=true) or otherwise disable System.Text.Json's reflection fallback (<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>), the default CheckpointManager.CreateJson(store) call fails on checkpoint commit or rehydration.
The declarative-workflow package ships a source-generated JsonSerializerOptions instance, DeclarativeWorkflowJsonOptions.Default, that covers every declarative-package type flowing through the checkpoint pipeline. Pass it as the second argument to CheckpointManager.CreateJson:
using Microsoft.Agents.AI.Workflows.Checkpointing;
using Microsoft.Agents.AI.Workflows.Declarative;
// AOT-safe: type info is resolved via the source-generated JsonSerializerContext,
// so no runtime reflection is required.
CheckpointManager checkpointManager = CheckpointManager.CreateJson(
store,
DeclarativeWorkflowJsonOptions.Default);
Note
Passing DeclarativeWorkflowJsonOptions.Default is safe to use in non-AOT environments as well. It is a drop-in upgrade for CheckpointManager.CreateJson(store) — reflection-enabled apps see no behavior change. Adopt it unconditionally so the same code keeps working if you later publish with AOT or trimming.
DeclarativeWorkflowJsonOptions is marked [Experimental("MAAI001")]. Suppress the diagnostic at the call site or in your project file:
<PropertyGroup>
<NoWarn>$(NoWarn);MAAI001</NoWarn>
</PropertyGroup>
Registering user-defined types
If your workflow input, custom ActionExecutorResult.Result payloads, or non-primitive approval-request arguments are user-defined types, clone Default and append your own source-generated resolver:
// Compose: declarative-package types + your app's source-gen context.
JsonSerializerOptions options = new(DeclarativeWorkflowJsonOptions.Default);
options.TypeInfoResolverChain.Add(MyAppJsonContext.Default);
options.MakeReadOnly();
CheckpointManager checkpointManager = CheckpointManager.CreateJson(store, options);
Where MyAppJsonContext is a JsonSerializerContext you define for your app's types:
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web)]
[JsonSerializable(typeof(MyWorkflowInput))]
[JsonSerializable(typeof(MyCustomResult))]
internal sealed partial class MyAppJsonContext : JsonSerializerContext;
Tip
For an end-to-end runnable example — including the YAML workflow, an AzureCliCredential-backed agent, and an observable "drop the options to see the failure" mode — see the AotCheckpointing sample in dotnet/samples/03-workflows/Declarative/AotCheckpointing. The sample's .csproj sets JsonSerializerIsReflectionEnabledByDefault=false to reproduce the AOT failure mode without requiring a full AOT publish.
Actions Reference
Actions are the building blocks of declarative workflows. Each action performs a specific operation, and actions are executed sequentially in the order they appear in the YAML file.
Action Structure
All actions share common properties:
- kind: ActionType # Required: The type of action
id: unique_id # Optional: Unique identifier for referencing
displayName: Name # Optional: Human-readable name for logging
# Action-specific properties...
Variable Management Actions
SetVariable
Sets a variable to a specified value.
- kind: SetVariable
id: set_greeting
displayName: Set greeting message
variable: Local.greeting
value: Hello World
With an expression:
- kind: SetVariable
variable: Local.fullName
value: =Concat(Local.firstName, " ", Local.lastName)
Properties:
| Property | Required | Description |
|---|---|---|
variable |
Yes | Variable path (e.g., Local.name, Workflow.Outputs.result) |
value |
Yes | Value to set (literal or expression) |
SetMultipleVariables
Sets multiple variables in a single action.
- kind: SetMultipleVariables
id: initialize_vars
displayName: Initialize variables
variables:
Local.counter: 0
Local.status: pending
Local.message: =Concat("Processing order ", Local.orderId)
Properties:
| Property | Required | Description |
|---|---|---|
variables |
Yes | Map of variable paths to values |
SetTextVariable
Sets a text variable to a specified string value.
- kind: SetTextVariable
id: set_text
displayName: Set text content
variable: Local.description
value: This is a text description
Properties:
| Property | Required | Description |
|---|---|---|
variable |
Yes | Variable path for the text value |
value |
Yes | Text value to set |
ResetVariable
Clears a variable's value.
- kind: ResetVariable
id: clear_counter
variable: Local.counter
Properties:
| Property | Required | Description |
|---|---|---|
variable |
Yes | Variable path to reset |
ClearAllVariables
Resets all variables in the current context.
- kind: ClearAllVariables
id: clear_all
displayName: Clear all workflow variables
ParseValue
Extracts or converts data into a usable format.
- kind: ParseValue
id: parse_json
displayName: Parse JSON response
source: =Local.rawResponse
variable: Local.parsedData
Properties:
| Property | Required | Description |
|---|---|---|
source |
Yes | Expression returning the value to parse |
variable |
Yes | Variable path to store the parsed result |
EditTableV2
Modifies data in a structured table format.
- kind: EditTableV2
id: update_table
displayName: Update configuration table
table: Local.configTable
operation: update
row:
key: =Local.settingName
value: =Local.settingValue
Properties:
| Property | Required | Description |
|---|---|---|
table |
Yes | Variable path to the table |
operation |
Yes | Operation type (add, update, delete) |
row |
Yes | Row data for the operation |
Control Flow Actions
If
Executes actions conditionally based on a condition.
- kind: If
id: check_age
displayName: Check user age
condition: =Local.age >= 18
then:
- kind: SendActivity
activity:
text: "Welcome, adult user!"
else:
- kind: SendActivity
activity:
text: "Welcome, young user!"
Properties:
| Property | Required | Description |
|---|---|---|
condition |
Yes | Expression that evaluates to true/false |
then |
Yes | Actions to execute if condition is true |
else |
No | Actions to execute if condition is false |
ConditionGroup
Evaluates multiple conditions like a switch/case statement.
- kind: ConditionGroup
id: route_by_category
displayName: Route based on category
conditions:
- condition: =Local.category = "electronics"
id: electronics_branch
actions:
- kind: SetVariable
variable: Local.department
value: Electronics Team
- condition: =Local.category = "clothing"
id: clothing_branch
actions:
- kind: SetVariable
variable: Local.department
value: Clothing Team
elseActions:
- kind: SetVariable
variable: Local.department
value: General Support
Properties:
| Property | Required | Description |
|---|---|---|
conditions |
Yes | List of condition/actions pairs (first match wins) |
elseActions |
No | Actions if no condition matches |
Foreach
Iterates over a collection.
- kind: Foreach
id: process_items
displayName: Process each item
source: =Local.items
itemName: item
indexName: index
actions:
- kind: SendActivity
activity:
text: =Concat("Processing item ", index, ": ", item)
Properties:
| Property | Required | Description |
|---|---|---|
source |
Yes | Expression returning a collection |
itemName |
No | Variable name for current item (default: item) |
indexName |
No | Variable name for current index (default: index) |
actions |
Yes | Actions to execute for each item |
BreakLoop
Exits the current loop immediately.
- kind: Foreach
source: =Local.items
actions:
- kind: If
condition: =item = "stop"
then:
- kind: BreakLoop
- kind: SendActivity
activity:
text: =item
ContinueLoop
Skips to the next iteration of the loop.
- kind: Foreach
source: =Local.numbers
actions:
- kind: If
condition: =item < 0
then:
- kind: ContinueLoop
- kind: SendActivity
activity:
text: =Concat("Positive number: ", item)
GotoAction
Jumps to a specific action by ID.
- kind: SetVariable
id: start_label
variable: Local.attempts
value: =Local.attempts + 1
- kind: SendActivity
activity:
text: =Concat("Attempt ", Local.attempts)
- kind: If
condition: =And(Local.attempts < 3, Not(Local.success))
then:
- kind: GotoAction
actionId: start_label
Properties:
| Property | Required | Description |
|---|---|---|
actionId |
Yes | ID of the action to jump to |
Output Actions
SendActivity
Sends a message to the user.
- kind: SendActivity
id: send_welcome
displayName: Send welcome message
activity:
text: "Welcome to our service!"
With an expression:
- kind: SendActivity
activity:
text: =Concat("Hello, ", Local.userName, "! How can I help you today?")
Properties:
| Property | Required | Description |
|---|---|---|
activity |
Yes | The activity to send |
activity.text |
Yes | Message text (literal or expression) |
Agent Invocation Actions
InvokeAzureAgent
Invokes a Foundry agent.
Basic invocation:
- kind: InvokeAzureAgent
id: call_assistant
displayName: Call assistant agent
agent:
name: AssistantAgent
conversationId: =System.ConversationId
With input and output configuration:
- kind: InvokeAzureAgent
id: call_analyst
displayName: Call analyst agent
agent:
name: AnalystAgent
conversationId: =System.ConversationId
input:
messages: =Local.userMessage
arguments:
topic: =Local.topic
output:
responseObject: Local.AnalystResult
messages: Local.AnalystMessages
autoSend: true
With external loop (continues until condition is met):
- kind: InvokeAzureAgent
id: support_agent
agent:
name: SupportAgent
input:
externalLoop:
when: =Not(Local.IsResolved)
output:
responseObject: Local.SupportResult
Properties:
| Property | Required | Description |
|---|---|---|
agent.name |
Yes | Name of the registered agent |
conversationId |
No | Conversation context identifier |
input.messages |
No | Messages to send to the agent |
input.arguments |
No | Additional arguments for the agent |
input.externalLoop.when |
No | Condition to continue agent loop |
output.responseObject |
No | Path to store agent response |
output.messages |
No | Path to store conversation messages |
output.autoSend |
No | Automatically send response to user |
Tool and HTTP Actions
InvokeFunctionTool
Invokes a function tool directly from the workflow without going through an AI agent.
- kind: InvokeFunctionTool
id: invoke_get_data
displayName: Get data from function
functionName: GetUserData
conversationId: =System.ConversationId
requireApproval: true
arguments:
userId: =Local.userId
output:
autoSend: true
result: Local.UserData
messages: Local.FunctionMessages
Properties:
| Property | Required | Description |
|---|---|---|
functionName |
Yes | Name of the function to invoke |
conversationId |
No | Conversation context identifier |
requireApproval |
No | Whether to require user approval before execution |
arguments |
No | Arguments to pass to the function |
output.result |
No | Path to store function result |
output.messages |
No | Path to store function messages |
output.autoSend |
No | Automatically send result to user |
C# Setup for InvokeFunctionTool:
Functions must be registered with the WorkflowRunner or handled via external input:
// Define functions that can be invoked
AIFunction[] functions = [
AIFunctionFactory.Create(myPlugin.GetUserData),
AIFunctionFactory.Create(myPlugin.ProcessOrder),
];
// Create workflow runner with functions
WorkflowRunner runner = new(functions) { UseJsonCheckpoints = true };
await runner.ExecuteAsync(workflowFactory.CreateWorkflow, input);
InvokeMcpTool
Invokes a tool on an MCP (Model Context Protocol) server.
- kind: InvokeMcpTool
id: invoke_docs_search
displayName: Search documentation
serverUrl: https://learn.microsoft.com/api/mcp
serverLabel: microsoft_docs
toolName: microsoft_docs_search
conversationId: =System.ConversationId
requireApproval: false
headers:
X-Custom-Header: custom-value
arguments:
query: =Local.SearchQuery
output:
autoSend: true
result: Local.SearchResults
With connection name for hosted scenarios:
- kind: InvokeMcpTool
id: invoke_hosted_mcp
serverUrl: https://mcp.ai.azure.com
toolName: my_tool
# Connection name is used in hosted scenarios to connect to a ProjectConnectionId in Foundry.
# Note: This feature is not fully supported yet.
connection:
name: my-foundry-connection
output:
result: Local.ToolResult
Properties:
| Property | Required | Description |
|---|---|---|
serverUrl |
Yes | URL of the MCP server |
serverLabel |
No | Human-readable label for the server |
toolName |
Yes | Name of the tool to invoke |
conversationId |
No | Conversation context identifier |
requireApproval |
No | Whether to require user approval |
arguments |
No | Arguments to pass to the tool |