Skip to content

.NET: Subworkflows bound with BindAsExecutor() are not invoked when parent workflow runs as agent via AsAgent() #2585

@berndku

Description

@berndku

Description

When a workflow contains a subworkflow that has been bound as an executor using BindAsExecutor(), and the parent workflow is then run as an agent using AsAgent() and RunAsync(), the subworkflow executor is never invoked. Messages are not passed to the subworkflow, and executors within the subworkflow never execute.

Expected Behavior

When running a workflow as an agent that contains a subworkflow bound as an executor:

  1. The parent workflow's entry executor should execute
  2. Messages should be passed to the subworkflow executor
  3. The subworkflow should execute its internal executors
  4. Results should flow back through the workflow graph

Actual Behavior

Only the parent workflow's executors execute. The subworkflow executor is completely skipped:

  1. ✅ Parent workflow's InputExecutor executes successfully
  2. ❌ Subworkflow executor is never invoked
  3. ❌ Executors inside the subworkflow never execute
  4. ❌ No messages flow through the subworkflow

Minimal Reproducible Example

class Program
{
    static async Task Main(string[] args)
    {
        // Setup logging
        using var loggerFactory = LoggerFactory.Create(builder =>
        {
            builder.AddConsole();
            builder.SetMinimumLevel(LogLevel.Debug);
        });
        var logger = loggerFactory.CreateLogger<Program>();

        logger.LogInformation("Starting workflow test with subworkflow as executor");

        // Create a simple chat client (using a dummy implementation for testing)
        IChatClient chatClient = new TestChatClient();

        // Create the main workflow that uses a subworkflow as executor
        var mainWorkflow = CreateMainWorkflow(chatClient, logger);

        // Run the workflow as an agent
        logger.LogInformation("Running workflow as agent...");
        
        var agent = mainWorkflow.AsAgent();
        
        var messages = new List<ChatMessage>
        {
            new ChatMessage(ChatRole.User, "Process this item")
        };

        logger.LogInformation("Invoking agent with message: '{Message}'", messages[0].Text);

        try
        {
            var response = await agent.RunAsync(messages);
            
            // Extract messages from response
            var messagesProperty = response.GetType().GetProperty("Messages");
            if (messagesProperty != null)
            {
                var responseMessages = messagesProperty.GetValue(response) as IEnumerable<ChatMessage>;
                if (responseMessages != null)
                {
                    foreach (var msg in responseMessages)
                    {
                        logger.LogInformation("Response message [{Role}]: {Text}", msg.Role, msg.Text);
                    }
                }
            }
            
            logger.LogInformation("Workflow completed successfully!");
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Error running workflow: {Message}", ex.Message);
        }

        logger.LogInformation("Test complete");
    }

    static Workflow CreateMainWorkflow(IChatClient chatClient, ILogger logger)
    {
        logger.LogInformation("Creating main workflow with subworkflow executor");

        // Create the subworkflow
        var subWorkflow = CreateSubWorkflow(chatClient, logger);
        
        // Bind the subworkflow as an executor
        var subWorkflowExecutor = subWorkflow.BindAsExecutor("SubWorkflowExecutor");
        
        logger.LogInformation("Subworkflow bound as executor with ID: {ExecutorId}", subWorkflowExecutor.Id);

        // Create a simple input executor
        var inputExecutor = new InputExecutor();

        // Build the main workflow
        var builder = new WorkflowBuilder(inputExecutor);
        
        // Connect input executor to subworkflow executor
        builder.AddEdge<List<ChatMessage>>(
            source: inputExecutor,
            target: subWorkflowExecutor
        );

        // Set output from subworkflow executor
        builder.WithOutputFrom(subWorkflowExecutor);

        var workflow = builder.WithName("MainWorkflow").Build();
        
        var dotString = workflow.ToDotString();
        logger.LogInformation("Main Workflow Graphviz:\n{DotString}", dotString);

        return workflow;
    }

    static Workflow CreateSubWorkflow(IChatClient chatClient, ILogger logger)
    {
        logger.LogInformation("Creating subworkflow");

        // Create a simple processing executor
        var processingExecutor = new ProcessingExecutor();

        var builder = new WorkflowBuilder(processingExecutor);
        builder.WithOutputFrom(processingExecutor);

        var workflow = builder.WithName("SubWorkflow").Build();
        
        var dotString = workflow.ToDotString();
        logger.LogInformation("SubWorkflow Graphviz:\n{DotString}", dotString);

        return workflow;
    }
}


// Simple test chat client implementation
class TestChatClient : IChatClient
{
    public ChatClientMetadata Metadata { get; } = new ChatClientMetadata("TestClient");

    public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
        IEnumerable<ChatMessage> messages,
        ChatOptions? options = null,
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        yield return new ChatResponseUpdate 
        { 
            Role = ChatRole.Assistant,
            Contents = [new TextContent("Test streaming response")]
        };
    }

    public async Task<ChatResponse> GetResponseAsync(
        IEnumerable<ChatMessage> chatMessages,
        ChatOptions? options = null,
        CancellationToken cancellationToken = default)
    {
        return new ChatResponse(
            new ChatMessage(ChatRole.Assistant, "Test response from chat client"));
    }

    public void Dispose()
    {
    }

    public object? GetService(Type serviceType, object? serviceKey = null)
    {
        return null;
    }

Metadata

Metadata

Assignees

Labels

.NETv1.0Features being tracked for the version 1.0 GAworkflowsRelated to Workflows in agent-framework

Type

Projects

Status

Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions