NET SDK: Nexus WorkflowRunOperationHandler bypasses Interceptors and lacks Header support

The Problem:
I am implementing a Nexus Service using the .NET SDK. In my WorkflowRunOperationHandler, I receive headers from the incoming Nexus request via WorkflowRunOperationContext.HandlerContext.Headers. I need to propagate specific headers to the Workflow being started by context.StartWorkflowAsync.

However, the WorkflowOptions passed to StartWorkflowAsync does not expose a Headers property.

[NexusOperationHandler]
    public IOperationHandler<IMyNexusService.WorkflowStartInput, IMyNexusService.WorkflowStartOutput>
        StartWorkflowAsync()
    {
        return WorkflowRunOperationHandler.FromHandleFactory((WorkflowRunOperationContext context,
             IMyNexusService.WorkflowStartInput input) =>
        {
            var payloadConverter = DataConverter.Default.PayloadConverter;
            // Get headers from execution context and convert to Temporal Payload format
            var headers = new Dictionary<string, Payload>();
            if (context.HandlerContext.Headers is not null && context.HandlerContext.Headers.Count > 0)
            {
                headers["__my_context_key"] = payloadConverter.ToPayload(context.HandlerContext.Headers);
            }

            return context.StartWorkflowAsync(
                (IMyNexusService wf) => wf.RunAsync(
                    new IMyNexusService.WorkflowStartInput(input.WorkflowId, input.Questions)),
                new WorkflowOptions
                {
                    Id = input.WorkflowId,
                    TaskQueue = "nexus-task-queue",

                });
        });
    }
}

I have attempted to use the standard Interceptor pattern, but I’ve found that none of the following interceptors are triggered when a Nexus handler initiates a workflow:

  1. INexusInboundInterceptor: This is getting invoked while invoking the nexus operation handler which is working fine, but is there anything for nexus outbound request, I was expecting Workflow start request will be handled by the TemporalClient outbound interceptor but it’s not working.

  2. IWorkflowOutboundInterceptor: Does not make any sense but was trying to figure out all the possibilities.

  3. ITemporalClientOutboundInterceptor : The StartWorkflowAsync call inside the Nexus WorkflowRunOperationContext seems to bypass the client outbound interceptor pipeline entirely.

Expectation:
My goal is to achieve seamless header propagation from the Nexus entry point to the Workflow execution. Specifically:

  1. I receive a header in the Nexus Operation Handler (via context.HandlerContext.Headers).

  2. I want to pass this header into the StartWorkflowAsync call.

  3. Crucially, I expect that when this workflow starts, my registered WorkflowInboundInterceptor should be triggered, allowing it to extract that header and set up the AsyncLocal state used by the rest of my workflow logic.

Thank you for your help in advance, and let me know if any additional details required to understand the context.

This is surprising. The internal code that starts the workflow leverages the same Temporal client configured on the worker using the same start-workflow call users use on the client, which means its interceptors should be called. Can you confirm that the client you are passing to the worker has the client interceptor configured on it?

We can also double check on our side and maybe alter samples-dotnet/src/ContextPropagation at main · temporalio/samples-dotnet · GitHub sample to include Nexus in addition to activities.

EDIT: Just saw, we already have a NexusContextPropagation sample at samples-dotnet/src/NexusContextPropagation at main · temporalio/samples-dotnet · GitHub .

@Chad_Retz Thanks for the quick reply.

I have implemented a custom interceptor approach following the standard patterns for the .NET SDK, specifically utilizing NexusOperationInboundInterceptor and TemporalClientOutboundInterceptor.

The NexusOperationInboundInterceptor triggers correctly just before the Nexus service operation handler is invoked, and I can verify that all custom headers are available at that stage. However, a critical issue occurs during the “hand-off” from the Nexus handler to the Workflow. When context.StartWorkflowAsync() is called, the request is not captured by the TemporalClientOutboundInterceptor. It appears the internal client used by the Nexus context to initiate workflows completely bypasses the standard outbound interceptor pipeline.

Consequently, I cannot programmatically inject the headers received in the Nexus handler into the target workflow’s metadata. My goal is to ensure the outbound interceptor is hit so that the subsequent WorkflowInboundInterceptor on the receiving side can extract these headers.


public class TemporalClientOutboundInterceptor(ClientOutboundInterceptor next, 
    IContextPropagationHelper contextPropagationHelper) :
    ClientOutboundInterceptor(next)
{
    
    public override Task<WorkflowHandle<TWorkflow, TResult>> StartWorkflowAsync<TWorkflow, TResult>(
        StartWorkflowInput input)
    {
      // Ignore this as it's our helper class which is doing pretty much same which you are doing using the `AsyncLocal` context
        var headers = contextPropagationHelper.GetTemporalHeadersFromContext(input.Headers);
        
        var result = base.StartWorkflowAsync<TWorkflow, TResult>(
            input with { Headers =  headers});
        
        return result;
    }
}

My Startup.cs follows the standard .NET dependency injection pattern for Temporal, where I register the client via AddTemporalClient and the worker via AddHostedTemporalWorker.

According to Temporal documentation, the Hosted Worker should utilize the same ITemporalClient instance registered in the DI container. However, despite having my TemporalClientOutboundInterceptor registered on that client, it is never invoked for workflows initiated within the Nexus operation context. This behavior is inconsistent with standard workflow starts, where the interceptor functions as expected.

services.AddTemporalClient()
            .Configure<MyTemporalInboundInterceptor>((opt, dep) =>
        {
            opt.TargetHost = temporalHost;
            opt.Namespace = temporalNamespace;
            opt.Interceptors = [dep];
        });

        services.AddHostedTemporalWorker(
                clientTargetHost: temporalHost,
                clientNamespace: temporalNamespace,
                taskQueue: taskQueue)
            .ConfigureOptions(options =>
            {
                options.MaxConcurrentNexusTaskPolls = 5;
                options.MaxConcurrentNexusTasks = 100;
            })
            .AddSingletonNexusService<MyNexusServiceHandler>()
            .AddWorkflow<CallerWorkflow>()
            .AddInterceptor(services); // This is my extension method which is also adding the interceptor on worker.

Summary: While the NexusOperationInboundInterceptor successfully wraps the execution of the Nexus service handler, it does not solve the issue regarding the workflow execution triggered from within that handler.

I have registered my outbound interceptor on the same ITemporalClient that the worker uses for all other operations. When using this client to start a standard workflow, the interceptor triggers perfectly. However, when the same worker starts a workflow via the Nexus Operation Handler, the interceptor is ignored. This suggests that the WorkflowRunOperationContext is either using a different internal client instance or the .NET SDK does not currently support outbound interceptors for requests originating from a Nexus context.

If you think I’m doing something wrong then let me know, it’s kind of blocker for us to move forward with the nexus service implementation.

Can you confirm this sample works for you? It demonstrates a header being passed all the way through including the client outbound interceptor (granted it’s split across two interceptors to reuse code from another sample). You can also look at this closed PR I wrote before I saw that sample that adjusts the existing context propagation sample and confirms it also properly passes headers. Can you see if that sample works for you as well?

If those samples and their tests work for you and therefore are properly passing headers through Nexus and the client outbound interceptors, can you adjust them to replicate what you’re seeing?

Thank you @Chad_Retz for your response.

Provided nexus propagation sample is using the TemporalWorker class to run the worker and to validate my theory I replaced my AddHostedTemporalWorker with this class and my solution also started working, there is no change in the interceptor, so just wondering as per documentation AddHostedTemporalWorker is using the same TemporalClient instance in the worker which we have registered using the AddTemporalClient extension method but in case of nexus operation it’s not working as expected and using different client for the operations.

So can you suggest should we be using TemporalWorker or AddHostedTemporalWorker for registering the worker, which one is the recommended approach, also is there any nexus example which is using AddHostedTemporalWorker extension method as most of our workers are using the hosted services only.

Actually, while it sounds strange, it is actually dependent upon which AddHostedTemporalWorker overload you are using. Per this doc:

One overload of AddHostedTemporalWorker, used in the quick start sample above, accepts the client target host, the client namespace, and the worker task queue. This form will connect to a client for the worker. The other overload of AddHostedTemporalWorker only accepts the worker task queue. In the latter, an ITemporalClient can either be set on the services container and therefore reused across workers, or the resulting builder can have client options set.

Are you using the overload that provides the target host and namespace which implicitly creates a client with that information, or are you using the form that only accepts the task queue that will use the existing client via DI?

Ahh that make sense, I’m using the overload which is accepting the target host and namespace and that was causing issue, just switched to other overload and it’s resolved now.

Thank you so much for the help!

1 Like