Skip to content

.NET: [Feature Branch] Add basic durable workflow support#3648

Open
kshyju wants to merge 5 commits intomicrosoft:feat/durable_taskfrom
kshyju:dwf_console_1
Open

.NET: [Feature Branch] Add basic durable workflow support#3648
kshyju wants to merge 5 commits intomicrosoft:feat/durable_taskfrom
kshyju:dwf_console_1

Conversation

@kshyju
Copy link
Contributor

@kshyju kshyju commented Feb 3, 2026

Motivation and Context

This PR adds durable workflow support to the Agent Framework, enabling workflows to run as Durable Task orchestrations. This addresses the need for long-running, reliable workflow execution that can survive process restarts, handle failures gracefully, and supports distributed workflow execution across multiple nodes.

Note: This is the first in a series of PRs for durable workflow support. This PR establishes the foundational APIs and infrastructure. Follow-up PRs will add additional features such as events, yield output, human-in-the-loop patterns, and sub-workflow support.

Key scenarios enabled:

  • Running workflows as durable orchestrations with automatic state persistence
  • Sequential and concurrent workflow execution patterns
  • Fan-out/fan-in workflow patterns
  • Conditional routing based on runtime conditions
  • Integration with existing Durable Task infrastructure

Description

This PR introduces a new set of public APIs for running workflows as Durable Task orchestrations.

New Public APIs

Workflow Execution Interfaces

API Description
IWorkflowRun Represents a running workflow instance with event tracking
IAwaitableWorkflowRun Extends IWorkflowRun with completion awaiting capability
IWorkflowClient Client interface for starting and managing workflow executions
public interface IWorkflowRun
{
    string RunId { get; }
    IEnumerable<WorkflowEvent> OutgoingEvents { get; }
    IEnumerable<WorkflowEvent> NewEvents { get; }
    int NewEventCount { get; }
}

public interface IAwaitableWorkflowRun : IWorkflowRun
{
    ValueTask<TResult?> WaitForCompletionAsync<TResult>(CancellationToken cancellationToken = default);
}

public interface IWorkflowClient
{
    ValueTask<IWorkflowRun> RunAsync<TInput>(Workflow workflow, TInput input, string? runId = null, CancellationToken cancellationToken = default) where TInput : notnull;
    ValueTask<IWorkflowRun> RunAsync(Workflow workflow, string input, string? runId = null, CancellationToken cancellationToken = default);
}

Configuration Options

API Description
DurableOptions Root configuration container for durable agents and workflows
DurableWorkflowOptions Configuration for registering and managing durable workflows
public sealed class DurableOptions
{
    public DurableAgentsOptions Agents { get; }
    public DurableWorkflowOptions Workflows { get; }
}

public sealed class DurableWorkflowOptions
{
    public IReadOnlyDictionary<string, Workflow> Workflows { get; }
    public void AddWorkflow(Workflow workflow);
    public void AddWorkflows(IEnumerable<Workflow> workflows);
}

Service Collection Extensions

API Description
ConfigureDurableOptions Configures both durable agents and workflows
ConfigureDurableWorkflows Configures durable workflows only
public static IServiceCollection ConfigureDurableOptions(
    this IServiceCollection services,
    Action<DurableOptions> configure,
    Action<IDurableTaskWorkerBuilder>? workerBuilder = null,
    Action<IDurableTaskClientBuilder>? clientBuilder = null);

public static IServiceCollection ConfigureDurableWorkflows(
    this IServiceCollection services,
    Action<DurableWorkflowOptions> configure,
    Action<IDurableTaskWorkerBuilder>? workerBuilder = null,
    Action<IDurableTaskClientBuilder>? clientBuilder = null);

Usage Examples

Sequential Workflow (Sample: 01_SequentialWorkflow)

// Define executors for the workflow
OrderLookup orderLookup = new();
OrderCancel orderCancel = new();
SendEmail sendEmail = new();

// Build the CancelOrder workflow: OrderLookup -> OrderCancel -> SendEmail
Workflow cancelOrder = new WorkflowBuilder(orderLookup)
    .WithName("CancelOrder")
    .WithDescription("Cancel an order and notify the customer")
    .AddEdge(orderLookup, orderCancel)
    .AddEdge(orderCancel, sendEmail)
    .Build();

// Register durable workflows
services.ConfigureDurableWorkflows(
    workflowOptions => workflowOptions.AddWorkflow(cancelOrder),
    workerBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString),
    clientBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString));

// Run a workflow
IWorkflowClient client = serviceProvider.GetRequiredService<IWorkflowClient>();
IAwaitableWorkflowRun run = (IAwaitableWorkflowRun)await client.RunAsync(cancelOrder, orderId);
string? result = await run.WaitForCompletionAsync<string>();

Fan-out/Fan-in Workflow (Sample: 02_ConcurrentWorkflow)

// Define executors: 2 class-based executors and 2 AI agents
ParseQuestionExecutor parseQuestion = new();
AIAgent physicist = chatClient.AsAIAgent("You are a physics expert.", "Physicist");
AIAgent chemist = chatClient.AsAIAgent("You are a chemistry expert.", "Chemist");
AggregatorExecutor aggregator = new();

// Build workflow: ParseQuestion -> [Physicist, Chemist] (parallel) -> Aggregator
Workflow workflow = new WorkflowBuilder(parseQuestion)
    .WithName("ExpertReview")
    .AddFanOutEdge(parseQuestion, [physicist, chemist])
    .AddFanInEdge([physicist, chemist], aggregator)
    .Build();

// Register using ConfigureDurableOptions (supports both agents and workflows)
services.ConfigureDurableOptions(
    options => options.Workflows.AddWorkflow(workflow),
    workerBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString),
    clientBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString));

Conditional Edges Workflow (Sample: 03_ConditionalEdges)

// Define executors for the workflow
OrderIdParser orderParser = new();
OrderEnrich orderEnrich = new();
PaymentProcessor paymentProcessor = new();
NotifyFraud notifyFraud = new();

// Build workflow with conditional routing based on customer status
// OrderIdParser -> OrderEnrich --[IsBlocked]--> NotifyFraud
//                             |--[NotBlocked]--> PaymentProcessor
Workflow orderAudit = new WorkflowBuilder(orderParser)
    .WithName("OrderAudit")
    .WithDescription("Audit order and route based on customer status")
    .AddEdge(orderParser, orderEnrich)
    .AddEdge(orderEnrich, notifyFraud, condition: OrderRouteConditions.WhenBlocked())
    .AddEdge(orderEnrich, paymentProcessor, condition: OrderRouteConditions.WhenNotBlocked())
    .Build();

// Condition functions for routing logic
internal static class OrderRouteConditions
{
    internal static Func<Order?, bool> WhenBlocked() => 
        order => order?.Customer?.IsBlocked == true;

    internal static Func<Order?, bool> WhenNotBlocked() => 
        order => order?.Customer?.IsBlocked == false;
}

Validation/Testing

Samples Added:

Sample Description
samples/Durable/Workflow/ConsoleApps/01_SequentialWorkflow Demonstrates a simple sequential workflow pattern
samples/Durable/Workflow/ConsoleApps/02_ConcurrentWorkflow Demonstrates fan-out/fan-in pattern with AI agents
samples/Durable/Workflow/ConsoleApps/03_ConditionalEdges Demonstrates conditional routing based on runtime conditions

Integration Tests: Integration tests have been added to run and validate the sample workflows, ensuring end-to-end functionality works as expected.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • The PR follows the Contribution Guidelines
  • All unit tests pass, and I have added new tests where possible
  • Is this a breaking change? No

@markwallace-microsoft markwallace-microsoft added documentation Improvements or additions to documentation .NET workflows Related to Workflows in agent-framework labels Feb 3, 2026
[Collection("Samples")]
[Trait("Category", "SampleValidation")]
public sealed class ConsoleAppSamplesValidation(ITestOutputHelper outputHelper) : IAsyncLifetime
public sealed class ConsoleAppSamplesValidation(ITestOutputHelper outputHelper) : SamplesValidationBase(outputHelper)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change here is to move common logic to SamplesValidationBase so that both this and the workflow's console app samples validation IT can use same code.

}
}

private sealed class DefaultDataConverter : DataConverter
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class was moved to DurableDataConverter (renamed)

/// This converter handles special cases like <see cref="DurableAgentState"/> using source-generated
/// JSON contexts for AOT compatibility, and falls back to reflection-based serialization for other types.
/// </remarks>
internal sealed class DurableDataConverter : DataConverter
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was the previously called DefaultDataConverter. I moved it to this file and renamed.

</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="Microsoft.Agents.AI.DurableTask" />
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was added after talking to MAF folks. we are still discussing the proposal about the api change which may go in the core library. In that case, this will be removed.

/// Base class for sample validation integration tests providing shared infrastructure
/// setup and utility methods for running console app samples.
/// </summary>
public abstract class SamplesValidationBase : IAsyncLifetime
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code here is moved from the existing agents console app samples validation file.

@kshyju kshyju requested review from cgillum and Copilot February 3, 2026 16:32
@kshyju kshyju changed the title .NET: Add basic durable workflow support .NET: [Feature Branch] Add basic durable workflow support Feb 3, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces the initial infrastructure for running Microsoft.Agents.AI.Workflows.Workflow graphs as Durable Task orchestrations, plus samples and tests that exercise sequential and concurrent patterns.

Changes:

  • Add durable workflow runtime components (options, DI extensions, orchestration runner, edge routing, executor dispatch, and JSON context) under Microsoft.Agents.AI.DurableTask.
  • Add public workflow execution APIs (IWorkflowClient, IWorkflowRun, IAwaitableWorkflowRun) and wire them into the Durable Task registration pipeline.
  • Add two durable workflow console samples (sequential and fan-out/fan-in with AI agents) and corresponding integration tests that validate them end-to-end, including shared DTS/Redis test infrastructure.

Reviewed changes

Copilot reviewed 46 out of 46 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
dotnet/tests/Microsoft.Agents.AI.DurableTask.UnitTests/Workflows/WorkflowNamingHelperTests.cs Adds unit tests for workflow/orchestration name conversion and executor ID parsing.
dotnet/tests/Microsoft.Agents.AI.DurableTask.UnitTests/Microsoft.Agents.AI.DurableTask.UnitTests.csproj References the Workflows project so workflow-related tests can compile.
dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/WorkflowConsoleAppSamplesValidation.cs Adds integration tests that drive the new durable workflow console samples and assert expected log lines and completion.
dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/SamplesValidationBase.cs Introduces shared infra for console-sample integration tests (DTS/Redis bootstrapping, process I/O, log streaming).
dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/ConsoleAppSamplesValidation.cs Refactors durable agent sample tests to reuse SamplesValidationBase and rely on overridden env-var wiring (e.g., Redis) for agents.
dotnet/src/Microsoft.Agents.AI.Workflows/Microsoft.Agents.AI.Workflows.csproj Grants internals visibility to Microsoft.Agents.AI.DurableTask so workflows internals can be analyzed.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/WorkflowNamingHelper.cs Centralizes naming conventions for orchestration function names and executor IDs, including stripping GUID suffixes.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/WorkflowGraphInfo.cs Defines the minimal graph representation (successors, predecessors, conditions, output types) used for message-driven superstep execution.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/WorkflowAnalyzer.cs Analyzes Workflow instances into WorkflowExecutorInfo and WorkflowGraphInfo, identifies agent executors, and extracts executor output types.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/SentMessageInfo.cs Models messages emitted from executors via IWorkflowContext.SendMessageAsync for durable transport.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/IWorkflowRun.cs Public interface describing a workflow run by ID and exposing outgoing/new WorkflowEvent streams.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/IWorkflowClient.cs Public client interface for starting workflows with typed or string inputs and obtaining IWorkflowRun handles.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/IAwaitableWorkflowRun.cs Public interface extending IWorkflowRun with WaitForCompletionAsync<TResult> for runs that support awaiting completion.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/ExecutorRegistry.cs Internal registry mapping logical executor names to bindings for later instantiation, and a helper ExecutorRegistration record.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/EdgeRouters/IDurableEdgeRouter.cs Defines the routing contract for delivering messages between executors in a durable workflow.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/EdgeRouters/DurableFanOutEdgeRouter.cs Implements fan-out routing from a single source to multiple target edge routers.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/EdgeRouters/DurableEdgeMap.cs Builds per-executor routing tables and predecessor counts from WorkflowGraphInfo and manages message queues and fan-in detection.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/EdgeRouters/DurableDirectEdgeRouter.cs Implements direct edge routing with optional conditional evaluation and JSON-based deserialization of predecessor outputs.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableWorkflowServiceCollectionExtensions.cs Adds ConfigureDurableWorkflows DI extension that configures only workflows via the underlying ConfigureDurableOptions.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableWorkflowRunner.cs Orchestration runner that executes workflows via message-driven supersteps, dispatching executors and propagating outputs/messages between steps.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableWorkflowRun.cs Implements IAwaitableWorkflowRun for durable orchestrations using DurableTaskClient and exposes an event sink (not yet wired) for workflow events.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableWorkflowOptions.cs Adds workflow-specific options: registering named workflows, tracking executors, and auto-registering any referenced agents into DurableAgentsOptions.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableWorkflowJsonContext.cs Source-generated JSON serialization context for durable workflow payload types (activity input/output, sent messages, state dictionary).
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableWorkflowClient.cs Implements IWorkflowClient using DurableTaskClient to schedule new orchestration instances and wrap them in DurableWorkflowRun.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableMessageEnvelope.cs Wraps serialized message payloads with type-name and source-executor metadata for durable routing.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableExecutorDispatcher.cs Chooses between dispatching an executor as a Durable activity or AI agent, including entity-based execution for agents.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableActivityOutput.cs Defines the serialized output format from an activity, including result and any sent messages.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableActivityInput.cs Defines the serialized input format into an activity, including executor input, input type name, and shared state.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableActivityExecutor.cs Executes a bound executor from serialized durable input, invokes Executor.ExecuteAsync, and returns serialized output plus sent messages.
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableActivityContext.cs Provides an IWorkflowContext implementation for activities, capturing sent messages but currently no-ops for state/events/halt APIs.
dotnet/src/Microsoft.Agents.AI.DurableTask/ServiceCollectionExtensions.cs Switches the durable task data converter registration to the new shared DurableDataConverter.
dotnet/src/Microsoft.Agents.AI.DurableTask/Microsoft.Agents.AI.DurableTask.csproj References the Workflows project, enabling durable workflows to use workflow primitives.
dotnet/src/Microsoft.Agents.AI.DurableTask/Logs.cs Adds logging helpers for workflow lifecycle, supersteps, fan-in aggregation, executor results, and agent lookup failures.
dotnet/src/Microsoft.Agents.AI.DurableTask/DurableServiceCollectionExtensions.cs New DI extension entry point for configuring durable agents and workflows, registering orchestrators, activities, entities, and agent services.
dotnet/src/Microsoft.Agents.AI.DurableTask/DurableOptions.cs Introduces root options object combining durable agents and workflows, used by ConfigureDurableOptions.
dotnet/src/Microsoft.Agents.AI.DurableTask/DurableDataConverter.cs Shared DataConverter handling durable agent state via source-generated context and camel-cased JSON for other payloads.
dotnet/src/Microsoft.Agents.AI.DurableTask/DurableAgentsOptions.cs Adds ContainsAgent helper to check whether an agent has already been registered.
dotnet/samples/Durable/Workflow/ConsoleApps/02_ConcurrentWorkflow/README.md Documents the concurrent workflow sample, its fan-out/fan-in pattern, environment configuration, and example output.
dotnet/samples/Durable/Workflow/ConsoleApps/02_ConcurrentWorkflow/Program.cs Sample host wiring a fan-out/fan-in Workflow with 2 class executors and 2 AI agents into Durable Task via ConfigureDurableOptions.
dotnet/samples/Durable/Workflow/ConsoleApps/02_ConcurrentWorkflow/ExpertExecutors.cs Implements the ParseQuestionExecutor and AggregatorExecutor used in the concurrent workflow sample.
dotnet/samples/Durable/Workflow/ConsoleApps/02_ConcurrentWorkflow/02_ConcurrentWorkflow.csproj Project file for the concurrent workflow console sample, referencing core durable/AI projects.
dotnet/samples/Durable/Workflow/ConsoleApps/01_SequentialWorkflow/README.md Documents the sequential cancellation workflow sample and how to demonstrate durability across restarts.
dotnet/samples/Durable/Workflow/ConsoleApps/01_SequentialWorkflow/Program.cs Sample host wiring a 3-step order-cancellation Workflow into Durable Task via ConfigureDurableWorkflows.
dotnet/samples/Durable/Workflow/ConsoleApps/01_SequentialWorkflow/OrderCancelExecutors.cs Defines the OrderLookup, OrderCancel, and SendEmail executors used by the sequential workflow sample.
dotnet/samples/Durable/Workflow/ConsoleApps/01_SequentialWorkflow/01_SequentialWorkflow.csproj Project file for the sequential workflow console sample, referencing durable task and agent projects.
dotnet/agent-framework-dotnet.slnx Adds the new durable workflow sample projects to the .NET solution.

Additional note: Because this PR changes dotnet/src/Microsoft.Agents.AI.DurableTask/**, it likely needs a new bullet under the [Unreleased] section of dotnet/src/Microsoft.Agents.AI.DurableTask/CHANGELOG.md per the repo’s durable-task instructions.

Comment on lines 16 to 103
private readonly List<WorkflowEvent> _eventSink = [];
private int _lastBookmark;

/// <summary>
/// Initializes a new instance of the <see cref="DurableWorkflowRun"/> class.
/// </summary>
/// <param name="client">The durable task client for orchestration operations.</param>
/// <param name="instanceId">The unique instance ID for this orchestration run.</param>
/// <param name="workflowName">The name of the workflow being executed.</param>
internal DurableWorkflowRun(DurableTaskClient client, string instanceId, string workflowName)
{
this._client = client;
this.RunId = instanceId;
this.WorkflowName = workflowName;
}

/// <inheritdoc/>
public string RunId { get; }

/// <summary>
/// Gets the name of the workflow being executed.
/// </summary>
public string WorkflowName { get; }

/// <summary>
/// Waits for the workflow to complete and returns the result.
/// </summary>
/// <typeparam name="TResult">The expected result type.</typeparam>
/// <param name="cancellationToken">A cancellation token to observe.</param>
/// <returns>The result of the workflow execution.</returns>
/// <exception cref="InvalidOperationException">Thrown when the workflow failed or was terminated.</exception>
public async ValueTask<TResult?> WaitForCompletionAsync<TResult>(CancellationToken cancellationToken = default)
{
OrchestrationMetadata metadata = await this._client.WaitForInstanceCompletionAsync(
this.RunId,
getInputsAndOutputs: true,
cancellation: cancellationToken).ConfigureAwait(false);

if (metadata.RuntimeStatus == OrchestrationRuntimeStatus.Completed)
{
return metadata.ReadOutputAs<TResult>();
}

if (metadata.RuntimeStatus == OrchestrationRuntimeStatus.Failed)
{
string errorMessage = metadata.FailureDetails?.ErrorMessage ?? "Workflow execution failed.";
throw new InvalidOperationException(errorMessage);
}

throw new InvalidOperationException($"Workflow ended with unexpected status: {metadata.RuntimeStatus}");
}

/// <summary>
/// Waits for the workflow to complete and returns the string result.
/// </summary>
/// <param name="cancellationToken">A cancellation token to observe.</param>
/// <returns>The string result of the workflow execution.</returns>
public ValueTask<string?> WaitForCompletionAsync(CancellationToken cancellationToken = default)
=> this.WaitForCompletionAsync<string>(cancellationToken);

/// <summary>
/// Gets all events that have been collected from the workflow.
/// </summary>
public IEnumerable<WorkflowEvent> OutgoingEvents => this._eventSink;

/// <summary>
/// Gets the number of events collected since the last access to <see cref="NewEvents"/>.
/// </summary>
public int NewEventCount => this._eventSink.Count - this._lastBookmark;

/// <summary>
/// Gets all events collected since the last access to <see cref="NewEvents"/>.
/// </summary>
public IEnumerable<WorkflowEvent> NewEvents
{
get
{
if (this._lastBookmark >= this._eventSink.Count)
{
return [];
}

int currentBookmark = this._lastBookmark;
this._lastBookmark = this._eventSink.Count;

return this._eventSink.Skip(currentBookmark);
}
}
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DurableWorkflowRun exposes OutgoingEvents/NewEvents and maintains an _eventSink, but no code ever appends to this list, so callers will always see zero events even if executors call IWorkflowContext.AddEventAsync.
To make the event-tracking APIs useful, you either need to plumb workflow events from the durable execution path into this sink (similar to Run in Microsoft.Agents.AI.Workflows), or clearly document/guard that durable runs do not yet support event streaming.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is intentional. The next PR will use this in a meaningful way. I wanted to keep the props for now.

Comment on lines 95 to 100
foreach (WorkflowRegistrationInfo registration in registrations)
{
registry.AddOrchestratorFunc<string, string>(
registration.OrchestrationName,
(context, input) => RunWorkflowOrchestrationAsync(context, input, durableOptions));

Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The worker registration hard-codes orchestrator input and output types as string (AddOrchestratorFunc<string, string>), but IWorkflowClient.RunAsync<TInput> allows arbitrary TInput, so any non-string input will be serialized using DurableDataConverter and then deserialized back into a string before entering DurableWorkflowRunner.
This effectively ignores the generic TInput type and makes non-string inputs fragile or unusable; consider either constraining RunAsync to string input or updating the orchestrator registration and DurableWorkflowRunner to honor the actual input type (including propagating its type name into the initial DurableMessageEnvelope).

Copilot uses AI. Check for mistakes.
{
this._outputHelper.WriteLine("Starting shared DTS infrastructure...");
await this.StartDtsEmulatorAsync();
s_dtsInfrastructureStarted = true;
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Write to static field from instance method, property, or constructor.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fixed.

Copy link
Member

@cgillum cgillum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some initial comments. I'm not yet done reviewing.

object? messageObj = DeserializeForCondition(envelope.Message, this._sourceOutputType);
if (!this._condition(messageObj))
{
if (logger.IsEnabled(LogLevel.Debug))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use source-generated logging so that we don't need to add all this boilerplate?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, missed these. Updated now to use source gen logging.

}
catch (JsonException)
{
return json;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When is this case expected? Can you add comments?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really needed. I kept it as defensive fallback just in case the input is something which fails to deserialize. Removed now. Let the error bubble up.

/// <inheritdoc/>
public ValueTask YieldOutputAsync(
object output,
CancellationToken cancellationToken = default) => default;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we returning default for all these methods. Can you please add comments explaining?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry. I forgot to add that comment. But yes, in this first PR I am returning default. The follow up PRs will add implementation for these. Events (YieldOutputAsync uses events) support is coming in follow up PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also added a comment to this file. But the next PR will have implementation of these methods.


string serializedInput = JsonSerializer.Serialize(activityInput, DurableWorkflowJsonContext.Default.DurableActivityInput);

return await context.CallActivityAsync<string>(activityName, serializedInput).ConfigureAwait(true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment explaining why we do .ConfigureAwait(true) since most people reading this code won't intuitively understand.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comments to relevant files explaining why we do .ConfigureAwait(true).

/// </summary>
/// <remarks>
/// <para>
/// This is the durable equivalent of <c>MessageEnvelope</c> in the in-process runner.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is MessageEnvelope public? If so, prefer <see href="..."> instead of <c> so that you get the benefits of compiler warnings if this type can't be found or is renamed in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MessageEnvelope is internal as of today.

Copy link
Member

@cgillum cgillum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great start on this! I finished my first full round. Added a few comments below. Overall, I'm good with the direction.

if (metadata.RuntimeStatus == OrchestrationRuntimeStatus.Failed)
{
string errorMessage = metadata.FailureDetails?.ErrorMessage ?? "Workflow execution failed.";
throw new InvalidOperationException(errorMessage);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a lot of information we're throwing away here. Unless InvalidOperationException is a required part of the API contract, I suggest an exception that contains more information, such as TaskFailedException, which allows you to pass in that FailureDetails object in the line above this one. If you go with that approach, just use a dummy value 0 for taskId.

/// <remarks>
/// This method provides a workflow-focused configuration experience.
/// If you need to configure both agents and workflows, consider using
/// <see cref="DurableServiceCollectionExtensions.ConfigureDurableOptions"/> instead.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a similar actors-only API for parity? Can both ConfigureDurableWorkflows and ConfigureDurableOptions be used together?

/// <param name="executorName">The executor name to look up.</param>
/// <param name="registration">When this method returns, contains the registration if found; otherwise, null.</param>
/// <returns><see langword="true"/> if the executor was found; otherwise, <see langword="false"/>.</returns>
internal bool TryGetExecutor(string executorName, out ExecutorRegistration? registration)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use [NotNullWhen(true)] on the registration parameter for improved nullability checks.

/// <summary>
/// Represents a running instance of a workflow.
/// </summary>
public interface IWorkflowRun
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the core workflow MAF library not have an abstraction for workflow runs?

/// <param name="IsAgenticExecutor">Indicates whether this executor is an agentic executor.</param>
/// <param name="RequestPort">The request port if this executor is a request port executor; otherwise, null.</param>
/// <param name="SubWorkflow">The sub-workflow if this executor is a sub-workflow executor; otherwise, null.</param>
internal sealed record WorkflowExecutorInfo(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't expect to find a class like this in a file named WorkflowAnalyzer.cs. Should it go into it's own file?

/// <summary>
/// Extension methods for configuring durable agents and workflows with dependency injection.
/// </summary>
public static class DurableServiceCollectionExtensions
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need both this and ServiceCollectionExtensions.cs or should we have just one?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation .NET workflows Related to Workflows in agent-framework

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants