Edit

Declarative Workflows - Overview

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

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 strings
  • If(condition, trueValue, falseValue) - Conditional expression
  • IsBlank(value) - Check if value is empty
  • Upper(text) / Lower(text) - Case conversion
  • Find(searchText, withinText) - Find text within string
  • MessageText(message) - Extract text from a message object
  • UserMessage(text) - Create a user message from text
  • AgentMessage(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