Skip to content

.NET: [Feature Branch] Adding support for events & shared state in durable workflows#4020

Open
kshyju wants to merge 8 commits intofeat/durable_taskfrom
shkr/feat_dt_streaming
Open

.NET: [Feature Branch] Adding support for events & shared state in durable workflows#4020
kshyju wants to merge 8 commits intofeat/durable_taskfrom
shkr/feat_dt_streaming

Conversation

@kshyju
Copy link
Contributor

@kshyju kshyju commented Feb 17, 2026

Motivation and Context

This PR adds workflow events and shared state support for durable workflows, building on the foundational workflow APIs introduced in #3648 and the Azure Functions hosting support added in #3935. While previous PRs established the core execution model, this PR enables real-time observability and cross-executor data sharing — two capabilities essential for production workflow scenarios.

Note: This PR targets the feat/durable_task feature branch.

Key scenarios enabled:

  • Emitting custom domain events from executors and streaming them to callers in real-time
  • Sharing state across executors without passing data through the message chain (as return value)
  • Partitioning state into isolated scopes and clearing them when no longer needed
  • Early workflow termination.
  • Observing workflow progress via streaming (executor invocations, completions, outputs, terminal events)

Description

This PR introduces event streaming and shared state infrastructure for durable workflows, along with new public APIs and supporting types.

New Public APIs

API Description
IWorkflowClient.StreamAsync<TInput> Starts a workflow and returns an IStreamingWorkflowRun for real-time event observation.
IStreamingWorkflowRun Interface with RunId and WatchStreamAsync() that yields WorkflowEvent instances as they occur.
DurableWorkflowCompletedEvent Terminal event emitted when the workflow completes successfully. Contains the final result.
DurableWorkflowFailedEvent Terminal event emitted when the workflow fails. Contains the error message.
DurableHaltRequestedEvent Event indicating early workflow halt (e.g., validation failure via RequestHaltAsync).
// Start a workflow with streaming
IStreamingWorkflowRun run = await workflowClient.StreamAsync(workflow, "ORDER-123");

// Observe events in real-time
await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
    switch (evt)
    {
        case WorkflowOutputEvent e:
            Console.WriteLine($"[Output] {e.SourceId}: {e.Data}");
            break;
        case DurableWorkflowCompletedEvent e:
            Console.WriteLine($"Completed: {e.Result}");
            break;
    }
}

Note: All other new types added in this PR (DurableStreamingWorkflowRun, DurableWorkflowCustomStatus, DurableWorkflowResult, TypedPayload, DurableSerialization, DurableRunStatus) are internal and not part of the public API surface.

Shared State APIs (via IWorkflowContext)

Shared state is implemented through the existing IWorkflowContext interface, which executors already receive in HandleAsync. No new public types are needed — the following methods are now functional in the durable context:

Method Description
QueueStateUpdateAsync Write a value to shared state for downstream executors
ReadStateAsync<T> Read a value written by a previous executor
ReadOrInitStateAsync<T> Read or lazily initialize a state value
QueueClearScopeAsync Clear all state entries under a scope
RequestHaltAsync Stop the workflow early (no downstream executors run)
AddEventAsync Emit a custom domain event observable via streaming
YieldOutputAsync Emit an intermediate output as a WorkflowOutputEvent
// In ValidateOrder executor: write tax rate to shared state
await context.QueueStateUpdateAsync("taxRate", 0.085m);

// In ProcessPayment executor: read tax rate from shared state
decimal taxRate = await context.ReadOrInitStateAsync("taxRate", () => 0.0m);
decimal total = orderAmount + (orderAmount * taxRate);

// Use custom scopes to partition state
await context.QueueStateUpdateAsync("carrier", "Contoso Express", scopeName: "shipping");

// Clear all entries under a scope when no longer needed
await context.QueueClearScopeAsync("shipping");

Internal Infrastructure Changes

Component Description
DurableActivityContext Full implementation of IWorkflowContext — state read/write, events, halt requests, and scoped state management
DurableWorkflowRunner Orchestration carries shared state across supersteps, merges state updates from activities, publishes accumulated events via custom status, and handles halt requests
DurableStreamingWorkflowRun Polls orchestration custom status for new events with configurable polling interval (default: 100ms)
DurableActivityExecutor Passes shared state to activity context and returns state updates, events, and halt status in activity output
DurableExecutorDispatcher Updated to pass shared state through to activity execution
DurableWorkflowJsonContext Updated with new types for AOT-compatible serialization

How Event Streaming Works

The orchestration accumulates events from activity outputs and publishes them via SetCustomStatus after each superstep. DurableStreamingWorkflowRun polls the orchestration metadata at a configurable interval, deserializes new events, and yields them through WatchStreamAsync. On completion, events are also stored in the orchestration output as a fallback (since SerializedCustomStatus is cleared by the framework on completion).

How Shared State Works

Shared state is a Dictionary<string, string> maintained by the orchestration. Before each activity dispatch, the current state snapshot is passed to the activity. Activities collect state updates locally and return them as part of their output. The orchestration merges updates after each superstep, making them available to the next round of executors. State keys can be partitioned into scopes (e.g., scopeName: "shipping") so that different parts of a workflow can manage their state independently.

Validation/Testing

Samples Added:

Sample Description
05_WorkflowEvents Demonstrates custom event emission (AddEventAsync) and real-time streaming (StreamAsync/WatchStreamAsync) with an order cancellation workflow. No Azure OpenAI required.
07_WorkflowSharedState Demonstrates shared state across executors (tax rate, audit trail, scoped shipping data) in an order processing pipeline with QueueStateUpdateAsync, ReadStateAsync, ReadOrInitStateAsync, scoped writes, and QueueClearScopeAsync. No Azure OpenAI required.

Integration Tests: Integration tests have been added (WorkflowConsoleAppSamplesValidation.cs) to validate both samples — WorkflowEventsSampleValidationAsync and WorkflowSharedStateSampleValidationAsync — ensuring end-to-end functionality works as expected.

Both samples have been manually verified against the local DTS emulator.

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 labels Feb 17, 2026
@kshyju kshyju requested a review from cgillum February 17, 2026 22:51
@kshyju kshyju added the azure-functions Issues and PRs related to Azure Functions label Feb 17, 2026
@kshyju kshyju requested a review from Copilot February 17, 2026 22:56
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

Adds durable-workflow event streaming and shared state support to the .NET DurableTask workflow runtime, enabling real-time observability (via StreamAsync/WatchStreamAsync) and cross-executor state without passing data through the message chain.

Changes:

  • Introduces IWorkflowClient.StreamAsync<TInput> and IStreamingWorkflowRun for live event streaming.
  • Implements shared-state + event/halt plumbing through durable activity inputs/outputs and orchestration custom status.
  • Adds two console samples (events + shared state) and integration tests validating end-to-end behavior.

Reviewed changes

Copilot reviewed 32 out of 32 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
dotnet/tests/Microsoft.Agents.AI.DurableTask.IntegrationTests/WorkflowConsoleAppSamplesValidation.cs Adds integration validations for new samples
dotnet/src/Microsoft.Agents.AI.Hosting.AzureFunctions/BuiltInFunctions.cs Uses shared HTTP error-response helper
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/TypedPayload.cs Adds typed payload wrapper for events/messages
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/SentMessageInfo.cs Removes old message wrapper type
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/IWorkflowClient.cs Adds StreamAsync<TInput> API
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/IStreamingWorkflowRun.cs New public streaming-run interface
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableWorkflowRunner.cs Orchestrator: shared state + event accumulation + halt
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableWorkflowRun.cs Uses streaming result extraction logic
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableWorkflowResult.cs Output wrapper including events + result
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableWorkflowJsonContext.cs Updates STJ source-gen registrations
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableWorkflowFailedEvent.cs Public terminal failure event
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableWorkflowCustomStatus.cs Custom-status model for streaming
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableWorkflowCompletedEvent.cs Public terminal completion event
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableWorkflowClient.cs Implements StreamAsync scheduling
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableStreamingWorkflowRun.cs Poll-based streaming implementation
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableSerialization.cs Shared JSON options for runtime types
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableRunStatus.cs Adds durable run status enum
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableHaltRequestedEvent.cs Public halt-request event
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableExecutorDispatcher.cs Passes shared state into activities
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableActivityOutput.cs Activity output now includes events/state/halt
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableActivityExecutor.cs Serializes events + state updates in output
dotnet/src/Microsoft.Agents.AI.DurableTask/Workflows/DurableActivityContext.cs Implements shared-state + event/halt APIs
dotnet/src/Microsoft.Agents.AI.DurableTask/ServiceCollectionExtensions.cs Refactors binding classification helper
dotnet/samples/Durable/Workflow/ConsoleApps/07_WorkflowSharedState/README.md Documents shared-state sample behavior
dotnet/samples/Durable/Workflow/ConsoleApps/07_WorkflowSharedState/Program.cs Shared-state streaming sample entrypoint
dotnet/samples/Durable/Workflow/ConsoleApps/07_WorkflowSharedState/Executors.cs Sample executors using shared state + halt
dotnet/samples/Durable/Workflow/ConsoleApps/07_WorkflowSharedState/07_WorkflowSharedState.csproj Adds new shared-state sample project
dotnet/samples/Durable/Workflow/ConsoleApps/05_WorkflowEvents/README.md Documents events + streaming usage
dotnet/samples/Durable/Workflow/ConsoleApps/05_WorkflowEvents/Program.cs Events streaming sample entrypoint
dotnet/samples/Durable/Workflow/ConsoleApps/05_WorkflowEvents/Executors.cs Sample executors emitting custom events
dotnet/samples/Durable/Workflow/ConsoleApps/05_WorkflowEvents/05_WorkflowEvents.csproj Adds new workflow-events sample project
dotnet/agent-framework-dotnet.slnx Includes new samples in solution

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

Copilot reviewed 34 out of 34 changed files in this pull request and generated 3 comments.

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

Copilot reviewed 34 out of 34 changed files in this pull request and generated 1 comment.

The integration test asserts that WorkflowOutputEvent is found in the
stream, but the sample executors only used AddEventAsync for custom
events and never called YieldOutputAsync. Since WorkflowOutputEvent is
only emitted via explicit YieldOutputAsync calls, the assertion would
fail. Added YieldOutputAsync to each executor to match the test
expectation and demonstrate the API in the sample.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Contributor

Copilot AI commented Feb 18, 2026

@kshyju I've opened a new pull request, #4026, to work on those changes. Once the pull request is ready, I'll request review from you.

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.

I haven't gotten through the full PR yet, but here's some initial feedback/questions. Mostly very minor things so far.

Console.WriteLine($" Read from shared state: shippingTier = {shippingTier}");

// Write shipping details under a custom "shipping" scope.
// Scoped keys are isolated from the default namespace, so "carrier" here
Copy link
Member

Choose a reason for hiding this comment

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

Just my ignorance: What is a "default namespace" and what is a "scoped key"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That comment shouldn't be here. It was more of an implementation detail (how the keys are stored behind the scene when QueueStateUpdateAsync method is called). I updated the comments to remove it, so the comments are cleaner.

Console.WriteLine($" Completed: {e.Result}");
break;

case DurableWorkflowFailedEvent e:
Copy link
Member

Choose a reason for hiding this comment

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

Sorry if this is outside the scope of this PR, but IIRC there are some built-in MAF events for various lifecycle state transitions. Does MAF already have something for workflow completion/failed? I'm wondering why we need to have Durable-specific versions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good question. MAF core currently doesn't have a WorkflowCompletedEvent at all, so DurableWorkflowCompletedEvent fills that gap. For the failure case, MAF has WorkflowErrorEvent but it carries an Exception? object. In the durable streaming context (via WatchStreamAsync), we only have the error message string from the orchestration status. There's no Exception instance available to construct a WorkflowErrorEvent. That's why DurableWorkflowFailedEvent carries string ErrorMessage instead.
For halt, MAF has RequestHaltEvent but it's internal, so it can't be yielded to external stream consumers.

We are using existing MAF events which are available to use in other areas of durable workflow (ex: WorkflowOutputEvent , ExecutorInvokedEvent and ExecutorCompletedEvent)

All three durable specific evernts I created extend WorkflowEvent from MAF.

I have a GH issue opened in the repo to expose these events as public. Once they are(before GA), we can switch to them and remove our copy. #4063

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for the explanation. One thought I have is that we can do better than just string ErrorMessage and provide TaskFailureDetails FailureDetails from the Durable Task SDK so that the user gets the error type, message, stack trace, and inner-failure details.

Console.WriteLine(" Wrote to shared state: shipping:estimatedDays = 2");

// Verify we can read the audit entry from the previous step
AuditEntry? previousAudit = await context.ReadStateAsync<AuditEntry>("audit:validate", cancellationToken: cancellationToken);
Copy link
Member

Choose a reason for hiding this comment

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

More ignorance on my part: I noticed that the update API has a separate scope and key value, but it seems the read API only has a single input type containing both the scope and the key. Is this a MAF convention? I'm wondering why we wouldn't have a more type-safe API for this.

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 read and update APIs actually have the same signature. Both take (string key, string? scopeName = null, ...):

I think my previous sample code was not the easiest to follow. The audit keys used : in the key name (e.g., "audit:validate"), which made it look like a combined scope+key format. I've cleaned this up now.

  • Renamed audit keys to camelCase (auditValidate, auditEnrich, auditPayment)
  • Simplified the scoped write to a single key (carrier with scopeName: "shipping")
  • Added a scoped read in GenerateInvoice so the sample demonstrates the full write → read → clear lifecycle with scopes

| **ProcessPayment** | payment ref string | `taxRate` | `audit:payment` |
| **GenerateInvoice** | invoice string | `audit:validate`, `audit:enrich`, `audit:payment` | clears `shipping` scope |

> **Note:** `EnrichOrder` writes `carrier` and `estimatedDays` under the `"shipping"` scope using `scopeName: "shipping"`. Scoped keys are isolated from the default namespace, so a key like `"carrier"` in the `"shipping"` scope won't collide with a `"carrier"` key in the default scope.
Copy link
Member

Choose a reason for hiding this comment

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

nit: instead of **Note:** we should use the standard callout syntax - e.g.

> [!NOTE]
> blah blah blah

I usually have to instruct Copilot about this since it prefers the ad-hoc syntax that you have here.

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, Fixed.

public ValueTask RequestHaltAsync()
{
this.HaltRequested = true;
this.Events.Add(new DurableHaltRequestedEvent(this._executor.Id));
Copy link
Member

Choose a reason for hiding this comment

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

Hmm...so we have to emit our own lifecycle events for cases like the workflow halting?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is a gap today. I have a GH issue opened in the repo to expose these events as public. Once they are(before GA), we can switch to them and remove our copy. #4063


// Remove any pending updates in this scope (snapshot keys to allow removal during iteration)
string scopePrefix = GetScopePrefix(scopeName);
foreach (string key in this.StateUpdates.Keys.ToArray())
Copy link
Member

Choose a reason for hiding this comment

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

nit: I usually go with ToList() instead of ToArray() because ToArray() has a slower implementation last I checked.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh okay. Switched to that. It might be interesting to benchmark in the latest .net versions though.

/// Gets or sets the state updates (scope-prefixed key to value; null indicates deletion).
/// </summary>
public List<SentMessageInfo> SentMessages { get; set; } = [];
public Dictionary<string, string?> StateUpdates { get; set; } = [];
Copy link
Member

Choose a reason for hiding this comment

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

Do all these collection properties need to be settable?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed all properties from set to init. They only need to be settable during construction/deserialization, not afterward.

public enum DurableRunStatus
{
/// <summary>
/// The orchestration instance was not found.
Copy link
Member

Choose a reason for hiding this comment

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

It looks like this is a public API, so should we be using "workflow" instead of "orchestration" in these descriptions?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good. Updated the description.

/// <summary>
/// Represents the execution status of a durable workflow run.
/// </summary>
public enum DurableRunStatus
Copy link
Member

Choose a reason for hiding this comment

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

I'm also curious why we need to define this ourselves vs. use some workflow status enum defined in MAF?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

MAF's RunStatus tracks in-process run lifecycle (NotStarted, Idle, PendingRequests, Ended, Running)


The DurableRunStatus I added maps durable orchestration states (Completed, Failed, Terminated, Suspended, NotFound) which have no equivalent in MAF's RunStatus today. Only Running overlaps. Longer term, it might make sense to unify these at the MAF layer, but the current states don't map cleanly.

Copy link
Member

Choose a reason for hiding this comment

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

Makes sense. I wonder if we could/should expose both RunStatus and DurableRunStatus to the user so that existing MAF tooling can consume the MAF status and the durable status can be used to get more information?

@kshyju kshyju requested a review from cgillum February 19, 2026 05:12
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.

Got through the full PR! Added a few more comments.

/// Output payload from activity execution, containing the result and other metadata.
/// Output payload from activity execution, containing the result, state updates, and emitted events.
/// </summary>
internal sealed class DurableActivityOutput
Copy link
Member

Choose a reason for hiding this comment

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

Maybe call this a DurableExecutorOutput?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense. Renamed to DurableExecutorOutput.

/// <summary>
/// Represents the execution status of a durable workflow run.
/// </summary>
public enum DurableRunStatus
Copy link
Member

Choose a reason for hiding this comment

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

Makes sense. I wonder if we could/should expose both RunStatus and DurableRunStatus to the user so that existing MAF tooling can consume the MAF status and the durable status can be used to get more information?

if (metadata.SerializedCustomStatus is not null)
{
DurableWorkflowCustomStatus? customStatus = TryParseCustomStatus(metadata.SerializedCustomStatus);
if (customStatus is not null)
Copy link
Member

Choose a reason for hiding this comment

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

nit: it's more conventional to have a TryXXX method return a bool and put the result as an out parameter. That would also allow you to combine these two lines into one and remove the null check.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

}
else
{
yield return new DurableWorkflowCompletedEvent(metadata.SerializedOutput);
Copy link
Member

Choose a reason for hiding this comment

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

Is this ever expected? I think we should add a comment here about when this situation might occur and how we expect users to consume the raw serialized output.

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 added this as a defensive fallback. Ideally this line of code should not be executed as we expect the runner to always wraps output in DurableWorkflowResult. I changed this to return a more strict response , DurableWorkflowFailedEvent now with the failure details.

{
// The framework clears custom status on completion, so events may be in
// SerializedOutput as a DurableWorkflowResult wrapper.
DurableWorkflowResult? outputResult = TryParseWorkflowResult(metadata.SerializedOutput);
Copy link
Member

Choose a reason for hiding this comment

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

nit: same comment about TryXXX methods.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

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

Choose a reason for hiding this comment

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

Related to an earlier comment - ideally, we'd send back the full metadata.FailureDetails object.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, updated to include FailureDetails.

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.

It might be more helpful to throw a TaskFailedException here and pass the metadata.FailureDetails is a constructor argument.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Switched to TaskFailedException with FailureDetails

/// This interface defines the contract for streaming workflow runs in durable execution
/// environments. Implementations provide real-time access to workflow events.
/// </remarks>
public interface IStreamingWorkflowRun
Copy link
Member

Choose a reason for hiding this comment

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

I forget if I asked this already in the previous review, but why do we need our own interface types? Do the MAF ones not work for us?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

MAF's StreamingRun is a concrete class tied to in process execution.It holds an AsyncRunHandle and supports SendResponseAsync, TrySendMessageAsync, and CancelRunAsync, none of which apply to the durable (polling-based) execution model. The durable streaming run polls orchestration metadata externally and has its own lifecycle (e.g., GetStatusAsync returning DurableRunStatus, WaitForCompletionAsync). We defined IStreamingWorkflowRun as a minimal interface (RunId + WatchStreamAsync) that captures just the common streaming contract. If MAF core library introduces an abstraction over both in-process and durable streaming, we can switch to that.

// Assert
Assert.Equal(2, events.Count);
Assert.IsType<DurableHaltRequestedEvent>(events[0]);
Assert.IsType<DurableWorkflowCompletedEvent>(events[1]);
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 also evaluate the payloads for these events, or is that covered elsewhere?

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, updated tests to assert that as well.

@kshyju kshyju requested a review from cgillum February 20, 2026 05:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

azure-functions Issues and PRs related to Azure Functions documentation Improvements or additions to documentation .NET

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

Comments