diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index 5c9f2e06d9..0de4409d53 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -111,10 +111,10 @@
-
-
-
-
+
+
+
+
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index dfb8717c85..92968ac153 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -215,6 +215,7 @@
+
diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/InvokeFunctionTool/InvokeFunctionTool.csproj b/dotnet/samples/GettingStarted/Workflows/Declarative/InvokeFunctionTool/InvokeFunctionTool.csproj
new file mode 100644
index 0000000000..23e1c91e0a
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Workflows/Declarative/InvokeFunctionTool/InvokeFunctionTool.csproj
@@ -0,0 +1,38 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+
+
+
+ true
+ true
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/InvokeFunctionTool/InvokeFunctionTool.yaml b/dotnet/samples/GettingStarted/Workflows/Declarative/InvokeFunctionTool/InvokeFunctionTool.yaml
new file mode 100644
index 0000000000..8bc0ffe8be
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Workflows/Declarative/InvokeFunctionTool/InvokeFunctionTool.yaml
@@ -0,0 +1,55 @@
+#
+# This workflow demonstrates using InvokeFunctionTool to call functions directly
+# from the workflow without going through an AI agent first.
+#
+# InvokeFunctionTool allows workflows to:
+# - Pre-fetch data before calling an AI agent
+# - Execute operations directly without AI involvement
+# - Store function results in workflow variables for later use
+#
+# Example input:
+# What are the specials in the menu?
+#
+kind: Workflow
+trigger:
+
+ kind: OnConversationStart
+ id: workflow_invoke_function_tool_demo
+ actions:
+
+ # Invoke GetSpecials function to get today's specials directly from the workflow
+ - kind: InvokeFunctionTool
+ id: invoke_get_specials
+ conversationId: =System.ConversationId
+ requireApproval: true
+ functionName: GetSpecials
+ output:
+ autoSend: true
+ result: Local.Specials
+ messages: Local.FunctionMessage
+
+ # Display a message showing we retrieved the specials
+ - kind: SendMessage
+ id: show_specials_intro
+ message: "Today's specials have been retrieved. Here they are: {Local.Specials}"
+
+ # Now use an agent to format and present the specials to the user
+ - kind: InvokeAzureAgent
+ id: invoke_menu_agent
+ conversationId: =System.ConversationId
+ agent:
+ name: FunctionMenuAgent
+ input:
+ messages: =UserMessage("Please describe today's specials in an appealing way.")
+ output:
+ messages: Local.AgentResponse
+
+ # Allow the user to ask follow-up questions in a loop
+ - kind: InvokeAzureAgent
+ id: invoke_followup
+ conversationId: =System.ConversationId
+ agent:
+ name: FunctionMenuAgent
+ input:
+ externalLoop:
+ when: =Upper(System.LastMessage.Text) <> "EXIT"
diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/InvokeFunctionTool/MenuPlugin.cs b/dotnet/samples/GettingStarted/Workflows/Declarative/InvokeFunctionTool/MenuPlugin.cs
new file mode 100644
index 0000000000..a2c00f37cc
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Workflows/Declarative/InvokeFunctionTool/MenuPlugin.cs
@@ -0,0 +1,85 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.ComponentModel;
+
+namespace Demo.Workflows.Declarative.InvokeFunctionTool;
+
+#pragma warning disable CA1822 // Mark members as static
+
+///
+/// Plugin providing menu-related functions that can be invoked directly by the workflow
+/// using the InvokeFunctionTool action.
+///
+public sealed class MenuPlugin
+{
+ [Description("Provides a list items on the menu.")]
+ public MenuItem[] GetMenu()
+ {
+ return s_menuItems;
+ }
+
+ [Description("Provides a list of specials from the menu.")]
+ public MenuItem[] GetSpecials()
+ {
+ return [.. s_menuItems.Where(i => i.IsSpecial)];
+ }
+
+ [Description("Provides the price of the requested menu item.")]
+ public float? GetItemPrice(
+ [Description("The name of the menu item.")]
+ string name)
+ {
+ return s_menuItems.FirstOrDefault(i => i.Name.Equals(name, StringComparison.OrdinalIgnoreCase))?.Price;
+ }
+
+ private static readonly MenuItem[] s_menuItems =
+ [
+ new()
+ {
+ Category = "Soup",
+ Name = "Clam Chowder",
+ Price = 4.95f,
+ IsSpecial = true,
+ },
+ new()
+ {
+ Category = "Soup",
+ Name = "Tomato Soup",
+ Price = 4.95f,
+ IsSpecial = false,
+ },
+ new()
+ {
+ Category = "Salad",
+ Name = "Cobb Salad",
+ Price = 9.99f,
+ },
+ new()
+ {
+ Category = "Salad",
+ Name = "House Salad",
+ Price = 4.95f,
+ },
+ new()
+ {
+ Category = "Drink",
+ Name = "Chai Tea",
+ Price = 2.95f,
+ IsSpecial = true,
+ },
+ new()
+ {
+ Category = "Drink",
+ Name = "Soda",
+ Price = 1.95f,
+ },
+ ];
+
+ public sealed class MenuItem
+ {
+ public string Category { get; init; } = string.Empty;
+ public string Name { get; init; } = string.Empty;
+ public float Price { get; init; }
+ public bool IsSpecial { get; init; }
+ }
+}
diff --git a/dotnet/samples/GettingStarted/Workflows/Declarative/InvokeFunctionTool/Program.cs b/dotnet/samples/GettingStarted/Workflows/Declarative/InvokeFunctionTool/Program.cs
new file mode 100644
index 0000000000..456bba0f88
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Workflows/Declarative/InvokeFunctionTool/Program.cs
@@ -0,0 +1,88 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Azure.AI.Projects;
+using Azure.AI.Projects.OpenAI;
+using Azure.Identity;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Configuration;
+using OpenAI.Responses;
+using Shared.Foundry;
+using Shared.Workflows;
+
+namespace Demo.Workflows.Declarative.InvokeFunctionTool;
+
+///
+/// Demonstrate a workflow that uses InvokeFunctionTool to call functions directly
+/// from the workflow without going through an AI agent first.
+///
+///
+/// The InvokeFunctionTool action allows workflows to invoke function tools directly,
+/// enabling pre-fetching of data or executing operations before calling an AI agent.
+/// See the README.md file in the parent folder (../README.md) for detailed
+/// information about the configuration required to run this sample.
+///
+internal sealed class Program
+{
+ public static async Task Main(string[] args)
+ {
+ // Initialize configuration
+ IConfiguration configuration = Application.InitializeConfig();
+ Uri foundryEndpoint = new(configuration.GetValue(Application.Settings.FoundryEndpoint));
+
+ // Create the menu plugin with functions that can be invoked directly by the workflow
+ MenuPlugin menuPlugin = new();
+ AIFunction[] functions =
+ [
+ AIFunctionFactory.Create(menuPlugin.GetMenu),
+ AIFunctionFactory.Create(menuPlugin.GetSpecials),
+ AIFunctionFactory.Create(menuPlugin.GetItemPrice),
+ ];
+
+ // Ensure sample agent exists in Foundry
+ await CreateAgentAsync(foundryEndpoint, configuration);
+
+ // Get input from command line or console
+ string workflowInput = Application.GetInput(args);
+
+ // Create the workflow factory.
+ WorkflowFactory workflowFactory = new("InvokeFunctionTool.yaml", foundryEndpoint);
+
+ // Execute the workflow
+ WorkflowRunner runner = new(functions) { UseJsonCheckpoints = true };
+ await runner.ExecuteAsync(workflowFactory.CreateWorkflow, workflowInput);
+ }
+
+ private static async Task CreateAgentAsync(Uri foundryEndpoint, IConfiguration configuration)
+ {
+ // WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
+ AIProjectClient aiProjectClient = new(foundryEndpoint, new DefaultAzureCredential());
+
+ await aiProjectClient.CreateAgentAsync(
+ agentName: "FunctionMenuAgent",
+ agentDefinition: DefineMenuAgent(configuration, []), // Create Agent with no function tool in the definition.
+ agentDescription: "Provides information about the restaurant menu");
+ }
+
+ private static PromptAgentDefinition DefineMenuAgent(IConfiguration configuration, AIFunction[] functions)
+ {
+ PromptAgentDefinition agentDefinition =
+ new(configuration.GetValue(Application.Settings.FoundryModelMini))
+ {
+ Instructions =
+ """
+ Answer the users questions about the menu.
+ Use the information provided in the conversation history to answer questions.
+ If the information is already available in the conversation, use it directly.
+ For questions or input that do not require searching the documentation, inform the
+ user that you can only answer questions about what's on the menu.
+ """
+ };
+
+ foreach (AIFunction function in functions)
+ {
+ agentDefinition.Tools.Add(function.AsOpenAIResponseTool());
+ }
+
+ return agentDefinition;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs
index 3ecb91ea3a..7b84e24839 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowActionVisitor.cs
@@ -390,6 +390,27 @@ protected override void Visit(InvokeAzureAgent item)
this._workflowModel.AddNode(new DelegateActionExecutor(postId, this._workflowState, action.CompleteAsync), action.ParentId);
}
+ protected override void Visit(InvokeFunctionTool item)
+ {
+ this.Trace(item);
+
+ // Entry point to invoke function tool - always yields for external execution
+ InvokeFunctionToolExecutor action = new(item, this._workflowOptions.AgentProvider, this._workflowState);
+ this.ContinueWith(action);
+
+ // Define request-port for function tool invocation (always requires external input)
+ string externalInputPortId = InvokeFunctionToolExecutor.Steps.ExternalInput(action.Id);
+ RequestPortAction externalInputPort = new(RequestPort.Create(externalInputPortId));
+ this._workflowModel.AddNode(externalInputPort, action.ParentId);
+ this._workflowModel.AddLinkFromPeer(action.ParentId, externalInputPortId);
+
+ // Capture response when external input is received
+ string resumeId = InvokeFunctionToolExecutor.Steps.Resume(action.Id);
+ this.ContinueWith(
+ new DelegateActionExecutor(resumeId, this._workflowState, action.CaptureResponseAsync),
+ action.ParentId);
+ }
+
protected override void Visit(InvokeAzureResponse item)
{
this.NotSupported(item);
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowTemplateVisitor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowTemplateVisitor.cs
index 56901762f4..568a38950c 100644
--- a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowTemplateVisitor.cs
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/Interpreter/WorkflowTemplateVisitor.cs
@@ -365,6 +365,8 @@ protected override void Visit(SendActivity item)
#region Not supported
+ protected override void Visit(InvokeFunctionTool item) => this.NotSupported(item);
+
protected override void Visit(AnswerQuestionWithAI item) => this.NotSupported(item);
protected override void Visit(DeleteActivity item) => this.NotSupported(item);
diff --git a/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeFunctionToolExecutor.cs b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeFunctionToolExecutor.cs
new file mode 100644
index 0000000000..615d4d88ab
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.Workflows.Declarative/ObjectModel/InvokeFunctionToolExecutor.cs
@@ -0,0 +1,312 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Agents.AI.Workflows.Declarative.Events;
+using Microsoft.Agents.AI.Workflows.Declarative.Extensions;
+using Microsoft.Agents.AI.Workflows.Declarative.Interpreter;
+using Microsoft.Agents.AI.Workflows.Declarative.Kit;
+using Microsoft.Agents.AI.Workflows.Declarative.PowerFx;
+using Microsoft.Agents.ObjectModel;
+using Microsoft.Extensions.AI;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI.Workflows.Declarative.ObjectModel;
+
+///
+/// Executor for the action.
+/// This executor yields to the caller for function execution and resumes when results are provided.
+///
+internal sealed class InvokeFunctionToolExecutor(
+ InvokeFunctionTool model,
+ ResponseAgentProvider agentProvider,
+ WorkflowFormulaState state) :
+ DeclarativeActionExecutor(model, state)
+{
+ ///
+ /// Step identifiers for the function tool invocation workflow.
+ ///
+ public static class Steps
+ {
+ ///
+ /// Step for waiting for external input (function result).
+ ///
+ public static string ExternalInput(string id) => $"{id}_{nameof(ExternalInput)}";
+
+ ///
+ /// Step for resuming after receiving function result.
+ ///
+ public static string Resume(string id) => $"{id}_{nameof(Resume)}";
+ }
+
+ ///
+ protected override bool EmitResultEvent => false;
+
+ ///
+ protected override bool IsDiscreteAction => false;
+
+ ///
+ protected override async ValueTask