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 various action types. The following table shows availability by language:
| Category | Actions | C# | Python |
|---|---|---|---|
| Variable Management | SetVariable, SetMultipleVariables, ResetVariable |
✅ | ✅ |
| Variable Management | AppendValue |
❌ | ✅ |
| Variable Management | SetTextVariable, ClearAllVariables, ParseValue, EditTableV2 |
✅ | ❌ |
| Control Flow | If, ConditionGroup, Foreach, BreakLoop, ContinueLoop, GotoAction |
✅ | ✅ |
| Control Flow | RepeatUntil |
❌ | ✅ |
| Output | SendActivity |
✅ | ✅ |
| Output | EmitEvent |
❌ | ✅ |
| Agent Invocation | InvokeAzureAgent |
✅ | ✅ |
| Tool Invocation | InvokeFunctionTool |
✅ | ✅ |
| Tool Invocation | InvokeMcpTool |
✅ | ❌ |
| Human-in-the-Loop | Question, RequestExternalInput |
✅ | ✅ |
| Human-in-the-Loop | Confirmation, WaitForInput |
❌ | ✅ |
| Workflow Control | EndWorkflow, EndConversation, CreateConversation |
✅ | ✅ |
| Conversation | AddConversationMessage, CopyConversationMessages, RetrieveConversationMessage, RetrieveConversationMessages |
✅ | ❌ |
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
- An Azure AI 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 Azure AI 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 Azure AI 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,
// 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 Azure AI 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...
}
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 (C# only)
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 (C# only)
Resets all variables in the current context.
- kind: ClearAllVariables
id: clear_all
displayName: Clear all workflow variables
ParseValue (C# only)
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 (C# only)
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 an Azure AI 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 Invocation Actions (C# only)
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 |
headers |
No | Custom HTTP headers for the request |
connection.name |
No | Named connection for hosted scenarios (connects to ProjectConnectionId in Foundry; not fully supported yet) |
output.result |
No | Path to store tool result |
output.messages |
No | Path to store result messages |
output.autoSend |
No | Automatically send result to user |
C# Setup for InvokeMcpTool:
Configure the McpToolHandler in your workflow factory:
using Azure.Core;
using Azure.Identity;
using Microsoft.Agents.AI.Workflows.Declarative;
// Create MCP tool handler with authentication callback
DefaultAzureCredential credential = new();
DefaultMcpToolHandler mcpToolHandler = new(
httpClientProvider: async (serverUrl, cancellationToken) =>
{
if (serverUrl.StartsWith("https://mcp.ai.azure.com", StringComparison.OrdinalIgnoreCase))
{
// Acquire token for Azure MCP server
AccessToken token = await credential.GetTokenAsync(
new TokenRequestContext(["https://mcp.ai.azure.com/.default"]),
cancellationToken);
HttpClient httpClient = new();
httpClient.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token.Token);
return httpClient;
}
// Return null for servers that don't require authentication
return null;
});
// Configure workflow factory with MCP handler
WorkflowFactory workflowFactory = new("workflow.yaml", foundryEndpoint)
{
McpToolHandler = mcpToolHandler
};
Human-in-the-Loop Actions
Question
Asks the user a question and stores the response.
- kind: Question
id: ask_name
displayName: Ask for user name
question:
text: "What is your name?"
variable: Local.userName
default: "Guest"
Properties:
| Property | Required | Description |
|---|---|---|
question.text |
Yes | The question to ask |
variable |
Yes | Path to store the response |
default |
No | Default value if no response |
RequestExternalInput
Requests input from an external system or process.
- kind: RequestExternalInput
id: request_approval
displayName: Request manager approval
prompt:
text: "Please provide approval for this request."
variable: Local.approvalResult
default: "pending"
Properties:
| Property | Required | Description |
|---|---|---|
prompt.text |
Yes | Description of required input |
variable |
Yes | Path to store the input |
default |
No | Default value |
Workflow Control Actions
EndWorkflow
Terminates the workflow execution.
- kind: EndWorkflow
id: finish
displayName: End workflow
EndConversation
Ends the current conversation.
- kind: EndConversation
id: end_chat
displayName: End conversation
CreateConversation
Creates a new conversation context.
- kind: CreateConversation
id: create_new_conv
displayName: Create new conversation
conversationId: Local.NewConversationId
Properties:
| Property | Required | Description |
|---|---|---|
conversationId |
Yes | Path to store the new conversation ID |
Conversation Actions (C# only)
AddConversationMessage
Adds a message to a conversation thread.
- kind: AddConversationMessage
id: add_system_message
displayName: Add system context
conversationId: =System.ConversationId
message:
role: system
content: =Local.contextInfo
Properties:
| Property | Required | Description |
|---|---|---|
conversationId |
Yes | Target conversation identifier |
message |
Yes | Message to add |
message.role |
Yes | Message role (system, user, assistant) |
message.content |
Yes | Message content |
CopyConversationMessages
Copies messages from one conversation to another.
- kind: CopyConversationMessages
id: copy_context
displayName: Copy conversation context
sourceConversationId: =Local.SourceConversation
targetConversationId: =System.ConversationId
limit: 10
Properties:
| Property | Required | Description |
|---|---|---|
sourceConversationId |
Yes | Source conversation identifier |
targetConversationId |
Yes | Target conversation identifier |
limit |
No | Maximum number of messages to copy |
RetrieveConversationMessage
Retrieves a specific message from a conversation.
- kind: RetrieveConversationMessage
id: get_message
displayName: Get specific message
conversationId: =System.ConversationId
messageId: =Local.targetMessageId
variable: Local.retrievedMessage
Properties:
| Property | Required | Description |
|---|---|---|
conversationId |
Yes | Conversation identifier |
messageId |
Yes | Message identifier to retrieve |
variable |
Yes | Path to store the retrieved message |
RetrieveConversationMessages
Retrieves multiple messages from a conversation.
- kind: RetrieveConversationMessages
id: get_history
displayName: Get conversation history
conversationId: =System.ConversationId
limit: 20
newestFirst: true
variable: Local.conversationHistory
Properties:
| Property | Required | Description |
|---|---|---|
conversationId |
Yes | Conversation identifier |
limit |
No | Maximum messages to retrieve (default: 20) |
newestFirst |
No | Return in descending order |
after |
No | Cursor for pagination |
before |
No | Cursor for pagination |
variable |
Yes | Path to store retrieved messages |
Actions Quick Reference
| Action | Category | C# | Python | Description |
|---|---|---|---|---|
SetVariable |
Variable | ✅ | ✅ | Set a single variable |
SetMultipleVariables |
Variable | ✅ | ✅ | Set multiple variables |
SetTextVariable |
Variable | ✅ | ❌ | Set a text variable |
AppendValue |
Variable | ❌ | ✅ | Append to list/string |
ResetVariable |
Variable | ✅ | ✅ | Clear a variable |
ClearAllVariables |
Variable | ✅ | ❌ | Clear all variables |
ParseValue |
Variable | ✅ | ❌ | Parse/transform data |
EditTableV2 |
Variable | ✅ | ❌ | Modify table data |
If |
Control Flow | ✅ | ✅ | Conditional branching |
ConditionGroup |
Control Flow | ✅ | ✅ | Multi-branch switch |
Foreach |
Control Flow | ✅ | ✅ | Iterate over collection |
RepeatUntil |
Control Flow | ❌ | ✅ | Loop until condition |
BreakLoop |
Control Flow | ✅ | ✅ | Exit current loop |
ContinueLoop |
Control Flow | ✅ | ✅ | Skip to next iteration |
GotoAction |
Control Flow | ✅ | ✅ | Jump to action by ID |
SendActivity |
Output | ✅ | ✅ | Send message to user |
EmitEvent |
Output | ❌ | ✅ | Emit custom event |
InvokeAzureAgent |
Agent | ✅ | ✅ | Call Azure AI agent |
InvokeFunctionTool |
Tool | ✅ | ✅ | Invoke function directly |
InvokeMcpTool |
Tool | ✅ | ❌ | Invoke MCP server tool |
Question |
Human-in-the-Loop | ✅ | ✅ | Ask user a question |
Confirmation |
Human-in-the-Loop | ❌ | ✅ | Yes/no confirmation |
RequestExternalInput |
Human-in-the-Loop | ✅ | ✅ | Request external input |
WaitForInput |
Human-in-the-Loop | ❌ | ✅ | Wait for input |
EndWorkflow |
Workflow Control | ✅ | ✅ | Terminate workflow |
EndConversation |
Workflow Control | ✅ | ✅ | End conversation |
CreateConversation |
Workflow Control | ✅ | ✅ | Create new conversation |
AddConversationMessage |
Conversation | ✅ | ❌ | Add message to thread |
CopyConversationMessages |
Conversation | ✅ | ❌ | Copy messages |
RetrieveConversationMessage |
Conversation | ✅ | ❌ | Get single message |
RetrieveConversationMessages |
Conversation | ✅ | ❌ | Get multiple messages |
Advanced Patterns
Multi-Agent Orchestration
Sequential Agent Pipeline
Pass work through multiple agents in sequence.
#
# Sequential agent pipeline for content creation
#
kind: Workflow
trigger:
kind: OnConversationStart
id: content_workflow
actions:
# First agent: Research
- kind: InvokeAzureAgent
id: invoke_researcher
displayName: Research phase
conversationId: =System.ConversationId
agent:
name: ResearcherAgent
# Second agent: Write draft
- kind: InvokeAzureAgent
id: invoke_writer
displayName: Writing phase
conversationId: =System.ConversationId
agent:
name: WriterAgent
# Third agent: Edit
- kind: InvokeAzureAgent
id: invoke_editor
displayName: Editing phase
conversationId: =System.ConversationId
agent:
name: EditorAgent
C# Setup:
using Azure.AI.Projects;
using Azure.AI.Projects.OpenAI;
using Azure.Identity;
// Ensure agents exist in Azure AI Foundry
AIProjectClient aiProjectClient = new(foundryEndpoint, new DefaultAzureCredential());
await aiProjectClient.CreateAgentAsync(
agentName: "ResearcherAgent",
agentDefinition: new PromptAgentDefinition(modelName)
{
Instructions = "You are a research specialist..."
},
agentDescription: "Research agent for content pipeline");
// Create and run workflow
WorkflowFactory workflowFactory = new("content-pipeline.yaml", foundryEndpoint);
WorkflowRunner runner = new();
await runner.ExecuteAsync(workflowFactory.CreateWorkflow, "Create content about AI");
Conditional Agent Routing
Route requests to different agents based on conditions.
#
# Route to specialized support agents based on category
#
kind: Workflow
trigger:
kind: OnConversationStart
id: support_router
actions:
# Capture category from user input or set via another action
- kind: SetVariable
id: set_category
variable: Local.category
value: =System.LastMessage.Text
- kind: ConditionGroup
id: route_request
displayName: Route to appropriate agent
conditions:
- condition: =Local.category = "billing"
id: billing_route
actions:
- kind: InvokeAzureAgent
id: billing_agent
agent:
name: BillingAgent
conversationId: =System.ConversationId
- condition: =Local.category = "technical"
id: technical_route
actions:
- kind: InvokeAzureAgent
id: technical_agent
agent:
name: TechnicalAgent
conversationId: =System.ConversationId
elseActions:
- kind: InvokeAzureAgent
id: general_agent
agent:
name: GeneralAgent
conversationId: =System.ConversationId
Tool Integration Patterns
Pre-fetching Data with InvokeFunctionTool
Fetch data before calling an agent:
#
# Pre-fetch menu data before agent interaction
#
kind: Workflow
trigger:
kind: OnConversationStart
id: menu_workflow
actions:
# Pre-fetch today's specials
- kind: InvokeFunctionTool
id: get_specials
functionName: GetSpecials
requireApproval: true
output:
autoSend: true
result: Local.Specials
# Agent uses pre-fetched data
- kind: InvokeAzureAgent
id: menu_agent
conversationId: =System.ConversationId
agent:
name: MenuAgent
input:
messages: =UserMessage("Describe today's specials: " & Local.Specials)
MCP Tool Integration
Call external server using MCP:
#
# Search documentation using MCP
#
kind: Workflow
trigger:
kind: OnConversationStart
id: docs_search
actions:
- kind: SetVariable
variable: Local.SearchQuery
value: =System.LastMessage.Text
# Search Microsoft Learn
- kind: InvokeMcpTool
id: search_docs
serverUrl: https://learn.microsoft.com/api/mcp
toolName: microsoft_docs_search
conversationId: =System.ConversationId
arguments:
query: =Local.SearchQuery
output:
result: Local.SearchResults
autoSend: true
# Summarize results with agent
- kind: InvokeAzureAgent
id: summarize
agent:
name: SummaryAgent
conversationId: =System.ConversationId
input:
messages: =UserMessage("Summarize these search results")
Prerequisites
Before you begin, ensure you have:
- Python 3.10 - 3.13 (Python 3.14 is not yet supported due to PowerFx compatibility)
- The Agent Framework declarative package installed:
pip install agent-framework-declarative --pre
This package pulls in the underlying agent-framework-core automatically.
- Basic familiarity with YAML syntax
- Understanding of workflow concepts
Your First Declarative Workflow
Let's create a simple workflow that greets a user by name.
Step 1: Create the YAML File
Create a file named 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:
# 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, ", ", Workflow.Inputs.name, "!")
# Send the greeting to the user
- kind: SendActivity
id: send_greeting
displayName: Send greeting to user
activity:
text: =Local.message
# Store the result in outputs
- kind: SetVariable
id: set_output
displayName: Store result in outputs
variable: Workflow.Outputs.greeting
value: =Local.message
Step 2: Load and Run the Workflow
Create a Python file to execute the workflow:
import asyncio
from pathlib import Path
from agent_framework.declarative import WorkflowFactory
async def main() -> None:
"""Run the greeting workflow."""
# Create a workflow factory
factory = WorkflowFactory()
# Load the workflow from YAML
workflow_path = Path(__file__).parent / "greeting-workflow.yaml"
workflow = factory.create_workflow_from_yaml_path(workflow_path)
print(f"Loaded workflow: {workflow.name}")
print("-" * 40)
# Run with a name input
result = await workflow.run({"name": "Alice"})
for output in result.get_outputs():
print(f"Output: {output}")
if __name__ == "__main__":
asyncio.run(main())
Expected Output
Loaded workflow: greeting-workflow
----------------------------------------
Output: Hello, Alice!
Core Concepts
Variable Namespaces
Declarative workflows use namespaced variables to organize state:
| Namespace | Description | Example |
|---|---|---|
Local.* |
Variables local to the workflow | Local.message |
Workflow.Inputs.* |
Input parameters | Workflow.Inputs.name |
Workflow.Outputs.* |
Output values | Workflow.Outputs.result |
System.* |
System-provided values | System.ConversationId |
Expression Language
Values prefixed with = are evaluated as expressions:
# Literal value (no evaluation)
value: Hello
# Expression (evaluated at runtime)
value: =Concat("Hello, ", Workflow.Inputs.name)
Common functions include:
Concat(str1, str2, ...)- Concatenate stringsIf(condition, trueValue, falseValue)- Conditional expressionIsBlank(value)- Check if value is empty
Action Types
Declarative workflows support various action types:
| Category | Actions |
|---|---|
| Variable Management | SetVariable, SetMultipleVariables, AppendValue, ResetVariable |
| Control Flow | If, ConditionGroup, Foreach, RepeatUntil, BreakLoop, ContinueLoop, GotoAction |
| Output | SendActivity, EmitEvent |
| Agent Invocation | InvokeAzureAgent |
| Tool Invocation | InvokeFunctionTool |
| Human-in-the-Loop | Question, Confirmation, RequestExternalInput, WaitForInput |
| Workflow Control | EndWorkflow, EndConversation, CreateConversation |
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(Workflow.Inputs.firstName, " ", Workflow.Inputs.lastName)
Properties:
| Property | Required | Description |
|---|---|---|
variable |
Yes | Variable path (e.g., Local.name, Workflow.Outputs.result) |
value |
Yes | Value to set (literal or expression) |
Note
Python also supports the SetValue action kind, which uses path instead of variable for the target property. Both SetVariable (with variable) and SetValue (with path) achieve the same result. For example:
- kind: SetValue
id: set_greeting
path: Local.greeting
value: Hello World
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 ", Workflow.Inputs.orderId)
Properties:
| Property | Required | Description |
|---|---|---|
variables |
Yes | Map of variable paths to values |
AppendValue
Appends a value to a list or concatenates to a string.
- kind: AppendValue
id: add_item
variable: Local.items
value: =Workflow.Inputs.newItem
Properties:
| Property | Required | Description |
|---|---|---|
variable |
Yes | Variable path to append to |
value |
Yes | Value to append |
ResetVariable
Clears a variable's value.
- kind: ResetVariable
id: clear_counter
variable: Local.counter
Properties:
| Property | Required | Description |
|---|---|---|
variable |
Yes | Variable path to reset |
Control Flow Actions
If
Executes actions conditionally based on a condition.
- kind: If
id: check_age
displayName: Check user age
condition: =Workflow.Inputs.age >= 18
then:
- kind: SendActivity
activity:
text: "Welcome, adult user!"
else:
- kind: SendActivity
activity:
text: "Welcome, young user!"
Nested conditions:
- kind: If
condition: =Workflow.Inputs.role = "admin"
then:
- kind: SendActivity
activity:
text: "Admin access granted"
else:
- kind: If
condition: =Workflow.Inputs.role = "user"
then:
- kind: SendActivity
activity:
text: "User access granted"
else:
- kind: SendActivity
activity:
text: "Access denied"
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: =Workflow.Inputs.category = "electronics"
id: electronics_branch
actions:
- kind: SetVariable
variable: Local.department
value: Electronics Team
- condition: =Workflow.Inputs.category = "clothing"
id: clothing_branch
actions:
- kind: SetVariable
variable: Local.department
value: Clothing Team
- condition: =Workflow.Inputs.category = "food"
id: food_branch
actions:
- kind: SetVariable
variable: Local.department
value: Food 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: =Workflow.Inputs.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 |
RepeatUntil
Repeats actions until a condition becomes true.
- kind: SetVariable
variable: Local.counter
value: 0
- kind: RepeatUntil
id: count_loop
displayName: Count to 5
condition: =Local.counter >= 5
actions:
- kind: SetVariable
variable: Local.counter
value: =Local.counter + 1
- kind: SendActivity
activity:
text: =Concat("Counter: ", Local.counter)
Properties:
| Property | Required | Description |
|---|---|---|
condition |
Yes | Loop continues until this is true |
actions |
Yes | Actions to repeat |
BreakLoop
Exits the current loop immediately.
- kind: Foreach
source: =Workflow.Inputs.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: =Workflow.Inputs.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, ", Workflow.Inputs.name, "! How can I help you today?")
Properties:
| Property | Required | Description |
|---|---|---|
activity |
Yes | The activity to send |
activity.text |
Yes | Message text (literal or expression) |
EmitEvent
Emits a custom event.
- kind: EmitEvent
id: emit_status
displayName: Emit status event
eventType: order_status_changed
data:
orderId: =Workflow.Inputs.orderId
status: =Local.newStatus
Properties:
| Property | Required | Description |
|---|---|---|
eventType |
Yes | Type identifier for the event |
data |
No | Event payload data |
Agent Invocation Actions
InvokeAzureAgent
Invokes an Azure AI 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: =Workflow.Inputs.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 Invocation Actions
InvokeFunctionTool
Invokes a registered Python function directly from the workflow without going through an AI agent.
- kind: InvokeFunctionTool
id: invoke_weather
displayName: Get weather data
functionName: get_weather
arguments:
location: =Local.location
unit: =Local.unit
output:
result: Local.weatherInfo
messages: Local.weatherToolCallItems
autoSend: true
Properties:
| Property | Required | Description |
|---|---|---|
functionName |
Yes | Name of the registered function to invoke |
arguments |
No | Arguments to pass to the function |
output.result |
No | Path to store the function result |
output.messages |
No | Path to store function messages |
output.autoSend |
No | Automatically send result to user |
Python setup for InvokeFunctionTool:
Functions must be registered with the WorkflowFactory using register_tool:
from agent_framework.declarative import WorkflowFactory
# Define your functions
def get_weather(location: str, unit: str = "F") -> dict:
"""Get weather information for a location."""
# Your implementation here
return {"location": location, "temp": 72, "unit": unit}
def format_message(template: str, data: dict) -> str:
"""Format a message template with data."""
return template.format(**data)
# Register functions with the factory
factory = (
WorkflowFactory()
.register_tool("get_weather", get_weather)
.register_tool("format_message", format_message)
)
# Load and run the workflow
workflow = factory.create_workflow_from_yaml_path("workflow.yaml")
result = await workflow.run({"location": "Seattle", "unit": "F"})
Human-in-the-Loop Actions
Question
Asks the user a question and stores the response.
- kind: Question
id: ask_name
displayName: Ask for user name
question:
text: "What is your name?"
variable: Local.userName
default: "Guest"
Properties:
| Property | Required | Description |
|---|---|---|
question.text |
Yes | The question to ask |
variable |
Yes | Path to store the response |
default |
No | Default value if no response |
Confirmation
Asks the user for a yes/no confirmation.
- kind: Confirmation
id: confirm_delete
displayName: Confirm deletion
question:
text: "Are you sure you want to delete this item?"
variable: Local.confirmed
Properties:
| Property | Required | Description |
|---|---|---|
question.text |
Yes | The confirmation question |
variable |
Yes | Path to store boolean result |
RequestExternalInput
Requests input from an external system or process.
- kind: RequestExternalInput
id: request_approval
displayName: Request manager approval
prompt:
text: "Please provide approval for this request."
variable: Local.approvalResult
default: "pending"
Properties:
| Property | Required | Description |
|---|---|---|
prompt.text |
Yes | Description of required input |
variable |
Yes | Path to store the input |
default |
No | Default value |
WaitForInput
Pauses the workflow and waits for external input.
- kind: WaitForInput
id: wait_for_response
variable: Local.externalResponse
Properties:
| Property | Required | Description |
|---|---|---|
variable |
Yes | Path to store the input when received |
Workflow Control Actions
EndWorkflow
Terminates the workflow execution.
- kind: EndWorkflow
id: finish
displayName: End workflow
EndConversation
Ends the current conversation.
- kind: EndConversation
id: end_chat
displayName: End conversation
CreateConversation
Creates a new conversation context.
- kind: CreateConversation
id: create_new_conv
displayName: Create new conversation
conversationId: Local.NewConversationId
Properties:
| Property | Required | Description |
|---|---|---|
conversationId |
Yes | Path to store the new conversation ID |
Actions Quick Reference
| Action | Category | Description |
|---|---|---|
SetVariable |
Variable | Set a single variable |
SetMultipleVariables |
Variable | Set multiple variables |
AppendValue |
Variable | Append to list/string |
ResetVariable |
Variable | Clear a variable |
If |
Control Flow | Conditional branching |
ConditionGroup |
Control Flow | Multi-branch switch |
Foreach |
Control Flow | Iterate over collection |
RepeatUntil |
Control Flow | Loop until condition |
BreakLoop |
Control Flow | Exit current loop |
ContinueLoop |
Control Flow | Skip to next iteration |
GotoAction |
Control Flow | Jump to action by ID |
SendActivity |
Output | Send message to user |
EmitEvent |
Output | Emit custom event |
InvokeAzureAgent |
Agent | Call Azure AI agent |
InvokeFunctionTool |
Tool | Invoke registered function |
Question |
Human-in-the-Loop | Ask user a question |
Confirmation |
Human-in-the-Loop | Yes/no confirmation |
RequestExternalInput |
Human-in-the-Loop | Request external input |
WaitForInput |
Human-in-the-Loop | Wait for input |
EndWorkflow |
Workflow Control | Terminate workflow |
EndConversation |
Workflow Control | End conversation |
CreateConversation |
Workflow Control | Create new conversation |
Expression Syntax
Declarative workflows use a PowerFx-like expression language to manage state and compute dynamic values. Values prefixed with = are evaluated as expressions at runtime.
Variable Namespace Details
| Namespace | Description | Access |
|---|---|---|
Local.* |
Workflow-local variables | Read/Write |
Workflow.Inputs.* |
Input parameters passed to the workflow | Read-only |
Workflow.Outputs.* |
Values returned from the workflow | Read/Write |
System.* |
System-provided values | Read-only |
Agent.* |
Results from agent invocations | Read-only |
System Variables
| Variable | Description |
|---|---|
System.ConversationId |
Current conversation identifier |
System.LastMessage |
The most recent message |
System.Timestamp |
Current timestamp |
Agent Variables
After invoking an agent, access response data through the output variable:
actions:
- kind: InvokeAzureAgent
id: call_assistant
agent:
name: MyAgent
output:
responseObject: Local.AgentResult
# Access agent response
- kind: SendActivity
activity:
text: =Local.AgentResult.text
Literal vs. Expression Values
# Literal string (stored as-is)
value: Hello World
# Expression (evaluated at runtime)
value: =Concat("Hello ", Workflow.Inputs.name)
# Literal number
value: 42
# Expression returning a number
value: =Workflow.Inputs.quantity * 2
String Operations
Concat
Concatenate multiple strings:
value: =Concat("Hello, ", Workflow.Inputs.name, "!")
# Result: "Hello, Alice!" (if Workflow.Inputs.name is "Alice")
value: =Concat(Local.firstName, " ", Local.lastName)
# Result: "John Doe" (if firstName is "John" and lastName is "Doe")
IsBlank
Check if a value is empty or undefined:
condition: =IsBlank(Workflow.Inputs.optionalParam)
# Returns true if the parameter is not provided
value: =If(IsBlank(Workflow.Inputs.name), "Guest", Workflow.Inputs.name)
# Returns "Guest" if name is blank, otherwise returns the name
Conditional Expressions
If Function
Return different values based on a condition:
value: =If(Workflow.Inputs.age < 18, "minor", "adult")
value: =If(Local.count > 0, "Items found", "No items")
# Nested conditions
value: =If(Workflow.Inputs.role = "admin", "Full access", If(Workflow.Inputs.role = "user", "Limited access", "No access"))
Comparison Operators
| Operator | Description | Example |
|---|---|---|
= |
Equal to | =Workflow.Inputs.status = "active" |
<> |
Not equal to | =Workflow.Inputs.status <> "deleted" |
< |
Less than | =Workflow.Inputs.age < 18 |
> |
Greater than | =Workflow.Inputs.count > 0 |
<= |
Less than or equal | =Workflow.Inputs.score <= 100 |
>= |
Greater than or equal | =Workflow.Inputs.quantity >= 1 |
Boolean Functions
# Or - returns true if any condition is true
condition: =Or(Workflow.Inputs.role = "admin", Workflow.Inputs.role = "moderator")
# And - returns true if all conditions are true
condition: =And(Workflow.Inputs.age >= 18, Workflow.Inputs.hasConsent)
# Not - negates a condition
condition: =Not(IsBlank(Workflow.Inputs.email))
Mathematical Operations
# Addition
value: =Workflow.Inputs.price + Workflow.Inputs.tax
# Subtraction
value: =Workflow.Inputs.total - Workflow.Inputs.discount
# Multiplication
value: =Workflow.Inputs.quantity * Workflow.Inputs.unitPrice
# Division
value: =Workflow.Inputs.total / Workflow.Inputs.count
Practical Expression Examples
User Categorization
name: categorize-user
inputs:
age:
type: integer
description: User's age
actions:
- kind: SetVariable
variable: Local.age
value: =Workflow.Inputs.age
- kind: SetVariable
variable: Local.category
value: =If(Local.age < 13, "child", If(Local.age < 20, "teenager", If(Local.age < 65, "adult", "senior")))
- kind: SendActivity
activity:
text: =Concat("You are categorized as: ", Local.category)
- kind: SetVariable
variable: Workflow.Outputs.category
value: =Local.category
Conditional Greeting
name: smart-greeting
inputs:
name:
type: string
description: User's name (optional)
timeOfDay:
type: string
description: morning, afternoon, or evening
actions:
# Set the greeting based on time of day
- kind: SetVariable
variable: Local.timeGreeting
value: =If(Workflow.Inputs.timeOfDay = "morning", "Good morning", If(Workflow.Inputs.timeOfDay = "afternoon", "Good afternoon", "Good evening"))
# Handle optional name
- kind: SetVariable
variable: Local.userName
value: =If(IsBlank(Workflow.Inputs.name), "friend", Workflow.Inputs.name)
# Build the full greeting
- kind: SetVariable
variable: Local.fullGreeting
value: =Concat(Local.timeGreeting, ", ", Local.userName, "!")
- kind: SendActivity
activity:
text: =Local.fullGreeting
Input Validation
name: validate-order
inputs:
quantity:
type: integer
description: Number of items to order
email:
type: string
description: Customer email
actions:
# Check if inputs are valid
- kind: SetVariable
variable: Local.isValidQuantity
value: =And(Workflow.Inputs.quantity > 0, Workflow.Inputs.quantity <= 100)
- kind: SetVariable
variable: Local.hasEmail
value: =Not(IsBlank(Workflow.Inputs.email))
- kind: SetVariable
variable: Local.isValid
value: =And(Local.isValidQuantity, Local.hasEmail)
- kind: If
condition: =Local.isValid
then:
- kind: SendActivity
activity:
text: "Order validated successfully!"
else:
- kind: SendActivity
activity:
text: =If(Not(Local.isValidQuantity), "Invalid quantity (must be 1-100)", "Email is required")
Advanced Patterns
As your workflows grow in complexity, you'll need patterns that handle multi-step processes, agent coordination, and interactive scenarios.
Multi-Agent Orchestration
Sequential Agent Pipeline
Pass work through multiple agents in sequence, where each agent builds on the previous agent's output.
Use case: Content creation pipelines where different specialists handle research, writing, and editing.
name: content-pipeline
description: Sequential agent pipeline for content creation
kind: Workflow
trigger:
kind: OnConversationStart
id: content_workflow
actions:
# First agent: Research and analyze
- kind: InvokeAzureAgent
id: invoke_researcher
displayName: Research phase
conversationId: =System.ConversationId
agent:
name: ResearcherAgent
# Second agent: Write draft based on research
- kind: InvokeAzureAgent
id: invoke_writer
displayName: Writing phase
conversationId: =System.ConversationId
agent:
name: WriterAgent
# Third agent: Edit and polish
- kind: InvokeAzureAgent
id: invoke_editor
displayName: Editing phase
conversationId: =System.ConversationId
agent:
name: EditorAgent
Python setup:
from agent_framework.declarative import WorkflowFactory
# Create factory and register agents
factory = WorkflowFactory()
factory.register_agent("ResearcherAgent", researcher_agent)
factory.register_agent("WriterAgent", writer_agent)
factory.register_agent("EditorAgent", editor_agent)
# Load and run
workflow = factory.create_workflow_from_yaml_path("content-pipeline.yaml")
result = await workflow.run({"topic": "AI in healthcare"})
Conditional Agent Routing
Route requests to different agents based on the input or intermediate results.
Use case: Support systems that route to specialized agents based on issue type.
name: support-router
description: Route to specialized support agents
inputs:
category:
type: string
description: Support category (billing, technical, general)
actions:
- kind: ConditionGroup
id: route_request
displayName: Route to appropriate agent
conditions:
- condition: =Workflow.Inputs.category = "billing"
id: billing_route
actions:
- kind: InvokeAzureAgent
id: billing_agent
agent:
name: BillingAgent
conversationId: =System.ConversationId
- condition: =Workflow.Inputs.category = "technical"
id: technical_route
actions:
- kind: InvokeAzureAgent
id: technical_agent
agent:
name: TechnicalAgent
conversationId: =System.ConversationId
elseActions:
- kind: InvokeAzureAgent
id: general_agent
agent:
name: GeneralAgent
conversationId: =System.ConversationId
Agent with External Loop
Continue agent interaction until a condition is met, such as the issue being resolved.
Use case: Support conversations that continue until the user's problem is solved.
name: support-conversation
description: Continue support until resolved
actions:
- kind: SetVariable
variable: Local.IsResolved
value: false
- kind: InvokeAzureAgent
id: support_agent
displayName: Support agent with external loop
agent:
name: SupportAgent
conversationId: =System.ConversationId
input:
externalLoop:
when: =Not(Local.IsResolved)
output:
responseObject: Local.SupportResult
- kind: SendActivity
activity:
text: "Thank you for contacting support. Your issue has been resolved."
Loop Control Patterns
Iterative Agent Conversation
Create back-and-forth conversations between agents with controlled iteration.
Use case: Student-teacher scenarios, debate simulations, or iterative refinement.
name: student-teacher
description: Iterative learning conversation between student and teacher
kind: Workflow
trigger:
kind: OnConversationStart
id: learning_session
actions:
# Initialize turn counter
- kind: SetVariable
id: init_counter
variable: Local.TurnCount
value: 0
- kind: SendActivity
id: start_message
activity:
text: =Concat("Starting session for: ", Workflow.Inputs.problem)
# Student attempts solution (loop entry point)
- kind: SendActivity
id: student_label
activity:
text: "\n[Student]:"
- kind: InvokeAzureAgent
id: student_attempt
conversationId: =System.ConversationId
agent:
name: StudentAgent
# Teacher reviews
- kind: SendActivity
id: teacher_label
activity:
text: "\n[Teacher]:"
- kind: InvokeAzureAgent
id: teacher_review
conversationId: =System.ConversationId
agent:
name: TeacherAgent
output:
messages: Local.TeacherResponse
# Increment counter
- kind: SetVariable
id: increment
variable: Local.TurnCount
value: =Local.TurnCount + 1
# Check completion conditions
- kind: ConditionGroup
id: check_completion
conditions:
# Success: Teacher congratulated student
- condition: =Not(IsBlank(Find("congratulations", Local.TeacherResponse)))
id: success_check
actions:
- kind: SendActivity
activity:
text: "Session complete - student succeeded!"
- kind: SetVariable
variable: Workflow.Outputs.result
value: success
# Continue: Under turn limit
- condition: =Local.TurnCount < 4
id: continue_check
actions:
- kind: GotoAction
actionId: student_label
elseActions:
# Timeout: Reached turn limit
- kind: SendActivity
activity:
text: "Session ended - turn limit reached."
- kind: SetVariable
variable: Workflow.Outputs.result
value: timeout
Counter-Based Loops
Implement traditional counting loops using variables and GotoAction.
name: counter-loop
description: Process items with a counter
actions:
- kind: SetVariable
variable: Local.counter
value: 0
- kind: SetVariable
variable: Local.maxIterations
value: 5
# Loop start
- kind: SetVariable
id: loop_start
variable: Local.counter
value: =Local.counter + 1
- kind: SendActivity
activity:
text: =Concat("Processing iteration ", Local.counter)
# Your processing logic here
- kind: SetVariable
variable: Local.result
value: =Concat("Result from iteration ", Local.counter)
# Check if should continue
- kind: If
condition: =Local.counter < Local.maxIterations
then:
- kind: GotoAction
actionId: loop_start
else:
- kind: SendActivity
activity:
text: "Loop complete!"
Early Exit with BreakLoop
Use BreakLoop to exit iterations early when a condition is met.
name: search-workflow
description: Search through items and stop when found
actions:
- kind: SetVariable
variable: Local.found
value: false
- kind: Foreach
source: =Workflow.Inputs.items
itemName: currentItem
actions:
# Check if this is the item we're looking for
- kind: If
condition: =currentItem.id = Workflow.Inputs.targetId
then:
- kind: SetVariable
variable: Local.found
value: true
- kind: SetVariable
variable: Local.result
value: =currentItem
- kind: BreakLoop
- kind: SendActivity
activity:
text: =Concat("Checked item: ", currentItem.name)
- kind: If
condition: =Local.found
then:
- kind: SendActivity
activity:
text: =Concat("Found: ", Local.result.name)
else:
- kind: SendActivity
activity:
text: "Item not found"
Human-in-the-Loop Patterns
Interactive Survey
Collect multiple pieces of information from the user.
name: customer-survey
description: Interactive customer feedback survey
actions:
- kind: SendActivity
activity:
text: "Welcome to our customer feedback survey!"
# Collect name
- kind: Question
id: ask_name
question:
text: "What is your name?"
variable: Local.userName
default: "Anonymous"
- kind: SendActivity
activity:
text: =Concat("Nice to meet you, ", Local.userName, "!")
# Collect rating
- kind: Question
id: ask_rating
question:
text: "How would you rate our service? (1-5)"
variable: Local.rating
default: "3"
# Respond based on rating
- kind: If
condition: =Local.rating >= 4
then:
- kind: SendActivity
activity:
text: "Thank you for the positive feedback!"
else:
- kind: Question
id: ask_improvement
question:
text: "What could we improve?"
variable: Local.feedback
# Collect additional feedback
- kind: RequestExternalInput
id: additional_comments
prompt:
text: "Any additional comments? (optional)"
variable: Local.comments
default: ""
# Summary
- kind: SendActivity
activity:
text: =Concat("Thank you, ", Local.userName, "! Your feedback has been recorded.")
- kind: SetVariable
variable: Workflow.Outputs.survey
value:
name: =Local.userName
rating: =Local.rating
feedback: =Local.feedback
comments: =Local.comments
Approval Workflow
Request approval before proceeding with an action.
name: approval-workflow
description: Request approval before processing
inputs:
requestType:
type: string
description: Type of request
amount:
type: number
description: Request amount
actions:
- kind: SendActivity
activity:
text: =Concat("Processing ", Workflow.Inputs.requestType, " request for $", Workflow.Inputs.amount)
# Check if approval is needed
- kind: If
condition: =Workflow.Inputs.amount > 1000
then:
- kind: SendActivity
activity:
text: "This request requires manager approval."
- kind: Confirmation
id: get_approval
question:
text: =Concat("Do you approve this ", Workflow.Inputs.requestType, " request for $", Workflow.Inputs.amount, "?")
variable: Local.approved
- kind: If
condition: =Local.approved
then:
- kind: SendActivity
activity:
text: "Request approved. Processing..."
- kind: SetVariable
variable: Workflow.Outputs.status
value: approved
else:
- kind: SendActivity
activity:
text: "Request denied."
- kind: SetVariable
variable: Workflow.Outputs.status
value: denied
else:
- kind: SendActivity
activity:
text: "Request auto-approved (under threshold)."
- kind: SetVariable
variable: Workflow.Outputs.status
value: auto_approved
Complex Orchestration
Support Ticket Workflow
A comprehensive example combining multiple patterns: agent routing, conditional logic, and conversation management.
name: support-ticket-workflow
description: Complete support ticket handling with escalation
kind: Workflow
trigger:
kind: OnConversationStart
id: support_workflow
actions:
# Initial self-service agent
- kind: InvokeAzureAgent
id: self_service
displayName: Self-service agent
agent:
name: SelfServiceAgent
conversationId: =System.ConversationId
input:
externalLoop:
when: =Not(Local.ServiceResult.IsResolved)
output:
responseObject: Local.ServiceResult
# Check if resolved by self-service
- kind: If
condition: =Local.ServiceResult.IsResolved
then:
- kind: SendActivity
activity:
text: "Issue resolved through self-service."
- kind: SetVariable
variable: Workflow.Outputs.resolution
value: self_service
- kind: EndWorkflow
id: end_resolved
# Create support ticket
- kind: SendActivity
activity:
text: "Creating support ticket..."
- kind: SetVariable
variable: Local.TicketId
value: =Concat("TKT-", System.ConversationId)
# Route to appropriate team
- kind: ConditionGroup
id: route_ticket
conditions:
- condition: =Local.ServiceResult.Category = "technical"
id: technical_route
actions:
- kind: InvokeAzureAgent
id: technical_support
agent:
name: TechnicalSupportAgent
conversationId: =System.ConversationId
output:
responseObject: Local.TechResult
- condition: =Local.ServiceResult.Category = "billing"
id: billing_route
actions:
- kind: InvokeAzureAgent
id: billing_support
agent:
name: BillingSupportAgent
conversationId: =System.ConversationId
output:
responseObject: Local.BillingResult
elseActions:
# Escalate to human
- kind: SendActivity
activity:
text: "Escalating to human support..."
- kind: SetVariable
variable: Workflow.Outputs.resolution
value: escalated
- kind: SendActivity
activity:
text: =Concat("Ticket ", Local.TicketId, " has been processed.")
Best Practices
Naming Conventions
Use clear, descriptive names for actions and variables:
# Good
- kind: SetVariable
id: calculate_total_price
variable: Local.orderTotal
# Avoid
- kind: SetVariable
id: sv1
variable: Local.x
Organizing Large Workflows
Break complex workflows into logical sections with comments:
actions:
# === INITIALIZATION ===
- kind: SetVariable
id: init_status
variable: Local.status
value: started
# === DATA COLLECTION ===
- kind: Question
id: collect_name
# ...
# === PROCESSING ===
- kind: InvokeAzureAgent
id: process_request
# ...
# === OUTPUT ===
- kind: SendActivity
id: send_result
# ...
Error Handling
Use conditional checks to handle potential issues:
actions:
- kind: SetVariable
variable: Local.hasError
value: false
- kind: InvokeAzureAgent
id: call_agent
agent:
name: ProcessingAgent
output:
responseObject: Local.AgentResult
- kind: If
condition: =IsBlank(Local.AgentResult)
then:
- kind: SetVariable
variable: Local.hasError
value: true
- kind: SendActivity
activity:
text: "An error occurred during processing."
else:
- kind: SendActivity
activity:
text: =Local.AgentResult.message
Testing Strategies
- Start simple: Test basic flows before adding complexity
- Use default values: Provide sensible defaults for inputs
- Add logging: Use SendActivity for debugging during development
- Test edge cases: Verify behavior with missing or invalid inputs
# Debug logging example
- kind: SendActivity
id: debug_log
activity:
text: =Concat("[DEBUG] Current state: counter=", Local.counter, ", status=", Local.status)
Next Steps
- C# Declarative Workflow Samples - Explore complete working examples including:
- StudentTeacher - Multi-agent conversation with iterative learning
- InvokeMcpTool - MCP server tool integration
- InvokeFunctionTool - Direct function invocation from workflows
- FunctionTools - Agent with function tools
- ToolApproval - Human approval for tool execution
- CustomerSupport - Complex support ticket workflow
- DeepResearch - Research workflow with multiple agents
- Python Declarative Workflow Samples - Explore complete working examples