diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 8b1b00fd2b..bed8bebe96 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -34,6 +34,7 @@
+
diff --git a/dotnet/samples/AzureFunctions/01_SingleAgent/01_SingleAgent.csproj b/dotnet/samples/AzureFunctions/01_SingleAgent/01_SingleAgent.csproj
index 99f78cc1ab..eedb524a38 100644
--- a/dotnet/samples/AzureFunctions/01_SingleAgent/01_SingleAgent.csproj
+++ b/dotnet/samples/AzureFunctions/01_SingleAgent/01_SingleAgent.csproj
@@ -6,8 +6,8 @@
enable
enable
- SingleAgent
- SingleAgent
+ Workflow
+ Workflow
diff --git a/dotnet/samples/AzureFunctions/01_SingleAgent/demo.http b/dotnet/samples/AzureFunctions/01_SingleAgent/demo.http
index 3b741adf31..aaf939c6cd 100644
--- a/dotnet/samples/AzureFunctions/01_SingleAgent/demo.http
+++ b/dotnet/samples/AzureFunctions/01_SingleAgent/demo.http
@@ -2,7 +2,7 @@
@authority=http://localhost:7071
### Prompt the agent
-POST {{authority}}/api/agents/Joker/run
+POST {{authority}}/api/workflows/MyTestWorkflow/run
Content-Type: text/plain
-Tell me a joke about a pirate.
+Hello world
diff --git a/dotnet/samples/AzureFunctions/09_Workflow/09_Workflow.csproj b/dotnet/samples/AzureFunctions/09_Workflow/09_Workflow.csproj
new file mode 100644
index 0000000000..ec1dca7683
--- /dev/null
+++ b/dotnet/samples/AzureFunctions/09_Workflow/09_Workflow.csproj
@@ -0,0 +1,44 @@
+
+
+ net10.0
+ v4
+ Exe
+ enable
+ enable
+
+ SingleAgent
+ SingleAgent
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/AzureFunctions/09_Workflow/OrchFunction.cs b/dotnet/samples/AzureFunctions/09_Workflow/OrchFunction.cs
new file mode 100644
index 0000000000..c20fa550b4
--- /dev/null
+++ b/dotnet/samples/AzureFunctions/09_Workflow/OrchFunction.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Azure.Functions.Worker.Http;
+using Microsoft.DurableTask;
+using Microsoft.DurableTask.Client;
+using Microsoft.Extensions.Logging;
+
+namespace SingleAgent;
+
+public static class OrchFunction
+{
+ [Function(nameof(OrchFunction))]
+ public static async Task> RunOrchestratorAsync(
+ [OrchestrationTrigger] TaskOrchestrationContext context)
+ {
+ ILogger logger = context.CreateReplaySafeLogger(nameof(OrchFunction));
+ logger.LogInformation("Saying hello.");
+ var outputs = new List();
+
+ outputs.Add("Tokyo");
+
+ return outputs;
+ }
+
+ [Function("OrchFunction_HttpStart")]
+ public static async Task HttpStartAsync(
+ [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req,
+ [DurableClient] DurableTaskClient client,
+ FunctionContext executionContext)
+ {
+ // Function input comes from the request content.
+ string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(
+ nameof(OrchFunction));
+
+ // Returns an HTTP 202 response with an instance management payload.
+ // See https://learn.microsoft.com/azure/azure-functions/durable/durable-functions-http-api#start-orchestration
+ return await client.CreateCheckStatusResponseAsync(req, instanceId);
+ }
+}
diff --git a/dotnet/samples/AzureFunctions/09_Workflow/Program.cs b/dotnet/samples/AzureFunctions/09_Workflow/Program.cs
new file mode 100644
index 0000000000..87ce302293
--- /dev/null
+++ b/dotnet/samples/AzureFunctions/09_Workflow/Program.cs
@@ -0,0 +1,53 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Azure;
+using Azure.AI.OpenAI;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.Hosting.AzureFunctions;
+using Microsoft.Agents.AI.Workflows;
+using Microsoft.Azure.Functions.Worker.Builder;
+using Microsoft.Extensions.Hosting;
+using OpenAI.Chat;
+
+// Get the Azure OpenAI endpoint and deployment name from environment variables.
+string endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
+ ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
+string deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT")
+ ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT is not set.");
+
+// Use Azure Key Credential if provided, otherwise use Azure CLI Credential.
+string? azureOpenAiKey = System.Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY");
+AzureOpenAIClient client = !string.IsNullOrEmpty(azureOpenAiKey)
+ ? new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(azureOpenAiKey))
+ : new AzureOpenAIClient(new Uri(endpoint), new AzureCliCredential());
+
+// Set up an AI agent following the standard Microsoft Agent Framework pattern.
+const string JokerName = "Joker";
+const string JokerInstructions = "You are good at telling jokes.";
+
+AIAgent agent = client.GetChatClient(deploymentName).CreateAIAgent(JokerInstructions, JokerName);
+
+Func uppercaseFunc = s => s.ToUpperInvariant();
+var uppercase = uppercaseFunc.BindAsExecutor("UppercaseExecutor");
+
+Func reverseTextFunc = s => s.ToUpperInvariant();
+var reverse = reverseTextFunc.BindAsExecutor("ReverseTextExecutor");
+
+WorkflowBuilder builder = new(uppercase);
+builder.AddEdge(uppercase, reverse).WithOutputFrom(reverse);
+var workflow = builder.WithName("MyTestWorkflow").Build();
+
+// Configure the function app to host the AI agent.
+// This will automatically generate HTTP API endpoints for the agent.
+using IHost app = FunctionsApplication
+ .CreateBuilder(args)
+ .ConfigureFunctionsWebApplication()
+ .ConfigureDurableAgents(options => options.AddAIAgent(agent, timeToLive: TimeSpan.FromHours(1)))
+ .AddDurableWorkflows(options =>
+ {
+ // Configure durable workflow options here if needed.
+ options.AddWorkflow(workflow);
+ })
+ .Build();
+app.Run();
diff --git a/dotnet/samples/AzureFunctions/09_Workflow/README.md b/dotnet/samples/AzureFunctions/09_Workflow/README.md
new file mode 100644
index 0000000000..d4ac968978
--- /dev/null
+++ b/dotnet/samples/AzureFunctions/09_Workflow/README.md
@@ -0,0 +1,89 @@
+# Single Agent Sample
+
+This sample demonstrates how to use the Durable Agent Framework (DAFx) to create a simple Azure Functions app that hosts a single AI agent and provides direct HTTP API access for interactive conversations.
+
+## Key Concepts Demonstrated
+
+- Using the Microsoft Agent Framework to define a simple AI agent with a name and instructions.
+- Registering agents with the Function app and running them using HTTP.
+- Conversation management (via session IDs) for isolated interactions.
+
+## Environment Setup
+
+See the [README.md](../README.md) file in the parent directory for more information on how to configure the environment, including how to install and run common sample dependencies.
+
+## Running the Sample
+
+With the environment setup and function app running, you can test the sample by sending an HTTP request to the agent endpoint.
+
+You can use the `demo.http` file to send a message to the agent, or a command line tool like `curl` as shown below:
+
+Bash (Linux/macOS/WSL):
+
+```bash
+curl -X POST http://localhost:7071/api/agents/Joker/run \
+ -H "Content-Type: text/plain" \
+ -d "Tell me a joke about a pirate."
+```
+
+PowerShell:
+
+```powershell
+Invoke-RestMethod -Method Post `
+ -Uri http://localhost:7071/api/agents/Joker/run `
+ -ContentType text/plain `
+ -Body "Tell me a joke about a pirate."
+```
+
+You can also send JSON requests:
+
+```bash
+curl -X POST http://localhost:7071/api/agents/Joker/run \
+ -H "Content-Type: application/json" \
+ -H "Accept: application/json" \
+ -d '{"message": "Tell me a joke about a pirate."}'
+```
+
+To continue a conversation, include the `thread_id` in the query string or JSON body:
+
+```bash
+curl -X POST "http://localhost:7071/api/agents/Joker/run?thread_id=your-thread-id" \
+ -H "Content-Type: application/json" \
+ -H "Accept: application/json" \
+ -d '{"message": "Tell me another one."}'
+```
+
+The response from the agent will be displayed in the terminal where you ran `func start`. The expected `text/plain` output will look something like:
+
+```text
+Why don't pirates ever learn the alphabet? Because they always get stuck at "C"!
+```
+
+The expected `application/json` output will look something like:
+
+```json
+{
+ "status": 200,
+ "thread_id": "ee6e47a0-f24b-40b1-ade8-16fcebb9eb40",
+ "response": {
+ "Messages": [
+ {
+ "AuthorName": "Joker",
+ "CreatedAt": "2025-11-11T12:00:00.0000000Z",
+ "Role": "assistant",
+ "Contents": [
+ {
+ "Type": "text",
+ "Text": "Why don't pirates ever learn the alphabet? Because they always get stuck at 'C'!"
+ }
+ ]
+ }
+ ],
+ "Usage": {
+ "InputTokenCount": 78,
+ "OutputTokenCount": 36,
+ "TotalTokenCount": 114
+ }
+ }
+}
+```
diff --git a/dotnet/samples/AzureFunctions/09_Workflow/demo.http b/dotnet/samples/AzureFunctions/09_Workflow/demo.http
new file mode 100644
index 0000000000..3b741adf31
--- /dev/null
+++ b/dotnet/samples/AzureFunctions/09_Workflow/demo.http
@@ -0,0 +1,8 @@
+# Default endpoint address for local testing
+@authority=http://localhost:7071
+
+### Prompt the agent
+POST {{authority}}/api/agents/Joker/run
+Content-Type: text/plain
+
+Tell me a joke about a pirate.
diff --git a/dotnet/samples/AzureFunctions/09_Workflow/host.json b/dotnet/samples/AzureFunctions/09_Workflow/host.json
new file mode 100644
index 0000000000..9384a0a583
--- /dev/null
+++ b/dotnet/samples/AzureFunctions/09_Workflow/host.json
@@ -0,0 +1,20 @@
+{
+ "version": "2.0",
+ "logging": {
+ "logLevel": {
+ "Microsoft.Agents.AI.DurableTask": "Information",
+ "Microsoft.Agents.AI.Hosting.AzureFunctions": "Information",
+ "DurableTask": "Information",
+ "Microsoft.DurableTask": "Information"
+ }
+ },
+ "extensions": {
+ "durableTask": {
+ "hubName": "default",
+ "storageProvider": {
+ "type": "AzureManaged",
+ "connectionStringName": "DURABLE_TASK_SCHEDULER_CONNECTION_STRING"
+ }
+ }
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableWorkflowOptions.cs b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableWorkflowOptions.cs
new file mode 100644
index 0000000000..c3f7b6c75d
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/DurableWorkflowOptions.cs
@@ -0,0 +1,37 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI.Workflows;
+
+namespace Microsoft.Agents.AI.DurableTask;
+
+///
+/// Provides configuration options for managing durable workflows within an application.
+///
+public sealed class DurableWorkflowOptions
+{
+ private readonly Dictionary _workflows = new(StringComparer.OrdinalIgnoreCase);
+
+ ///
+ /// Adds a workflow to the collection for processing or execution.
+ ///
+ /// The workflow instance to add. Cannot be null.
+ public void AddWorkflow(Workflow workflow)
+ {
+ ArgumentNullException.ThrowIfNull(workflow);
+
+ if (string.IsNullOrEmpty(workflow.Name))
+ {
+ throw new ArgumentException("Workflow must have a valid Name property.", nameof(workflow));
+ }
+
+ this._workflows[workflow.Name] = workflow;
+ }
+
+ ///
+ /// Gets the collection of workflows available in the current context, keyed by their unique names.
+ ///
+ /// The returned dictionary is read-only and reflects the current set of registered workflows.
+ /// Changes to the underlying workflow collection are immediately visible through this property. Accessing a
+ /// workflow by name that does not exist will result in a KeyNotFoundException.
+ public IReadOnlyDictionary Workflows => this._workflows;
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.DurableTask/Microsoft.Agents.AI.DurableTask.csproj b/dotnet/src/Microsoft.Agents.AI.DurableTask/Microsoft.Agents.AI.DurableTask.csproj
index 43ebe9c61f..782004ec6e 100644
--- a/dotnet/src/Microsoft.Agents.AI.DurableTask/Microsoft.Agents.AI.DurableTask.csproj
+++ b/dotnet/src/Microsoft.Agents.AI.DurableTask/Microsoft.Agents.AI.DurableTask.csproj
@@ -24,6 +24,7 @@
+
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctionExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctionExecutor.cs
index fa0b9ef287..dc1221d4b9 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctionExecutor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctionExecutor.cs
@@ -5,6 +5,7 @@ using Microsoft.Azure.Functions.Worker.Context.Features;
using Microsoft.Azure.Functions.Worker.Extensions.Mcp;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Azure.Functions.Worker.Invocation;
+using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
@@ -25,6 +26,20 @@ internal sealed class BuiltInFunctionExecutor : IFunctionExecutor
IFunctionInputBindingFeature? functionInputBindingFeature = context.Features.Get() ??
throw new InvalidOperationException("Function input binding feature is not available on the current context.");
+ if (context.FunctionDefinition.EntryPoint == BuiltInFunctions.RunWorkflowOrechstrtationFunctionEntryPoint)
+ {
+ var triggerBinding = context.FunctionDefinition.InputBindings.Values.FirstOrDefault(b => b.Type == "orchestrationTrigger");
+ var taskOrechstrationContextBinding = context.BindInputAsync(triggerBinding!);
+
+ if (taskOrechstrationContextBinding.IsCompletedSuccessfully)
+ {
+ var t = taskOrechstrationContextBinding.Result.Value;
+ context.GetInvocationResult().Value = await BuiltInFunctions.RunWorkflowOrchestratorAsync(t!);
+ }
+
+ return;
+ }
+
FunctionInputBindingResult? inputBindingResults = await functionInputBindingFeature.BindFunctionInputAsync(context);
if (inputBindingResults is not { Values: { } values })
{
@@ -35,6 +50,7 @@ internal sealed class BuiltInFunctionExecutor : IFunctionExecutor
string? encodedEntityRequest = null;
DurableTaskClient? durableTaskClient = null;
ToolInvocationContext? mcpToolInvocationContext = null;
+ //string? encodedTaskOrchestrationContext = null;
foreach (var binding in values)
{
@@ -52,10 +68,13 @@ internal sealed class BuiltInFunctionExecutor : IFunctionExecutor
case ToolInvocationContext toolContext:
mcpToolInvocationContext = toolContext;
break;
+ //case string orchestrationContext:
+ // encodedTaskOrchestrationContext = orchestrationContext;
+ // break;
}
}
- if (durableTaskClient is null)
+ if (durableTaskClient is null && context.FunctionDefinition.EntryPoint != BuiltInFunctions.RunWorkflowOrechstrtationFunctionEntryPoint)
{
// This is not expected to happen since all built-in functions are
// expected to have a Durable Task client binding.
@@ -71,7 +90,7 @@ internal sealed class BuiltInFunctionExecutor : IFunctionExecutor
context.GetInvocationResult().Value = await BuiltInFunctions.RunAgentHttpAsync(
httpRequestData,
- durableTaskClient,
+ durableTaskClient!,
context);
return;
}
@@ -84,7 +103,7 @@ internal sealed class BuiltInFunctionExecutor : IFunctionExecutor
}
context.GetInvocationResult().Value = await BuiltInFunctions.InvokeAgentAsync(
- durableTaskClient,
+ durableTaskClient!,
encodedEntityRequest,
context);
return;
@@ -98,7 +117,40 @@ internal sealed class BuiltInFunctionExecutor : IFunctionExecutor
}
context.GetInvocationResult().Value =
- await BuiltInFunctions.RunMcpToolAsync(mcpToolInvocationContext, durableTaskClient, context);
+ await BuiltInFunctions.RunMcpToolAsync(mcpToolInvocationContext, durableTaskClient!, context);
+ return;
+ }
+
+ if (context.FunctionDefinition.EntryPoint == BuiltInFunctions.RunWorkflowOrechstrtationHttpFunctionEntryPoint)
+ {
+ //if (httpRequestData == null)
+ //{
+ // throw new InvalidOperationException($"HTTP request data binding is missing for the invocation {context.InvocationId}.");
+ //}
+
+ if (httpRequestData == null)
+ {
+ throw new InvalidOperationException($"HTTP request data binding is missing for the invocation {context.InvocationId}.");
+ }
+
+ context.GetInvocationResult().Value = await BuiltInFunctions.RunWorkflowOrechstrtationHttpTriggerAsync(
+ httpRequestData,
+ durableTaskClient!,
+ context);
+ return;
+ }
+ if (context.FunctionDefinition.EntryPoint == BuiltInFunctions.RunWorkflowOrechstrtationFunctionEntryPoint)
+ {
+ var triggerBinding = context.FunctionDefinition.InputBindings.Values.FirstOrDefault(b => b.Type == "orchestrationTrigger");
+ var taskOrechstrationContextBinding = context.BindInputAsync(triggerBinding!);
+
+ if (taskOrechstrationContextBinding.IsCompletedSuccessfully)
+ {
+ var t = taskOrechstrationContextBinding.Result.Value;
+ context.GetInvocationResult().Value = await BuiltInFunctions.RunWorkflowOrchestratorAsync(t!);
+ }
+
+ //context.GetInvocationResult().Value = await BuiltInFunctions.RunWorkflowOrchestratorAsync(null);
return;
}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs
index edde523271..0463d4ecc9 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs
@@ -6,6 +6,7 @@ using Microsoft.Agents.AI.DurableTask;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Extensions.Mcp;
using Microsoft.Azure.Functions.Worker.Http;
+using Microsoft.DurableTask;
using Microsoft.DurableTask.Client;
using Microsoft.DurableTask.Worker.Grpc;
using Microsoft.Extensions.AI;
@@ -20,8 +21,47 @@ internal static class BuiltInFunctions
internal static readonly string RunAgentHttpFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(RunAgentHttpAsync)}";
internal static readonly string RunAgentEntityFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(InvokeAgentAsync)}";
+ internal static readonly string RunWorkflowOrechstrtationHttpFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(RunWorkflowOrechstrtationHttpTriggerAsync)}";
+ internal static readonly string RunWorkflowOrechstrtationFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(RunWorkflowOrchestratorAsync)}";
+ internal static readonly string InvokeWorkflowActivityFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(InvokeWorkflowActivityAsync)}";
internal static readonly string RunAgentMcpToolFunctionEntryPoint = $"{typeof(BuiltInFunctions).FullName!}.{nameof(RunMcpToolAsync)}";
+#pragma warning disable IL3000 // Avoid accessing Assembly file path when publishing as a single file - Azure Functions does not use single-file publishing
+ internal static readonly string ScriptFile = Path.GetFileName(typeof(BuiltInFunctions).Assembly.Location);
+#pragma warning restore IL3000
+
+ // Exposed as an activity trigger for workflow executors
+ public static Task InvokeWorkflowActivityAsync(
+ [ActivityTrigger] string input,
+ FunctionContext functionContext)
+ {
+ return Task.FromResult($"Hello from activity with input: {input}");
+ }
+
+ //[Function("my-Orchestration")]
+ //public static async Task> RunOrchestrator1Async(
+ //[OrchestrationTrigger] TaskOrchestrationContext context)
+ //{
+ // ILogger logger = context.CreateReplaySafeLogger(nameof(Function));
+ // logger.LogInformation("Saying hello.");
+
+ // // returns ["Hello Tokyo!", "Hello Seattle!", "Hello London!"]
+ // return new List();
+ //}
+
+ //[Function("dafx-Orchestration")]
+ public static async Task> RunWorkflowOrchestratorAsync(TaskOrchestrationContext taskOrchestrationContext)
+ {
+ //ILogger logger = context.CreateReplaySafeLogger(nameof(Function));
+ //logger.LogInformation("Invoking RunWorkflowOrchestrator");
+ var outputs = new List();
+
+ await Task.Delay(1);
+ outputs.Add("to do - call get executor result");
+
+ return outputs;
+ }
+
// Exposed as an entity trigger via AgentFunctionsProvider
public static Task InvokeAgentAsync(
[DurableClient] DurableTaskClient client,
@@ -43,6 +83,25 @@ internal static class BuiltInFunctions
return GrpcEntityRunner.LoadAndRunAsync(encodedEntityRequest, entity, combinedServiceProvider);
}
+ ///
+ /// Invokes a workflow orchestration in response to an HTTP request.
+ ///
+ public static async Task RunWorkflowOrechstrtationHttpTriggerAsync(
+ [HttpTrigger] HttpRequestData req,
+ [DurableClient] DurableTaskClient client,
+ FunctionContext context)
+ {
+ // to do: Retrieve the workflow and execute it.
+ var workflowName = context.FunctionDefinition.Name.Replace("http", "dafx");
+
+ //string instanceId = await client.ScheduleNewOrchestrationInstanceAsync("dafx-MyTestWorkflow");
+ string instanceId = await client.ScheduleNewOrchestrationInstanceAsync("OrchFunction"); // dafx-MyTestWorkflow");
+
+ HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);
+ await response.WriteStringAsync($"InvokeWorkflowOrechstrtationAsync is invoked for {workflowName}.{instanceId}");
+ return response;
+ }
+
public static async Task RunAgentHttpAsync(
[HttpTrigger] HttpRequestData req,
[DurableClient] DurableTaskClient client,
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowFunctionMetadataTransformer.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowFunctionMetadataTransformer.cs
new file mode 100644
index 0000000000..1e0690a7a2
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/DurableWorkflowFunctionMetadataTransformer.cs
@@ -0,0 +1,132 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.AI.DurableTask;
+using Microsoft.Azure.Functions.Worker.Core.FunctionMetadata;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
+
+internal sealed class DurableWorkflowFunctionMetadataTransformer : IFunctionMetadataTransformer
+{
+ private readonly ILogger _logger;
+ private readonly DurableWorkflowOptions _options;
+
+ public DurableWorkflowFunctionMetadataTransformer(ILogger logger, DurableWorkflowOptions durableWorkflowOptions)
+ {
+ this._logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ this._options = durableWorkflowOptions ?? throw new ArgumentNullException(nameof(durableWorkflowOptions));
+ }
+
+ public string Name => nameof(DurableWorkflowFunctionMetadataTransformer);
+
+ public void Transform(IList original)
+ {
+ if (this._logger.IsEnabled(LogLevel.Information))
+ {
+ this._logger.LogInformation("Transforming function metadata to add durable workflow functions. Initial function count: {FunctionCount}", original.Count);
+ }
+
+ foreach (var workflow in this._options.Workflows)
+ {
+ if (this._logger.IsEnabled(LogLevel.Information))
+ {
+ this._logger.LogInformation("Adding durable workflow function for workflow: {WorkflowName}", workflow.Key);
+ }
+
+ original.Add(CreateOrchestrationTrigger(workflow.Key));
+ // We also want to create an HTTP trigge for this orchestration so users can start it via HTTP.
+ if (this._logger.IsEnabled(LogLevel.Information))
+ {
+ this._logger.LogInformation("Adding HTTP trigger function for workflow: {WorkflowName}", workflow.Key);
+ var httpTriggerMetadata = CreateHttpTrigger(workflow.Key, $"workflows/{workflow.Key}/run");
+ original.Add(httpTriggerMetadata);
+ }
+
+ // Create activity functions for each executor in the workflow
+ // Extract executor IDs from edges and start executor (since ExecutorBindings is internal)
+ var executorIds = new HashSet { workflow.Value.StartExecutorId };
+
+ var reflectedEdges = workflow.Value.ReflectEdges();
+ foreach (var (sourceId, edgeSet) in reflectedEdges)
+ {
+ executorIds.Add(sourceId);
+ foreach (var edge in edgeSet)
+ {
+ foreach (var sinkId in edge.Connection.SinkIds)
+ {
+ executorIds.Add(sinkId);
+ }
+ }
+ }
+
+ foreach (var executorId in executorIds)
+ {
+ if (this._logger.IsEnabled(LogLevel.Information))
+ {
+ this._logger.LogInformation(
+ "Adding activity function for executor: {ExecutorId} in workflow: {WorkflowName}",
+ executorId,
+ workflow.Key);
+ }
+
+ original.Add(CreateActivityTrigger(workflow.Key, executorId));
+ }
+ }
+
+ if (this._logger.IsEnabled(LogLevel.Information))
+ {
+ this._logger.LogInformation("Transform finished. Updated function count: {FunctionCount}", original.Count);
+ }
+ }
+
+ private static DefaultFunctionMetadata CreateHttpTrigger(string name, string route)
+ {
+ return new DefaultFunctionMetadata()
+ {
+ Name = $"{BuiltInFunctions.HttpPrefix}{name}",
+ Language = "dotnet-isolated",
+ RawBindings =
+ [
+ $"{{\"name\":\"req\",\"type\":\"httpTrigger\",\"direction\":\"In\",\"authLevel\":\"function\",\"methods\": [\"post\"],\"route\":\"{route}\"}}",
+ "{\"name\":\"$return\",\"type\":\"http\",\"direction\":\"Out\"}",
+ "{\"name\":\"client\",\"type\":\"durableClient\",\"direction\":\"In\"}"
+ ],
+ EntryPoint = BuiltInFunctions.RunWorkflowOrechstrtationHttpFunctionEntryPoint,
+ ScriptFile = BuiltInFunctions.ScriptFile
+ };
+ }
+
+ private static DefaultFunctionMetadata CreateOrchestrationTrigger(string name)
+ {
+ return new DefaultFunctionMetadata()
+ {
+ Name = AgentSessionId.ToEntityName(name),
+ Language = "dotnet-isolated",
+ RawBindings =
+ [
+ // """{"name":"context","type":"orchestrationTrigger","direction":"In"}""",
+ """{"name":"taskOrchestrationContext","type":"orchestrationTrigger","direction":"In"}""",
+
+ ],
+ EntryPoint = BuiltInFunctions.RunWorkflowOrechstrtationFunctionEntryPoint,
+ ScriptFile = BuiltInFunctions.ScriptFile,
+ };
+ }
+
+ private static DefaultFunctionMetadata CreateActivityTrigger(string workflowName, string executorId)
+ {
+ string functionName = $"{AgentSessionId.ToEntityName(workflowName)}_{executorId}";
+
+ return new DefaultFunctionMetadata()
+ {
+ Name = functionName,
+ Language = "dotnet-isolated",
+ RawBindings =
+ [
+ """{"name":"input","type":"activityTrigger","direction":"In","dataType":"String"}""",
+ ],
+ EntryPoint = BuiltInFunctions.InvokeWorkflowActivityFunctionEntryPoint,
+ ScriptFile = BuiltInFunctions.ScriptFile,
+ };
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsApplicationBuilderExtensions.cs b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsApplicationBuilderExtensions.cs
index e13c6008ea..c9f4a3bc4a 100644
--- a/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsApplicationBuilderExtensions.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/FunctionsApplicationBuilderExtensions.cs
@@ -14,6 +14,22 @@ namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
///
public static class FunctionsApplicationBuilderExtensions
{
+ ///
+ /// Adds support for durable workflows to the specified Functions application builder.
+ ///
+ /// The Functions application builder to configure with durable workflow capabilities.
+ ///
+ /// The same instance of to allow for method chaining.
+ public static FunctionsApplicationBuilder AddDurableWorkflows(this FunctionsApplicationBuilder builder, Action configure)
+ {
+ var options = new DurableWorkflowOptions();
+ configure(options);
+ builder.Services.AddSingleton(options);
+ builder.Services.AddSingleton();
+
+ return builder;
+ }
+
///
/// Configures the application to use durable agents with a builder pattern.
///
@@ -38,6 +54,8 @@ public static class FunctionsApplicationBuilderExtensions
builder.UseWhen(static context =>
string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentHttpFunctionEntryPoint, StringComparison.Ordinal) ||
string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentMcpToolFunctionEntryPoint, StringComparison.Ordinal) ||
+ string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunWorkflowOrechstrtationFunctionEntryPoint, StringComparison.Ordinal) ||
+ string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunWorkflowOrechstrtationHttpFunctionEntryPoint, StringComparison.Ordinal) ||
string.Equals(context.FunctionDefinition.EntryPoint, BuiltInFunctions.RunAgentEntityFunctionEntryPoint, StringComparison.Ordinal));
builder.Services.AddSingleton();