microsoft / microsoft/agent-framework

.NET Workflows: Feature Request: Pipeline Behavior Extension Points for Cross-Cutting Concerns

Open
#3,960 2 comments 1 reaction 2 assignees Claimed by @peibekwe View on GitHub
.NET workflows
Dominant language
Python
Stars
13.6k
Forks
2.3k
Avg merge
2d 45m
Merged PRs (30d)
358

Description

# Feature Request: Pipeline Behavior Extension Points for Workflow Framework

## Problem Statement

The `Microsoft.Agents.AI.Workflows` framework currently lacks extensibility hooks for cross-cutting concerns at the workflow and executor execution levels. While an event system exists (`ExecutorInvokedEvent`, `SuperStepStartedEvent`, etc.), events are fire-and-forget and **cannot**:

- Intercept or modify execution flow
- Short-circuit execution based on validation
- Measure execution time and inject telemetry
- Transform inputs/outputs
- Implement retry or caching logic
- Add authentication/authorization checks

This limitation forces developers to either:
1. Fork and modify framework internals
2. Implement workarounds with reflection
3. Duplicate cross-cutting logic across every executor

## Proposed Solution

Add **pipeline behavior extension points** that allow developers to inject custom logic before and after:
- **Workflow start/end** - When workflow execution begins and completes
- **Executor step start/end** - When individual executors process messages

This enables a middleware-style pattern (similar to ASP.NET Core or MediatR's `IPipelineBehavior`) where multiple behaviors can be chained together.

## API Design

### 1. Workflow-Level Behaviors

```csharp
public interface IWorkflowBehavior
{
ValueTask HandleAsync(
WorkflowBehaviorContext context,
WorkflowBehaviorDelegate next,
CancellationToken cancellationToken);
}

public class WorkflowBehaviorContext
{
public string WorkflowName { get; init; }
public string? WorkflowDescription { get; init; }
public string RunId { get; init; }
public string StartExecutorId { get; init; }
public WorkflowStage Stage { get; init; } // Starting or Ending
public IDictionary? Properties { get; init; }
}
```

### 2. Executor-Level Behaviors

```csharp
public interface IExecutorBehavior
{
ValueTask HandleAsync(
ExecutorBehaviorContext context,
ExecutorBehaviorDelegate next,
CancellationToken cancellationToken);
}

public class ExecutorBehaviorContext
{
public string ExecutorId { get; init; }
public Type ExecutorType { get; init; }
public object Message { get; init; }
public Type MessageType { get; init; }
public string RunId { get; init; }
public ExecutorStage Stage { get; init; } // PreExecution or PostExecution
public IWorkflowContext WorkflowContext { get; init; }
public TraceContext TraceContext { get; init; }
public IDictionary? Properties { get; init; }
}
```

### 3. Registration API

```csharp
var workflow = new WorkflowBuilder(startExecutor)
.WithBehaviors(options =>
{
options.AddExecutorBehavior(new LoggingBehavior(logger));
options.AddExecutorBehavior(new ValidationBehavior());
options.AddWorkflowBehavior(new TelemetryBehavior());
})
.AddEdge(executor1, executor2)
.Build();
```

## Use Cases

### 1. Performance Monitoring
```csharp
public class PerformanceMonitoringBehavior : IExecutorBehavior
{
public async ValueTask HandleAsync(
ExecutorBehaviorContext context,
ExecutorBehaviorDelegate next,
CancellationToken cancellationToken)
{
var stopwatch = Stopwatch.StartNew();
try
{
return await next(cancellationToken);
}
finally
{
_metrics.RecordExecutorDuration(
context.ExecutorId,
stopwatch.ElapsedMilliseconds);
}
}
}
```

### 2. Input Validation
```csharp
public class ValidationBehavior : IExecutorBehavior
{
public async ValueTask HandleAsync(
ExecutorBehaviorContext context,
ExecutorBehaviorDelegate next,
CancellationToken cancellationToken)
{
if (context.Stage == ExecutorStage.PreExecution)
{
ValidateMessage(context.Message);
}
return await next(cancellationToken);
}
}
```

### 3. Distributed Tracing
```csharp
public class TracingBehavior : IWorkflowBehavior
{
public async ValueTask HandleAsync(
WorkflowBehaviorContext context,
WorkflowBehaviorDelegate next,
CancellationToken cancellationToken)
{
using var span = _tracer.StartSpan(
$"workflow.{context.Stage}",
new { workflowName = context.WorkflowName, runId = context.RunId });

return await next(cancellationToken);
}
}
```

### 4. Caching
```csharp
public class CachingBehavior : IExecutorBehavior
{
public async ValueTask HandleAsync(
ExecutorBehaviorContext context,
ExecutorBehaviorDelegate next,
CancellationToken cancellationToken)
{
var cacheKey = GetCacheKey(context);

if (_cache.TryGetValue(cacheKey, out var cached))
return cached;

var result = await next(cancellationToken);
_cache.Set(cacheKey, result);
return result;
}
}
```

## Benefits

### 1. **Separation of Concerns**
- Keep business logic in executors
- Extract cross-cutting concerns to behaviors
- Single Responsibility Principle

### 2. **Composability**
- Chain multiple behaviors together
- Mix and match behaviors per workflow
- Reuse behaviors across workflows

### 3. **Testability**
- Test behaviors independently
- Mock behavior pipelines in tests
- Verify behavior ordering

### 4. **Zero Breaking Changes**
- All changes are additive
- `BehaviorPipeline` property is nullable
- `WithBehaviors()` is optional
- Existing workflows work unchanged
- Fast path when no behaviors registered (zero overhead)

### 5. **Familiar Pattern**
- ASP.NET Core middleware: `app.Use()`
- MediatR: `IPipelineBehavior`
- Developers already know this pattern

## Implementation Notes

### Performance
- **Zero overhead** when no behaviors are registered (fast path check)
- **Minimal overhead** when behaviors are registered but do nothing
- Pipeline built once at workflow creation, not per execution

### Error Handling
```csharp
public class BehaviorExecutionException : Exception
{
public string BehaviorType { get; }
public string Stage { get; }

// Wraps exceptions thrown by behaviors for better diagnostics
}
```

### Execution Order
- Behaviors execute in registration order (outer to inner)
- Last registered behavior executes closest to core execution
- Similar to ASP.NET Core middleware ordering

## Comparison with Existing Patterns

| Feature | Events | Agent Middleware | **Pipeline Behaviors** |
|---------|--------|------------------|------------------------|
| Intercept execution | ❌ | ✅ | ✅ |
| Modify flow | ❌ | ✅ | ✅ |
| Workflow lifecycle | ❌ | ❌ | ✅ |
| Executor lifecycle | ❌ | ❌ | ✅ |
| Scope | Fire-and-forget | Per agent run | Per workflow/executor |
| Chain behaviors | ❌ | ✅ | ✅ |

**Note:** This complements agent middleware (`AgentRunOptions.RunMiddleware`) by providing workflow/executor-specific extension points, rather than replacing it.

## Related Issues

- #3951 - Bug about missing `AgentRunOptions.RunMiddleware` (agent-level middleware)
- #3823 - Bug about session in AG-UI middleware
- #3872 - Feature request for custom WorkflowEvent emission

This proposal addresses a **different extensibility layer** (workflow/executor behaviors) than existing agent middleware.

## Questions for Maintainers

1. **Naming**: Is "Pipeline Behavior" the right term, or would "Workflow Middleware" be clearer?
2. **Scope**: Should behaviors have access to full `IWorkflowContext`, or a limited subset?
3. **Async**: Should we support synchronous behaviors (`Func` vs `ValueTask`)?
4. **Properties**: Should `Properties` dictionary be mutable for passing data between behaviors?
5. **DI Integration**: Should behaviors support constructor injection from `IServiceProvider`?

## Proposed Implementation Plan

1. ✅ Create behavior interfaces (`IWorkflowBehavior`, `IExecutorBehavior`)
2. ✅ Create pipeline execution engine (`BehaviorPipeline`)
3. ✅ Add registration mechanism (`WorkflowBehaviorOptions`, `WithBehaviors()`)
4. ✅ Integrate into `Workflow` and `Executor` execution paths
5. ✅ Add unit tests for behavior execution and ordering
6. ⏳ Add integration tests with sample behaviors
7. ✅ Add XML documentation and code samples
8. ⏳ Update workflow documentation

## Additional Context

This feature enables enterprise-grade workflow observability and control without framework modifications. It follows established .NET patterns and maintains backward compatibility.

**Ready to implement**: POC is available and follows CONTRIBUTING.md guidelines.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.