// Copyright (c) Microsoft. All rights reserved.
using Microsoft.Agents.AI.DurableTask;
using Microsoft.Agents.AI.DurableTask.Workflows;
using Microsoft.Agents.AI.Workflows;
using Microsoft.Azure.Functions.Worker.Core.FunctionMetadata;
using Microsoft.Extensions.Logging;
namespace Microsoft.Agents.AI.Hosting.AzureFunctions;
///
/// Transforms function metadata by dynamically registering Azure Functions triggers
/// for each configured durable workflow and its executors.
///
///
/// For each workflow, this transformer registers:
///
/// - An HTTP trigger function to start the workflow orchestration via HTTP.
/// - An orchestration trigger function to run the workflow orchestration.
/// - An activity trigger function for each non-agent executor in the workflow.
/// - An entity trigger function for each AI agent executor in the workflow.
///
/// When multiple workflows share the same executor, the corresponding function is registered only once.
///
internal sealed class DurableWorkflowsFunctionMetadataTransformer : IFunctionMetadataTransformer
{
private readonly ILogger _logger;
private readonly FunctionsDurableOptions _options;
///
/// Initializes a new instance of the class.
///
/// The logger instance for diagnostic output.
/// The durable options containing workflow configurations.
public DurableWorkflowsFunctionMetadataTransformer(
ILogger logger,
FunctionsDurableOptions durableOptions)
{
this._logger = logger ?? throw new ArgumentNullException(nameof(logger));
ArgumentNullException.ThrowIfNull(durableOptions);
this._options = durableOptions;
}
///
public string Name => nameof(DurableWorkflowsFunctionMetadataTransformer);
///
public void Transform(IList original)
{
int initialCount = original.Count;
this._logger.LogTransformingFunctionMetadata(initialCount);
// Track registered function names to avoid duplicates when workflows share executors.
HashSet registeredFunctions = [];
DurableWorkflowOptions workflowOptions = this._options.Workflows;
foreach (var workflow in workflowOptions.Workflows)
{
string httpFunctionName = $"{BuiltInFunctions.HttpPrefix}{workflow.Key}";
if (this._logger.IsEnabled(LogLevel.Information))
{
this._logger.LogInformation("Registering durable workflow functions for workflow '{WorkflowKey}' with HTTP trigger function name '{HttpFunctionName}'", workflow.Key, httpFunctionName);
}
// Register an orchestration function for the workflow.
string orchestrationFunctionName = WorkflowNamingHelper.ToOrchestrationFunctionName(workflow.Key);
if (registeredFunctions.Add(orchestrationFunctionName))
{
this._logger.LogRegisteringWorkflowTrigger(workflow.Key, orchestrationFunctionName, "orchestration");
original.Add(FunctionMetadataFactory.CreateOrchestrationTrigger(
orchestrationFunctionName,
BuiltInFunctions.RunWorkflowOrchestrationFunctionEntryPoint));
}
// Register an HTTP trigger so users can start this workflow via HTTP.
if (registeredFunctions.Add(httpFunctionName))
{
this._logger.LogRegisteringWorkflowTrigger(workflow.Key, httpFunctionName, "http");
original.Add(FunctionMetadataFactory.CreateHttpTrigger(
workflow.Key,
$"workflows/{workflow.Key}/run",
BuiltInFunctions.RunWorkflowOrchestrationHttpFunctionEntryPoint));
}
// Register a status endpoint if opted in via AddWorkflow(exposeStatusEndpoint: true).
if (this._options.IsStatusEndpointEnabled(workflow.Key))
{
string statusFunctionName = $"{BuiltInFunctions.HttpPrefix}{workflow.Key}-status";
if (registeredFunctions.Add(statusFunctionName))
{
this._logger.LogRegisteringWorkflowTrigger(workflow.Key, statusFunctionName, "http-status");
original.Add(FunctionMetadataFactory.CreateHttpTrigger(
$"{workflow.Key}-status",
$"workflows/{workflow.Key}/status/{{runId}}",
BuiltInFunctions.GetWorkflowStatusHttpFunctionEntryPoint,
methods: "\"get\""));
}
}
// Register a respond endpoint when the workflow contains RequestPort nodes.
bool hasRequestPorts = workflow.Value.ReflectExecutors().Values.Any(b => b is RequestPortBinding);
if (hasRequestPorts)
{
string respondFunctionName = $"{BuiltInFunctions.HttpPrefix}{workflow.Key}-respond";
if (registeredFunctions.Add(respondFunctionName))
{
this._logger.LogRegisteringWorkflowTrigger(workflow.Key, respondFunctionName, "http-respond");
original.Add(FunctionMetadataFactory.CreateHttpTrigger(
$"{workflow.Key}-respond",
$"workflows/{workflow.Key}/respond/{{runId}}",
BuiltInFunctions.RespondToWorkflowHttpFunctionEntryPoint));
}
}
// Register an MCP tool trigger if opted in via AddWorkflow(exposeMcpToolTrigger: true).
if (this._options.IsMcpToolTriggerEnabled(workflow.Key))
{
string mcpToolFunctionName = $"{BuiltInFunctions.McpToolPrefix}{workflow.Key}";
if (registeredFunctions.Add(mcpToolFunctionName))
{
this._logger.LogRegisteringWorkflowTrigger(workflow.Key, mcpToolFunctionName, "mcpTool");
original.Add(FunctionMetadataFactory.CreateWorkflowMcpToolTrigger(workflow.Key, workflow.Value.Description));
}
}
// Register activity or entity functions for each executor in the workflow.
// ReflectExecutors() returns all executors across the graph; no need to manually traverse edges.
foreach (KeyValuePair entry in workflow.Value.ReflectExecutors())
{
// Sub-workflow and RequestPort bindings use specialized dispatch, not activities.
if (entry.Value is SubworkflowBinding or RequestPortBinding)
{
continue;
}
string executorName = WorkflowNamingHelper.GetExecutorName(entry.Key);
// AI agent executors are backed by durable entities; other executors use activity triggers.
if (entry.Value is AIAgentBinding)
{
string entityName = AgentSessionId.ToEntityName(executorName);
if (registeredFunctions.Add(entityName))
{
this._logger.LogRegisteringWorkflowTrigger(workflow.Key, entityName, "entity");
original.Add(FunctionMetadataFactory.CreateEntityTrigger(executorName));
}
}
else
{
string functionName = WorkflowNamingHelper.ToOrchestrationFunctionName(executorName);
if (registeredFunctions.Add(functionName))
{
this._logger.LogRegisteringWorkflowTrigger(workflow.Key, functionName, "activity");
original.Add(FunctionMetadataFactory.CreateActivityTrigger(functionName));
}
}
}
}
this._logger.LogTransformationComplete(original.Count - initialCount, original.Count);
}
}