diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index df6a36cc41..7b3464dc5e 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -80,6 +80,11 @@
+
+
+
+
+
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 71d8e77312..390bdd52bb 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -44,6 +44,17 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -80,6 +91,8 @@
+
+
@@ -175,6 +188,7 @@
+
@@ -204,6 +218,7 @@
+
diff --git a/dotnet/nuget.config b/dotnet/nuget.config
index 996a924ac0..538a2f45f1 100644
--- a/dotnet/nuget.config
+++ b/dotnet/nuget.config
@@ -2,6 +2,16 @@
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/DeclarativeWorkflow/DeclarativeWorkflow.csproj b/dotnet/samples/DeclarativeWorkflow/DeclarativeWorkflow.csproj
new file mode 100644
index 0000000000..ee1a58754c
--- /dev/null
+++ b/dotnet/samples/DeclarativeWorkflow/DeclarativeWorkflow.csproj
@@ -0,0 +1,33 @@
+
+
+
+ Exe
+ net9.0
+ net9.0
+ $(ProjectsDebugTargetFrameworks)
+ enable
+ disable
+ 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0
+ $(NoWarn);CA1812
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/DeclarativeWorkflow/Program.cs b/dotnet/samples/DeclarativeWorkflow/Program.cs
new file mode 100644
index 0000000000..c480e96c23
--- /dev/null
+++ b/dotnet/samples/DeclarativeWorkflow/Program.cs
@@ -0,0 +1,304 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Threading.Tasks;
+using Azure.AI.Agents.Persistent;
+using Azure.Identity;
+using Microsoft.Agents.Workflows;
+using Microsoft.Agents.Workflows.Declarative;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.Configuration;
+
+namespace Demo.DeclarativeWorkflow;
+
+///
+/// HOW TO: Create a workflow from a declartive (yaml based) definition.
+///
+///
+/// Configuration
+/// Define FOUNDRY_PROJECT_ENDPOINT as a user-secret or environment variable that
+/// points to your Foundry project endpoint.
+/// Usage
+/// Provide the path to the workflow definition file as the first argument.
+/// All other arguments are intepreted as a queue of inputs.
+/// When no input is queued, interactive input is requested from the console.
+///
+internal sealed class Program
+{
+ public static async Task Main(string[] args)
+ {
+ Program program = new(args);
+ await program.ExecuteAsync();
+ }
+
+ private async Task ExecuteAsync()
+ {
+ // Read and parse the declarative workflow.
+ Notify($"WORKFLOW: Parsing {Path.GetFullPath(this.WorkflowFile)}");
+
+ Stopwatch timer = Stopwatch.StartNew();
+
+ // Use DeclarativeWorkflowBuilder to build a workflow based on a YAML file.
+ DeclarativeWorkflowOptions options =
+ new(new FoundryAgentProvider(this.FoundryEndpoint, new AzureCliCredential()))
+ {
+ Configuration = this.Configuration
+ };
+ Workflow workflow = DeclarativeWorkflowBuilder.Build(this.WorkflowFile, options);
+
+ Notify($"\nWORKFLOW: Defined {timer.Elapsed}");
+
+ Notify("\nWORKFLOW: Starting...");
+
+ // Run the workflow, just like any other workflow
+ string input = this.GetWorkflowInput();
+ StreamingRun run = await InProcessExecution.StreamAsync(workflow, input);
+ await this.MonitorWorkflowRunAsync(run);
+
+ Notify("\nWORKFLOW: Done!");
+ }
+
+ private const string DefaultWorkflow = "HelloWorld.yaml";
+ private const string ConfigKeyFoundryEndpoint = "FOUNDRY_PROJECT_ENDPOINT";
+
+ private static readonly Dictionary s_nameCache = [];
+ private static readonly HashSet s_fileCache = [];
+
+ private string WorkflowFile { get; }
+ private string? WorkflowInput { get; }
+ private string FoundryEndpoint { get; }
+ private PersistentAgentsClient FoundryClient { get; }
+ private IConfiguration Configuration { get; }
+
+ private Program(string[] args)
+ {
+ this.WorkflowFile = ParseWorkflowFile(args);
+ this.WorkflowInput = ParseWorkflowInput(args);
+
+ this.Configuration = InitializeConfig();
+
+ this.FoundryEndpoint = this.Configuration[ConfigKeyFoundryEndpoint] ?? throw new InvalidOperationException($"Undefined configuration setting: {ConfigKeyFoundryEndpoint}");
+ this.FoundryClient = new PersistentAgentsClient(this.FoundryEndpoint, new AzureCliCredential());
+ }
+
+ private async Task MonitorWorkflowRunAsync(StreamingRun run)
+ {
+ string? messageId = null;
+
+ await foreach (WorkflowEvent evt in run.WatchStreamAsync().ConfigureAwait(false))
+ {
+ if (evt is ExecutorInvokeEvent executorInvoked)
+ {
+ Debug.WriteLine($"STEP ENTER #{executorInvoked.ExecutorId}");
+ }
+ else if (evt is ExecutorCompleteEvent executorComplete)
+ {
+ Debug.WriteLine($"STEP EXIT #{executorComplete.ExecutorId}");
+ }
+ else if (evt is ExecutorFailureEvent executorFailure)
+ {
+ Debug.WriteLine($"STEP ERROR #{executorFailure.ExecutorId}: {executorFailure.Data?.Message ?? "Unknown"}");
+ }
+ else if (evt is ConversationUpdateEvent invokeEvent)
+ {
+ Debug.WriteLine($"CONVERSATION: {invokeEvent.Data}");
+ }
+ else if (evt is AgentRunUpdateEvent streamEvent)
+ {
+ if (!string.Equals(messageId, streamEvent.Update.MessageId, StringComparison.Ordinal))
+ {
+ messageId = streamEvent.Update.MessageId;
+
+ if (messageId is not null)
+ {
+ string? agentId = streamEvent.Update.AuthorName;
+ if (agentId is not null)
+ {
+ if (!s_nameCache.TryGetValue(agentId, out string? realName))
+ {
+ PersistentAgent agent = await this.FoundryClient.Administration.GetAgentAsync(agentId);
+ s_nameCache[agentId] = agent.Name;
+ realName = agent.Name;
+ }
+ agentId = realName;
+ }
+ agentId ??= nameof(ChatRole.Assistant);
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.Write($"\n{agentId.ToUpperInvariant()}:");
+ Console.ForegroundColor = ConsoleColor.DarkGray;
+ Console.WriteLine($" [{messageId}]");
+ }
+ }
+
+ ChatResponseUpdate? chatUpdate = streamEvent.Update.RawRepresentation as ChatResponseUpdate;
+ switch (chatUpdate?.RawRepresentation)
+ {
+ case MessageContentUpdate messageUpdate:
+ string? fileId = messageUpdate.ImageFileId ?? messageUpdate.TextAnnotation?.OutputFileId;
+ if (fileId is not null && s_fileCache.Add(fileId))
+ {
+ BinaryData content = await this.FoundryClient.Files.GetFileContentAsync(fileId);
+ await DownloadFileContentAsync(Path.GetFileName(messageUpdate.TextAnnotation?.TextToReplace ?? "response.png"), content);
+ }
+ break;
+ }
+ try
+ {
+ Console.ResetColor();
+ Console.Write(streamEvent.Data);
+ }
+ finally
+ {
+ Console.ResetColor();
+ }
+ }
+ else if (evt is AgentRunResponseEvent messageEvent)
+ {
+ try
+ {
+ Console.WriteLine();
+ if (messageEvent.Response.AgentId is null)
+ {
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ Console.WriteLine("ACTIVITY:");
+ Console.ForegroundColor = ConsoleColor.Yellow;
+ Console.WriteLine(messageEvent.Response?.Text.Trim());
+ }
+ else
+ {
+ if (messageEvent.Response.Usage is not null)
+ {
+ Console.ForegroundColor = ConsoleColor.DarkGray;
+ Console.WriteLine($"[Tokens Total: {messageEvent.Response.Usage.TotalTokenCount}, Input: {messageEvent.Response.Usage.InputTokenCount}, Output: {messageEvent.Response.Usage.OutputTokenCount}]");
+ }
+ }
+ }
+ finally
+ {
+ Console.ResetColor();
+ }
+ }
+ }
+ }
+
+ private static string ParseWorkflowFile(string[] args)
+ {
+ string workflowFile = args.FirstOrDefault() ?? DefaultWorkflow;
+
+ if (!File.Exists(workflowFile) && !Path.IsPathFullyQualified(workflowFile))
+ {
+ string? repoFolder = GetRepoFolder();
+ if (repoFolder is not null)
+ {
+ workflowFile = Path.Combine(repoFolder, "Workflows", workflowFile);
+ workflowFile = Path.ChangeExtension(workflowFile, ".yaml");
+ }
+ }
+
+ if (!File.Exists(workflowFile))
+ {
+ throw new InvalidOperationException($"Unable to locate workflow: {Path.GetFullPath(workflowFile)}.");
+ }
+
+ return workflowFile;
+
+ static string? GetRepoFolder()
+ {
+ DirectoryInfo? current = new(Directory.GetCurrentDirectory());
+
+ while (current is not null)
+ {
+ if (Directory.Exists(Path.Combine(current.FullName, ".git")))
+ {
+ return current.FullName;
+ }
+
+ current = current.Parent;
+ }
+
+ return null;
+ }
+ }
+
+ private string GetWorkflowInput()
+ {
+ string? input = this.WorkflowInput;
+
+ try
+ {
+ Console.ForegroundColor = ConsoleColor.DarkGreen;
+
+ Console.Write("\nINPUT: ");
+
+ Console.ForegroundColor = ConsoleColor.White;
+
+ if (!string.IsNullOrWhiteSpace(input))
+ {
+ Console.WriteLine(input);
+ return input;
+ }
+ while (string.IsNullOrWhiteSpace(input))
+ {
+ input = Console.ReadLine();
+ }
+
+ return input.Trim();
+ }
+ finally
+ {
+ Console.ResetColor();
+ }
+ }
+
+ private static string? ParseWorkflowInput(string[] args)
+ {
+ if (args.Length == 0)
+ {
+ return null;
+ }
+
+ string[] workflowInput = [.. args.Skip(1)];
+
+ return workflowInput.FirstOrDefault();
+ }
+
+ // Load configuration from user-secrets
+ private static IConfigurationRoot InitializeConfig() =>
+ new ConfigurationBuilder()
+ .AddUserSecrets(Assembly.GetExecutingAssembly())
+ .AddEnvironmentVariables()
+ .Build();
+
+ private static void Notify(string message)
+ {
+ Console.ForegroundColor = ConsoleColor.Cyan;
+ try
+ {
+ Console.WriteLine(message);
+ }
+ finally
+ {
+ Console.ResetColor();
+ }
+ }
+
+ private static async ValueTask DownloadFileContentAsync(string filename, BinaryData content)
+ {
+ string filePath = Path.Combine(Path.GetTempPath(), Path.GetFileName(filename));
+ filePath = Path.ChangeExtension(filePath, ".png");
+
+ await File.WriteAllBytesAsync(filePath, content.ToArray());
+
+ Process.Start(
+ new ProcessStartInfo
+ {
+ FileName = "cmd.exe",
+ Arguments = $"/C start {filePath}"
+ });
+ }
+}
diff --git a/dotnet/samples/DeclarativeWorkflow/README.md b/dotnet/samples/DeclarativeWorkflow/README.md
new file mode 100644
index 0000000000..5229bd50c2
--- /dev/null
+++ b/dotnet/samples/DeclarativeWorkflow/README.md
@@ -0,0 +1,88 @@
+# Summary
+
+This demo showcases the ability to parse a declarative Foundry Workflow file (YAML) to build a `Workflow<>`
+be executed using the same pattern as any code-based workflow.
+
+## Configuration
+
+This demo requires configuration to access agents an [Azure Foundry Project](https://learn.microsoft.com/azure/ai-foundry).
+
+#### Settings
+
+We suggest using .NET [Secret Manager](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets)
+to avoid the risk of leaking secrets into the repository, branches and pull requests.
+You can also use environment variables if you prefer.
+
+To set your secrets as an environment variable (PowerShell):
+
+```pwsh
+$env:FOUNDRY_PROJECT_ENDPOINT="https://..."
+```
+
+To set your secrets with .NET Secret Manager:
+
+1. From the root of the respository, navigate the console to the project folder:
+
+ ```
+ cd dotnet/demos/DeclarativeWorkflow
+ ```
+
+2. Examine existing secret definitions:
+
+ ```
+ dotnet user-secrets list
+ ```
+
+3. If needed, perform first time initialization:
+
+ ```
+ dotnet user-secrets init
+ ```
+
+4. Define setting that identifies your Azure Foundry Project (endpoint):
+
+ ```
+ dotnet user-secrets set "FOUNDRY_PROJECT_ENDPOINT" "https://..."
+ ```
+
+#### Authorization
+
+Use [_Azure CLI_](https://learn.microsoft.com/cli/azure/authenticate-azure-cli) to authorize access to your Azure Foundry Project:
+
+ ```
+ az login
+ az account get-access-token
+ ```
+
+#### Agents
+
+The sample workflows rely on agents defined in your Azure Foundry Project.
+
+To create agents, run the [`Create.ps1`](../../../workflows/) script.
+This will create the agents used in the sample workflows in your Azure Foundry Project and format a script you can copy and use to configure your environment.
+
+> Note: `Create.ps1` relies upon the `FOUNDRY_PROJECT_ENDPOINT` setting.
+
+## Execution
+
+Run the demo from the console by specifying a path to a declarative (YAML) workflow file.
+The repository has example workflows available in the root [`/workflows`](../../../workflows) folder.
+
+1. From the root of the respository, navigate the console to the project folder:
+
+ ```sh
+ cd dotnet/demos/DeclarativeWorkflow
+
+ ```
+
+2. Run the demo referencing a sample workflow by name:
+
+ ```sh
+ dotnet run HelloWorld
+ ```
+
+3. Run the demo with a path to any workflow file:
+
+ ```sh
+ dotnet run c:/myworkflows/HelloWorld.yaml
+ ```
diff --git a/dotnet/samples/GettingStarted/AgentOrchestration/AgentOrchestration.csproj b/dotnet/samples/GettingStarted/AgentOrchestration/AgentOrchestration.csproj
index 5caf5fd086..43a254a910 100644
--- a/dotnet/samples/GettingStarted/AgentOrchestration/AgentOrchestration.csproj
+++ b/dotnet/samples/GettingStarted/AgentOrchestration/AgentOrchestration.csproj
@@ -14,7 +14,7 @@
$(ProjectsTargetFrameworks)
$(ProjectsDebugTargetFrameworks)
-
+
@@ -47,14 +47,14 @@
-
+
-
+
@@ -62,4 +62,4 @@
-
+
\ No newline at end of file
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/ConversationUpdateEvent.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ConversationUpdateEvent.cs
new file mode 100644
index 0000000000..7984ffcd11
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/ConversationUpdateEvent.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+namespace Microsoft.Agents.Workflows.Declarative;
+
+///
+/// Event that represents a message produced by a declarative workflow.
+///
+public class ConversationUpdateEvent(string executorid, string conversationId) : ExecutorEvent(executorid, conversationId)
+{
+ ///
+ /// The conversation ID associated with the workflow.
+ ///
+ public string ConversationId { get; } = conversationId;
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowBuilder.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowBuilder.cs
new file mode 100644
index 0000000000..97a7d93328
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowBuilder.cs
@@ -0,0 +1,101 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.IO;
+using System.Linq;
+using Microsoft.Agents.Workflows.Declarative.Extensions;
+using Microsoft.Agents.Workflows.Declarative.Interpreter;
+using Microsoft.Agents.Workflows.Declarative.PowerFx;
+using Microsoft.Bot.ObjectModel;
+using Microsoft.Bot.ObjectModel.Yaml;
+using Microsoft.Extensions.AI;
+
+namespace Microsoft.Agents.Workflows.Declarative;
+
+///
+/// Builder for converting a Foundry workflow object-model YAML definition into a process.
+///
+public static class DeclarativeWorkflowBuilder
+{
+ ///
+ /// Builds a process from the provided YAML definition of a CPS Topic ObjectModel.
+ ///
+ /// The type of the input message
+ /// The path to the workflow.
+ /// The execution context for the workflow.
+ /// An optional function to transform the input message into a .
+ ///
+ public static Workflow Build(
+ string workflowFile,
+ DeclarativeWorkflowOptions options,
+ Func? inputTransform = null)
+ where TInput : notnull
+ {
+ using StreamReader yamlReader = File.OpenText(workflowFile);
+ return Build(yamlReader, options, inputTransform);
+ }
+ ///
+ /// Builds a process from the provided YAML definition of a CPS Topic ObjectModel.
+ ///
+ /// The type of the input message
+ /// The reader that provides the workflow object model YAML.
+ /// The execution context for the workflow.
+ /// An optional function to transform the input message into a .
+ /// The that corresponds with the YAML object model.
+ public static Workflow Build(
+ TextReader yamlReader,
+ DeclarativeWorkflowOptions options,
+ Func? inputTransform = null)
+ where TInput : notnull
+ {
+ BotElement rootElement = YamlSerializer.Deserialize(yamlReader) ?? throw new DeclarativeModelException("Workflow undefined.");
+
+ // ISSUE #486 - Use "Workflow" element for Foundry.
+ if (rootElement is not AdaptiveDialog workflowElement)
+ {
+ throw new DeclarativeModelException($"Unsupported root element: {rootElement.GetType().Name}. Expected an {nameof(AdaptiveDialog)}.");
+ }
+
+ string rootId = WorkflowActionVisitor.RootId(workflowElement.BeginDialog?.Id.Value ?? "workflow");
+
+ WorkflowScopes scopes = new();
+ scopes.Initialize(WrapWithBot(workflowElement), options.Configuration);
+ DeclarativeWorkflowState state = new(options.CreateRecalcEngine(), scopes);
+ DeclarativeWorkflowExecutor rootExecutor =
+ new(rootId,
+ state,
+ message => inputTransform?.Invoke(message) ?? DefaultTransform(message));
+
+ WorkflowActionVisitor visitor = new(rootExecutor, state, options);
+ WorkflowElementWalker walker = new(rootElement, visitor);
+
+ return walker.GetWorkflow();
+ }
+
+ private static ChatMessage DefaultTransform(object message) =>
+ message switch
+ {
+ ChatMessage chatMessage => chatMessage,
+ string stringMessage => new ChatMessage(ChatRole.User, stringMessage),
+ _ => new(ChatRole.User, $"{message}")
+ };
+
+ // Wrap with bot to ensure schema is set.
+ private static AdaptiveDialog WrapWithBot(AdaptiveDialog dialog)
+ {
+ BotDefinition bot
+ = new BotDefinition.Builder
+ {
+ Components =
+ {
+ new DialogComponent.Builder
+ {
+ SchemaName = dialog.HasSchemaName ? dialog.SchemaName : "default-schema",
+ Dialog = new AdaptiveDialog.Builder(dialog),
+ }
+ }
+ }.Build();
+
+ return bot.Descendants().OfType().First();
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowOptions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowOptions.cs
new file mode 100644
index 0000000000..1f61d27253
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/DeclarativeWorkflowOptions.cs
@@ -0,0 +1,44 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.Workflows.Declarative;
+
+///
+/// Configuration options for workflow execution.
+///
+public sealed class DeclarativeWorkflowOptions(WorkflowAgentProvider agentProvider)
+{
+ ///
+ /// Defines the agent provider.
+ ///
+ public WorkflowAgentProvider AgentProvider { get; } = Throw.IfNull(agentProvider, nameof(agentProvider));
+
+ ///
+ /// Defines the configuration settings for the workflow.
+ ///
+ public IConfiguration? Configuration { get; init; }
+
+ ///
+ /// Optionally identifies a continued workflow conversation.
+ ///
+ public string? ConversationId { get; init; }
+
+ ///
+ /// Defines the maximum number of nested calls allowed in a PowerFx formula.
+ ///
+ public int? MaximumCallDepth { get; init; }
+
+ ///
+ /// Defines the maximum allowed length for expressions evaluated in the workflow.
+ ///
+ public int? MaximumExpressionLength { get; init; }
+
+ ///
+ /// Gets the used to create loggers for workflow components.
+ ///
+ public ILoggerFactory LoggerFactory { get; init; } = NullLoggerFactory.Instance;
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Exceptions/DeclarativeActionException.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Exceptions/DeclarativeActionException.cs
new file mode 100644
index 0000000000..6849a57bf2
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Exceptions/DeclarativeActionException.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+
+namespace Microsoft.Agents.Workflows.Declarative;
+
+///
+/// Represents an exception that occurs during action execution.
+///
+public sealed class DeclarativeActionException : DeclarativeWorkflowException
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public DeclarativeActionException()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class with a specified error message.
+ ///
+ /// The error message that explains the reason for the exception.
+ public DeclarativeActionException(string? message) : base(message)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception.
+ ///
+ /// The error message that explains the reason for the exception.
+ /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified.
+ public DeclarativeActionException(string? message, Exception? innerException) : base(message, innerException)
+ {
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Exceptions/DeclarativeModelException.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Exceptions/DeclarativeModelException.cs
new file mode 100644
index 0000000000..f6b6adfb11
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Exceptions/DeclarativeModelException.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+
+namespace Microsoft.Agents.Workflows.Declarative;
+
+///
+/// Represents an exception that occurs when the declarative model is not supported.
+///
+public class DeclarativeModelException : DeclarativeWorkflowException
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public DeclarativeModelException()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class with a specified error message.
+ ///
+ /// The error message that explains the reason for the exception.
+ public DeclarativeModelException(string? message) : base(message)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception.
+ ///
+ /// The error message that explains the reason for the exception.
+ /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified.
+ public DeclarativeModelException(string? message, Exception? innerException) : base(message, innerException)
+ {
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Exceptions/DeclarativeWorkflowException.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Exceptions/DeclarativeWorkflowException.cs
new file mode 100644
index 0000000000..1cad0fb2c8
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Exceptions/DeclarativeWorkflowException.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+
+namespace Microsoft.Agents.Workflows.Declarative;
+
+///
+/// Represents any exception that occurs during the execution of a process workflow.
+///
+public class DeclarativeWorkflowException : Exception
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public DeclarativeWorkflowException()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class with a specified error message.
+ ///
+ /// The error message that explains the reason for the exception.
+ public DeclarativeWorkflowException(string? message) : base(message)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception.
+ ///
+ /// The error message that explains the reason for the exception.
+ /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified.
+ public DeclarativeWorkflowException(string? message, Exception? innerException) : base(message, innerException)
+ {
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/BotElementExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/BotElementExtensions.cs
new file mode 100644
index 0000000000..bb409944f2
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/BotElementExtensions.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Bot.ObjectModel;
+
+namespace Microsoft.Agents.Workflows.Declarative.Extensions;
+
+internal static class BotElementExtensions
+{
+ public static string? GetParentId(this BotElement element) => element.Parent?.GetId();
+
+ public static string GetId(this BotElement element)
+ {
+ return element switch
+ {
+ DialogAction action => action.Id.Value,
+ ConditionItem conditionItem => conditionItem.Id ?? throw new DeclarativeModelException($"Undefined identifier for {nameof(ConditionItem)} that is member of {conditionItem.GetParentId() ?? "(root)"}."),
+ OnActivity activity => activity.Id.Value,
+ _ => throw new DeclarativeModelException($"Unknown identify for element type: {element.GetType().Name}"),
+ };
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/ChatMessageExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/ChatMessageExtensions.cs
new file mode 100644
index 0000000000..89a65c86b2
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/ChatMessageExtensions.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using Microsoft.Bot.ObjectModel;
+using Microsoft.Extensions.AI;
+using Microsoft.PowerFx.Types;
+
+namespace Microsoft.Agents.Workflows.Declarative.Extensions;
+
+internal static class ChatMessageExtensions
+{
+ // ISSUE #485 - Align with message type updated OM is available.
+ public static RecordValue ToRecord(this ChatMessage message) =>
+ RecordValue.NewRecordFromFields(message.GetMessageFields());
+
+ private static IEnumerable GetMessageFields(this ChatMessage message)
+ {
+ yield return new NamedValue(nameof(DialogAction.Id), message.MessageId.ToFormulaValue());
+ yield return new NamedValue(nameof(ChatMessage.Role), FormulaValue.New(message.Role.Value));
+ yield return new NamedValue(nameof(ChatMessage.AuthorName), message.AuthorName.ToFormulaValue());
+ yield return new NamedValue(nameof(ChatMessage.Text), message.Text.ToFormulaValue());
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DataValueExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DataValueExtensions.cs
new file mode 100644
index 0000000000..81666b6013
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DataValueExtensions.cs
@@ -0,0 +1,82 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Bot.ObjectModel;
+using Microsoft.PowerFx.Types;
+
+namespace Microsoft.Agents.Workflows.Declarative.Extensions;
+
+internal static class DataValueExtensions
+{
+ public static FormulaValue ToFormulaValue(this DataValue? value) =>
+ value switch
+ {
+ null => FormulaValue.NewBlank(),
+ BlankDataValue blankValue => BlankValue.NewBlank(),
+ BooleanDataValue boolValue => FormulaValue.New(boolValue.Value),
+ NumberDataValue numberValue => FormulaValue.New(numberValue.Value),
+ FloatDataValue floatValue => FormulaValue.New(floatValue.Value),
+ StringDataValue stringValue => FormulaValue.New(stringValue.Value),
+ DateTimeDataValue dateTimeValue => FormulaValue.New(dateTimeValue.Value.DateTime),
+ DateDataValue dateValue => FormulaValue.NewDateOnly(dateValue.Value),
+ TimeDataValue timeValue => FormulaValue.New(timeValue.Value),
+ TableDataValue tableValue =>
+ FormulaValue.NewTable(
+ tableValue.Values.FirstOrDefault()?.ParseRecordType() ?? RecordType.Empty(),
+ tableValue.Values.Select(value => value.ToRecordValue())),
+ RecordDataValue recordValue => recordValue.ToRecordValue(),
+ OptionDataValue optionValue => FormulaValue.New(optionValue.Value.Value),
+ _ => FormulaValue.NewError(new Microsoft.PowerFx.ExpressionError { Message = $"Unknown literal type: {value.GetType().Name}" }),
+ };
+
+ public static FormulaType ToFormulaType(this DataValue? value) => value?.GetDataType().ToFormulaType() ?? FormulaType.Blank;
+
+ public static FormulaType ToFormulaType(this DataType? type) =>
+ type switch
+ {
+ null => FormulaType.Blank,
+ BooleanDataType => FormulaType.Boolean,
+ NumberDataType => FormulaType.Decimal,
+ FloatDataType => FormulaType.Number,
+ StringDataType => FormulaType.String,
+ DateTimeDataType => FormulaType.DateTime,
+ DateDataType => FormulaType.Date,
+ TimeDataType => FormulaType.Time,
+ ColorDataType => FormulaType.Color,
+ GuidDataType => FormulaType.Guid,
+ FileDataType => FormulaType.Blob,
+ RecordDataType => RecordType.Empty(),
+ TableDataType => TableType.Empty(),
+ OptionSetDataType => FormulaType.String,
+ AnyType => FormulaType.UntypedObject,
+ _ => FormulaType.Unknown,
+ };
+
+ public static FormulaValue NewBlank(this DataType? type) => FormulaValue.NewBlank(type?.ToFormulaType() ?? FormulaType.Blank);
+
+ public static RecordValue ToRecordValue(this RecordDataValue recordDataValue) =>
+ FormulaValue.NewRecordFromFields(
+ recordDataValue.Properties.Select(
+ property => new NamedValue(property.Key, property.Value.ToFormulaValue())));
+
+ public static RecordType ToRecordType(this RecordDataType record)
+ {
+ RecordType recordType = RecordType.Empty();
+ foreach (KeyValuePair property in record.Properties)
+ {
+ recordType = recordType.Add(property.Key, property.Value.Type.ToFormulaType());
+ }
+ return recordType;
+ }
+
+ private static RecordType ParseRecordType(this RecordDataValue record)
+ {
+ RecordType recordType = RecordType.Empty();
+ foreach (KeyValuePair property in record.Properties)
+ {
+ recordType = recordType.Add(property.Key, property.Value.ToFormulaType());
+ }
+ return recordType;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DeclarativeWorkflowOptionsExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DeclarativeWorkflowOptionsExtensions.cs
new file mode 100644
index 0000000000..19b2a11819
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/DeclarativeWorkflowOptionsExtensions.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Agents.Workflows.Declarative.PowerFx;
+using Microsoft.PowerFx;
+
+namespace Microsoft.Agents.Workflows.Declarative.Extensions;
+
+internal static class DeclarativeWorkflowOptionsExtensions
+{
+ private const int DefaultMaximumExpressionLength = 10000;
+
+ public static RecalcEngine CreateRecalcEngine(this DeclarativeWorkflowOptions? context) =>
+ RecalcEngineFactory.Create(context?.MaximumExpressionLength ?? DefaultMaximumExpressionLength, context?.MaximumCallDepth);
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs
new file mode 100644
index 0000000000..568f3ba104
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/FormulaValueExtensions.cs
@@ -0,0 +1,264 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Dynamic;
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using Microsoft.Bot.ObjectModel;
+using Microsoft.PowerFx.Types;
+using BlankType = Microsoft.PowerFx.Types.BlankType;
+
+namespace Microsoft.Agents.Workflows.Declarative.Extensions;
+
+internal static class FormulaValueExtensions
+{
+ private static readonly JsonSerializerOptions s_options = new() { WriteIndented = true };
+
+ public static FormulaValue NewBlank(this FormulaType? type) => FormulaValue.NewBlank(type ?? FormulaType.Blank);
+
+ public static FormulaValue ToFormulaValue(this object? value) =>
+ value switch
+ {
+ null => FormulaValue.NewBlank(),
+ FormulaValue formulaValue => formulaValue,
+ bool booleanValue => FormulaValue.New(booleanValue),
+ int decimalValue => FormulaValue.New(decimalValue),
+ long decimalValue => FormulaValue.New(decimalValue),
+ float decimalValue => FormulaValue.New(decimalValue),
+ decimal decimalValue => FormulaValue.New(decimalValue),
+ double numberValue => FormulaValue.New(numberValue),
+ string stringValue => FormulaValue.New(stringValue),
+ DateTime dateonlyValue when dateonlyValue.TimeOfDay == TimeSpan.Zero => FormulaValue.NewDateOnly(dateonlyValue),
+ DateTime datetimeValue => FormulaValue.New(datetimeValue),
+ TimeSpan timeValue => FormulaValue.New(timeValue),
+ object when value is IEnumerable tableValue => tableValue.ToTable(),
+ ExpandoObject expandoValue => expandoValue.ToRecord(),
+ _ => throw new DeclarativeModelException($"Unsupported variable type: {value.GetType().Name}"),
+ };
+
+ public static FormulaType GetFormulaType(this object? value) =>
+ value switch
+ {
+ null => FormulaType.Blank,
+ bool => FormulaType.Boolean,
+ int => FormulaType.Decimal,
+ long => FormulaType.Decimal,
+ float => FormulaType.Decimal,
+ decimal => FormulaType.Decimal,
+ double => FormulaType.Number,
+ string => FormulaType.String,
+ DateTime => FormulaType.DateTime,
+ TimeSpan => FormulaType.Time,
+ object when value is IEnumerable tableValue => tableValue.ToTableType(),
+ ExpandoObject expandoValue => expandoValue.ToRecordType(),
+ _ => FormulaType.Unknown,
+ };
+
+ public static DataValue ToDataValue(this FormulaValue value) =>
+ value switch
+ {
+ BooleanValue booleanValue => BooleanDataValue.Create(booleanValue.Value),
+ DecimalValue decimalValue => NumberDataValue.Create(decimalValue.Value),
+ NumberValue numberValue => FloatDataValue.Create(numberValue.Value),
+ DateValue dateValue => DateDataValue.Create(dateValue.GetConvertedValue(TimeZoneInfo.Utc)),
+ DateTimeValue datetimeValue => DateTimeDataValue.Create(datetimeValue.GetConvertedValue(TimeZoneInfo.Utc)),
+ TimeValue timeValue => TimeDataValue.Create(timeValue.Value),
+ StringValue stringValue => StringDataValue.Create(stringValue.Value),
+ BlankValue blankValue => DataValue.Blank(),
+ VoidValue voidValue => DataValue.Blank(),
+ RecordValue recordValue => recordValue.ToRecord(),
+ TableValue tableValue => tableValue.ToTable(),
+ _ => throw new DeclarativeModelException($"Unsupported variable type: {value.GetType().Name}"),
+ };
+
+ public static DataType GetDataType(this FormulaValue value) =>
+ value switch
+ {
+ null => DataType.Blank,
+ BooleanValue => DataType.Boolean,
+ DecimalValue => DataType.Number,
+ NumberValue => DataType.Float,
+ DateValue => DataType.Date,
+ DateTimeValue => DataType.DateTime,
+ TimeValue => DataType.Time,
+ StringValue => DataType.String,
+ BlankValue => DataType.Blank,
+ ColorValue => DataType.Color,
+ GuidValue => DataType.Guid,
+ BlobValue => DataType.File,
+ RecordValue recordValue => recordValue.Type.ToDataType(),
+ TableValue tableValue => tableValue.Type.ToDataType(),
+ UntypedObjectValue => DataType.Any,
+ _ => DataType.Unspecified,
+ };
+
+ public static DataType ToDataType(this FormulaType type) =>
+ type switch
+ {
+ null => DataType.Blank,
+ BooleanType => DataType.Boolean,
+ DecimalType => DataType.Number,
+ NumberType => DataType.Float,
+ DateType => DataType.Date,
+ DateTimeType => DataType.DateTime,
+ TimeType => DataType.Time,
+ StringType => DataType.String,
+ BlankType => DataType.Blank,
+ ColorType => DataType.Color,
+ GuidType => DataType.Guid,
+ BlobType => DataType.File,
+ RecordType recordType => recordType.ToDataType(),
+ TableType tableType => tableType.ToDataType(),
+ UntypedObjectType => DataType.Any,
+ _ => DataType.Unspecified,
+ };
+
+ public static string Format(this FormulaValue value) =>
+ value switch
+ {
+ BooleanValue booleanValue => $"{booleanValue.Value}",
+ DecimalValue decimalValue => $"{decimalValue.Value}",
+ NumberValue numberValue => $"{numberValue.Value}",
+ DateValue dateValue => $"{dateValue.GetConvertedValue(TimeZoneInfo.Utc)}",
+ DateTimeValue datetimeValue => $"{datetimeValue.GetConvertedValue(TimeZoneInfo.Utc)}",
+ TimeValue timeValue => $"{timeValue.Value}",
+ StringValue stringValue => stringValue.Value,
+ BlankValue blankValue => string.Empty,
+ VoidValue voidValue => string.Empty,
+ ColorValue colorValue => colorValue.Value.ToString(),
+ GuidValue guidValue => guidValue.Value.ToString("N"),
+ TableValue tableValue => tableValue.ToJson().ToJsonString(s_options),
+ RecordValue recordValue => recordValue.ToJson().ToJsonString(s_options),
+ ErrorValue errorValue => $"Error:{Environment.NewLine}{string.Join(Environment.NewLine, errorValue.Errors.Select(error => $"{error.MessageKey}: {error.Message}"))}",
+ _ => $"[{value.GetType().Name}]",
+ };
+
+ public static TableDataValue ToTable(this TableValue value) =>
+ TableDataValue.TableFromRecords(value.Rows.Select(row => row.Value.ToRecord()).ToImmutableArray());
+
+ public static RecordDataValue ToRecord(this RecordValue value) =>
+ RecordDataValue.RecordFromFields(value.OriginalFields.Select(field => field.GetKeyValuePair()).ToImmutableArray());
+
+ private static RecordDataType ToDataType(this RecordType record)
+ {
+ RecordDataType recordType = new();
+ foreach (string fieldName in record.FieldNames)
+ {
+ recordType.Properties.Add(fieldName, PropertyInfo.Create(record.GetFieldType(fieldName).ToDataType()));
+ }
+ return recordType;
+ }
+
+ private static TableDataType ToDataType(this TableType table)
+ {
+ TableDataType tableType = new();
+ foreach (string fieldName in table.FieldNames)
+ {
+ tableType.Properties.Add(fieldName, PropertyInfo.Create(table.GetFieldType(fieldName).ToDataType()));
+ }
+ return tableType;
+ }
+
+ private static RecordType ToRecordType(this ExpandoObject value)
+ {
+ RecordType recordType = RecordType.Empty();
+ foreach (KeyValuePair property in value)
+ {
+ recordType.Add(property.Key, property.Value.GetFormulaType());
+ }
+ return recordType;
+ }
+
+ private static RecordValue ToRecord(this ExpandoObject value) =>
+ FormulaValue.NewRecordFromFields(
+ value.Select(
+ property => new NamedValue(property.Key, property.Value.ToFormulaValue())));
+
+ private static TableType ToTableType(this IEnumerable value)
+ {
+ Type valueType = value.GetType();
+ Type? elementType = valueType.GetElementType() ?? valueType.GetGenericArguments().FirstOrDefault();
+
+ if (elementType is not null)
+ {
+ if (elementType != typeof(ExpandoObject))
+ {
+ throw new DeclarativeModelException($"Invalid table element: {elementType.Name}");
+ }
+
+ foreach (ExpandoObject element in value)
+ {
+ return element.ToRecordType().ToTable();
+ }
+ }
+
+ return TableType.Empty();
+ }
+
+ private static TableValue ToTable(this IEnumerable value)
+ {
+ Type valueType = value.GetType();
+ Type? elementType = valueType.GetElementType() ?? valueType.GetGenericArguments().FirstOrDefault();
+
+ if (elementType is null)
+ {
+ return FormulaValue.NewTable(RecordType.EmptySealed());
+ }
+
+ if (elementType != typeof(ExpandoObject))
+ {
+ throw new DeclarativeModelException($"Invalid table element: {elementType.Name}");
+ }
+
+ List records = [.. value.OfType().Select(element => element.ToRecord())];
+
+ return FormulaValue.NewTable(value.ToTableType().ToRecord(), records);
+ }
+
+ private static KeyValuePair GetKeyValuePair(this NamedValue value) => new(value.Name, value.Value.ToDataValue());
+
+ private static JsonNode ToJson(this FormulaValue value) =>
+ value switch
+ {
+ BooleanValue booleanValue => JsonValue.Create(booleanValue.Value),
+ DecimalValue decimalValue => JsonValue.Create(decimalValue.Value),
+ NumberValue numberValue => JsonValue.Create(numberValue.Value),
+ DateValue dateValue => JsonValue.Create(dateValue.GetConvertedValue(TimeZoneInfo.Utc)),
+ DateTimeValue datetimeValue => JsonValue.Create(datetimeValue.GetConvertedValue(TimeZoneInfo.Utc)),
+ TimeValue timeValue => JsonValue.Create($"{timeValue.Value}"),
+ StringValue stringValue => JsonValue.Create(stringValue.Value),
+ GuidValue guidValue => JsonValue.Create(guidValue.Value),
+ RecordValue recordValue => recordValue.ToJson(),
+ TableValue tableValue => tableValue.ToJson(),
+ BlankValue blankValue => JsonValue.Create(string.Empty),
+ _ => $"[{value.GetType().Name}]",
+ };
+
+ private static JsonArray ToJson(this TableValue value)
+ {
+ return new([.. GetJsonElements()]);
+
+ IEnumerable GetJsonElements()
+ {
+ foreach (DValue row in value.Rows)
+ {
+ RecordValue recordValue = row.Value;
+ yield return recordValue.ToJson();
+ }
+ }
+ }
+
+ private static JsonObject ToJson(this RecordValue value)
+ {
+ JsonObject jsonObject = [];
+ foreach (NamedValue field in value.OriginalFields)
+ {
+ jsonObject.Add(field.Name, field.Value.ToJson());
+ }
+ return jsonObject;
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/PropertyPathExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/PropertyPathExtensions.cs
new file mode 100644
index 0000000000..2fbc5c28ef
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/PropertyPathExtensions.cs
@@ -0,0 +1,10 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Bot.ObjectModel;
+
+namespace Microsoft.Agents.Workflows.Declarative.Extensions;
+
+internal static class PropertyPathExtensions
+{
+ public static string Format(this PropertyPath path) => string.Join(".", path.Segments());
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/RecordDataTypeExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/RecordDataTypeExtensions.cs
new file mode 100644
index 0000000000..977a0217db
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/RecordDataTypeExtensions.cs
@@ -0,0 +1,49 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.Json;
+using Microsoft.Bot.ObjectModel;
+using Microsoft.PowerFx.Types;
+
+namespace Microsoft.Agents.Workflows.Declarative.Extensions;
+
+internal static class RecordDataTypeExtensions
+{
+ public static RecordValue ParseRecord(this RecordDataType recordType, JsonElement currentElement)
+ {
+ return FormulaValue.NewRecordFromFields(ParseValues());
+
+ IEnumerable ParseValues()
+ {
+ foreach (KeyValuePair property in recordType.Properties)
+ {
+ JsonElement propertyElement = currentElement.GetProperty(property.Key);
+ FormulaValue? parsedValue =
+ property.Value.Type switch
+ {
+ StringDataType => StringValue.New(propertyElement.GetString()),
+ NumberDataType => NumberValue.New(propertyElement.GetDecimal()),
+ BooleanDataType => BooleanValue.New(propertyElement.GetBoolean()),
+ DateTimeDataType => DateTimeValue.New(propertyElement.GetDateTime()),
+ DateDataType => DateValue.New(propertyElement.GetDateTime()),
+ TimeDataType => TimeValue.New(propertyElement.GetDateTimeOffset().TimeOfDay),
+ RecordDataType recordType => recordType.ParseRecord(propertyElement),
+ TableDataType tableType => ParseTable(tableType, propertyElement),
+ _ => throw new InvalidOperationException($"Unsupported data type '{property.Value.Type}' for property '{property.Key}'"),
+ };
+ yield return new NamedValue(property.Key, parsedValue);
+ }
+
+ static TableValue ParseTable(TableDataType tableType, JsonElement propertyElement)
+ {
+ RecordDataType recordType = tableType.ToRecord();
+ return
+ FormulaValue.NewTable(
+ recordType.ToRecordType(),
+ propertyElement.EnumerateArray().Select(tableElement => ParseRecord(recordType, tableElement)));
+ }
+ }
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/StringExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/StringExtensions.cs
new file mode 100644
index 0000000000..0f3a078d2c
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/StringExtensions.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text.RegularExpressions;
+using Microsoft.PowerFx.Types;
+
+namespace Microsoft.Agents.Workflows.Declarative.Extensions;
+
+internal static class StringExtensions
+{
+ private static readonly Regex s_regex = new(@"^```(?:\w*)\s*([\s\S]*?)\s*```$", RegexOptions.Compiled | RegexOptions.Multiline);
+
+ public static string TrimJsonDelimiter(this string value)
+ {
+ Match match = s_regex.Match(value.Trim());
+ if (match.Success)
+ {
+ return match.Groups[1].Value.Trim();
+ }
+
+ return value.Trim();
+ }
+
+ public static FormulaValue ToFormulaValue(this string? value) =>
+ string.IsNullOrWhiteSpace(value) ? FormulaValue.NewBlank() : FormulaValue.New(value);
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/TemplateExtensions.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/TemplateExtensions.cs
new file mode 100644
index 0000000000..9a41554a18
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Extensions/TemplateExtensions.cs
@@ -0,0 +1,49 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Bot.ObjectModel;
+using Microsoft.PowerFx;
+using Microsoft.PowerFx.Types;
+
+namespace Microsoft.Agents.Workflows.Declarative.Extensions;
+
+internal static class TemplateExtensions
+{
+ public static string? Format(this RecalcEngine engine, IEnumerable template)
+ {
+ return string.Concat(template.Select(line => engine.Format(line)));
+ }
+
+ public static string? Format(this RecalcEngine engine, TemplateLine? line)
+ {
+ return string.Concat(line?.Segments.Select(segment => engine.Format(segment)) ?? [string.Empty]);
+ }
+
+ public static string? Format(this RecalcEngine engine, TemplateSegment segment)
+ {
+ if (segment is TextSegment textSegment)
+ {
+ return textSegment.Value;
+ }
+
+ if (segment is ExpressionSegment expressionSegment)
+ {
+ if (expressionSegment.Expression is not null)
+ {
+ if (expressionSegment.Expression.ExpressionText is not null)
+ {
+ FormulaValue expressionValue = engine.Eval(expressionSegment.Expression.ExpressionText);
+ return expressionValue.Format();
+ }
+ if (expressionSegment.Expression.VariableReference is not null)
+ {
+ FormulaValue expressionValue = engine.Eval(expressionSegment.Expression.VariableReference.ToString());
+ return expressionValue.Format();
+ }
+ }
+ }
+
+ throw new DeclarativeModelException($"Unsupported segment type: {segment.GetType().Name}");
+ }
+}
diff --git a/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs
new file mode 100644
index 0000000000..0d0138f261
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.Workflows.Declarative/Interpreter/DeclarativeActionExecutor.cs
@@ -0,0 +1,120 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Agents.Workflows.Declarative.Extensions;
+using Microsoft.Agents.Workflows.Reflection;
+using Microsoft.Bot.ObjectModel;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.PowerFx.Types;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.Workflows.Declarative.Interpreter;
+
+internal sealed record class DeclarativeExecutorResult(string ExecutorId, object? Result = null);
+
+internal abstract class DeclarativeActionExecutor(TAction model, DeclarativeWorkflowState state) :
+ WorkflowActionExecutor(model, state)
+ where TAction : DialogAction
+{
+ public new TAction Model => (TAction)base.Model;
+}
+
+internal abstract class WorkflowActionExecutor :
+ ReflectingExecutor,
+ IMessageHandler
+{
+ public const string RootActionId = "(root)";
+
+ private static readonly ImmutableHashSet s_mutableScopes =
+ new HashSet
+ {
+ VariableScopeNames.Topic,
+ VariableScopeNames.Global,
+ }.ToImmutableHashSet();
+
+ private string? _parentId;
+
+ protected WorkflowActionExecutor(DialogAction model, DeclarativeWorkflowState state)
+ : base(model.Id.Value)
+ {
+ if (!model.HasRequiredProperties)
+ {
+ throw new DeclarativeModelException($"Missing required properties for element: {model.GetId()} ({model.GetType().Name}).");
+ }
+
+ this.Model = model;
+ this.State = state;
+ }
+
+ public DialogAction Model { get; }
+
+ public string ParentId => this._parentId ??= this.Model.GetParentId() ?? RootActionId;
+
+ internal ILogger Logger { get; set; } = NullLogger.Instance;
+
+ protected DeclarativeWorkflowState State { get; }
+
+ ///
+ public async ValueTask HandleAsync(DeclarativeExecutorResult message, IWorkflowContext context)
+ {
+ if (this.Model.Disabled)
+ {
+ Debug.WriteLine($"DISABLED {this.GetType().Name} [{this.Id}]");
+ return;
+ }
+
+ await this.State.RestoreAsync(context, default).ConfigureAwait(false);
+
+ try
+ {
+ object? result = await this.ExecuteAsync(context, cancellationToken: default).ConfigureAwait(false);
+
+ await context.SendMessageAsync(new DeclarativeExecutorResult(this.Id, result)).ConfigureAwait(false);
+ }
+ catch (DeclarativeActionException exception)
+ {
+ Debug.WriteLine($"ERROR [{this.Id}] {exception.GetType().Name}\n{exception.Message}");
+ throw;
+ }
+ catch (Exception exception)
+ {
+ Debug.WriteLine($"ERROR [{this.Id}] {exception.GetType().Name}\n{exception.Message}");
+ throw new DeclarativeActionException($"Unhandled workflow failure - #{this.Id} ({this.Model.GetType().Name})", exception);
+ }
+ }
+
+ protected abstract ValueTask