AddTraxJobRunner

Registers the minimal services needed to run JobRunnerTrain without the full scheduler. Used on the remote receiver side, the process that actually executes trains dispatched by a scheduler via UseRemoteWorkers(). Also documents UseTraxRunEndpoint() for handling synchronous run requests from UseRemoteRun().

Signatures

public static IServiceCollection AddTraxJobRunner(
    this IServiceCollection services,
    Action<TraxJobRunnerOptions> configure
)
 
public static IServiceCollection AddTraxJobRunner(
    this IServiceCollection services
)
public static RouteHandlerBuilder UseTraxJobRunner(
    this IEndpointRouteBuilder app,
    string route = "/trax/execute"
)
public static RouteHandlerBuilder UseTraxRunEndpoint(
    this IEndpointRouteBuilder app,
    string route = "/trax/run"
)

Parameters

AddTraxJobRunner

ParameterTypeDescription
configureAction<TraxJobRunnerOptions>Sets the runner's authorization posture. Every runner entry point refuses to start without one

The overload without configure registers the execution pipeline with no posture. It suits a host that needs ITraxScheduler without mapping a runner endpoint; mapping one afterwards fails at startup.

TraxJobRunnerOptions

Property / methodTypeDefaultDescription
SigningKeybyte[]?nullThe key shared with the scheduler's SigningKey, at least 32 bytes. When set, every request must carry a valid Trax-Signature
AuthorizationPolicystring?nullAn ASP.NET authorization policy applied to UseTraxJobRunner and UseTraxRunEndpoint. Must admit only the scheduler. Not applicable to SQS or Lambda
MaxRequestBodyByteslong8 MiB (DefaultMaxRequestBodyBytes)The largest request body the HTTP entry points (UseTraxJobRunner, UseTraxRunEndpoint, and TraxLambdaFunction's local routes) read; a larger one gets 413. The default holds a train input at the mediator's default stored-input cap; raise it together with the scheduler's MaxInputJsonBytes. Must be positive
MaxClockSkewTimeSpan5 minutesHow far a signed request's timestamp may be from the runner's clock, and how long a nonce is remembered
AllowUnsignedRequests()methodAccepts requests with no signature. Each entry point logs a warning when it starts
UseInMemoryNonceStore()methodKeeps accepted nonces in this process instead of the database. Only for a runner that runs as one instance: each instance keeps its own

See Authorization Posture for how the three combine and what each transport checks.

UseTraxJobRunner

ParameterTypeDefaultDescription
routestring"/trax/execute"The route to map the POST endpoint for queued jobs

UseTraxRunEndpoint

ParameterTypeDefaultDescription
routestring"/trax/run"The route to map the POST endpoint for synchronous run requests

Examples

Minimal Remote Executor (Queue Only)

var builder = WebApplication.CreateBuilder(args);
 
builder.Services.AddTrax(trax => trax
    .AddEffects(effects => effects
        .UsePostgres(connectionString)
    )
    .AddMediator(typeof(MyTrain).Assembly)
);
builder.Services.AddTraxJobRunner(runner =>
    runner.SigningKey = Convert.FromBase64String(builder.Configuration["Trax:RunnerSigningKey"]!)
);
 
var app = builder.Build();
app.UseTraxJobRunner("/trax/execute");
app.Run();

Remote Executor (Queue + Run)

var builder = WebApplication.CreateBuilder(args);
 
builder.Services.AddTrax(trax => trax
    .AddEffects(effects => effects
        .UsePostgres(connectionString)
    )
    .AddMediator(typeof(MyTrain).Assembly)
);
builder.Services.AddTraxJobRunner(runner =>
    runner.SigningKey = Convert.FromBase64String(builder.Configuration["Trax:RunnerSigningKey"]!)
);
 
var app = builder.Build();
app.UseTraxJobRunner("/trax/execute");  // queue path
app.UseTraxRunEndpoint("/trax/run");    // synchronous run path
app.Run();

Both endpoints need AddTraxJobRunner(runner => ...): they resolve ITraxRequestHandler from it, and read the posture from its options while mapping.

With an Authorization Policy

Name the policy in the runner options. The runner applies it to both endpoints and checks at startup that a posture exists, which it cannot do for a .RequireAuthorization() chained on afterwards. The policy must admit only the scheduler, because the runner runs what it is sent without per-train authorization:

var builder = WebApplication.CreateBuilder(args);
 
builder.Services.AddTrax(trax => trax
    .AddEffects(effects => effects
        .UsePostgres(connectionString)
    )
    .AddMediator(typeof(MyTrain).Assembly)
);
builder.Services.AddAuthentication(/* your scheme */);
builder.Services.AddAuthorization(o =>
    o.AddPolicy("trax-scheduler", p => p.RequireClaim("scope", "trax:dispatch")));
builder.Services.AddTraxJobRunner(runner => runner.AuthorizationPolicy = "trax-scheduler");
 
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.UseTraxJobRunner("/trax/execute");
app.Run();

AWS Lambda (via TraxLambdaFunction base class)

For Lambda deployments, use the Trax.Runner.Lambda package which provides a base class that handles service provider lifecycle, request routing, and cancellation:

using Amazon.Lambda.Core;
using Amazon.Lambda.Serialization.SystemTextJson;
using Trax.Runner.Lambda;
 
[assembly: LambdaSerializer(typeof(DefaultLambdaJsonSerializer))]
 
public class Function : TraxLambdaFunction
{
    protected override void ConfigureServices(IServiceCollection services, IConfiguration configuration)
    {
        var connString = configuration.GetConnectionString("TraxDatabase")!;
 
        services.AddTrax(trax => trax
            .AddEffects(effects => effects.UsePostgres(connString))
            .AddMediator(typeof(MyTrain).Assembly));
    }
 
    protected override void ConfigureRunner(TraxJobRunnerOptions runner, IConfiguration configuration) =>
        runner.SigningKey = Convert.FromBase64String(configuration["Trax:RunnerSigningKey"]!);
}

See the TraxLambdaFunction API reference for details.

What It Registers

AddTraxJobRunner

Registers the minimum set of services to run JobRunnerTrain:

ServiceLifetimeDescription
SchedulerConfigurationSingletonEmpty configuration (no manifests, no polling)
ICancellationRegistry → CancellationRegistrySingletonProcess-local cancellation tracking
ITraxScheduler → TraxSchedulerScopedRuntime scheduler interface
IDormantDependentContext → DormantDependentContextScopedDependent train context
IJobRunnerTrain → JobRunnerTrainScopedThe train execution pipeline
ITraxRequestHandler → TraxRequestHandlerScopedThe execution step behind the runner's entry points: reads a request's input, resolves the train and runs it inside a trusted scope. It checks no posture and verifies no signature, so call it only from an entry point that has already verified the request; the entry points Trax ships do. Hidden from IntelliSense
TraxJobRunnerOptionsSingletonThe posture passed to configure
INonceStoreSingletonWhere a signing runner records accepted nonces: the runner_nonce table through IDataContext.RunnerNonces, shared by every instance on the database, or memory after UseInMemoryNonceStore(). Registered with TryAdd, so a host's own store replaces it. With a SigningKey and neither a relational data provider nor UseInMemoryNonceStore(), resolving it fails at startup
RunnerRequestVerifierSingletonChecks the posture at startup and each request's signature and freshness
A decision-recording startup checkHosted serviceRefuses to start the host, naming the trains, when trains that ask a decider run on a host without AddDecisionRecording(). Runs as the host starts, before any worker claims work. Added once however many of AddScheduler, AddTraxJobRunner and AddTraxWorker a host calls. See A host that does not record

Not registered: ManifestManager, JobDispatcher, polling services, startup service, LocalWorkerService. This process only runs trains; it doesn't schedule or dispatch them.

The SchedulerConfiguration, ICancellationRegistry, ITraxScheduler and IDormantDependentContext registrations use TryAdd, and AddScheduler replaces the configuration and cancellation registry. A scheduler host that also maps a runner endpoint for another scheduler therefore keeps the configuration it built, whichever of AddTraxJobRunner and AddScheduler it calls first.

UseTraxJobRunner

Throws while mapping when the runner options have no posture. Otherwise maps a POST endpoint at the specified route that:

  1. Returns 415 for a body that is not JSON. With a SigningKey, returns 401 for a Trax-Signature header that is missing, malformed or stale before any of the body is read, 413 for a body over MaxRequestBodyBytes, and 401 for a signature the body does not match or a replayed nonce. Refusals are logged as a warning at most once a minute, with a count of those since the last warning, and at Debug otherwise
  2. Reads a RemoteJobRequest from the request body. A body that repeats a property (in any case), does not parse, or is null gets 400 Bad Request. The host's global JSON options are not used for this
  3. Deserializes the input (if present) into the registered train input type whose fully-qualified name the request gives. A name no registered train takes is refused
  4. Calls IJobRunnerTrain.Run(new RunJobRequest(metadataId, input)), which refuses a metadata row that is not Pending or belongs to a different train than the input's
  5. Returns 200 OK with a RemoteJobResponse containing the metadata ID on success
  6. On error: logs the exception, then returns 200 OK with RemoteJobResponse.IsError = true, the ExceptionType, and the message of a TrainException (any other exception gets a fixed message). No stack trace is returned

With an AuthorizationPolicy, the endpoint requires it.

UseTraxRunEndpoint

Enforces the same posture as UseTraxJobRunner, and throws while mapping without one. Maps a POST endpoint at the specified route that handles synchronous run requests from UseRemoteRun():

  1. Reads a RemoteRunRequest from the request body (contains train name and input JSON), after the same 415, 401 and 413 checks, and with the same 400 refusals
  2. Refuses a train name that is one of the scheduler's own trains (ManifestManager, JobDispatcher, JobRunner, MetadataCleanup, DeadLetterCleanup), by full name, or by short name unless a host train shares it
  3. Resolves ITrainExecutionService and calls RunAsync(trainName, inputJson)
  4. Serializes the train output as JSON
  5. Returns 200 OK with a RemoteRunResponse containing the metadata ID, output JSON, and output type
  6. On error: returns 200 OK with RemoteRunResponse.IsError = true and structured error fields (ErrorMessage, ExceptionType, FailureJunction, FailureClass). StackTrace is always null, and ErrorMessage is fixed text unless the failure is a TrainException. FailureClass is the classification this process's IFailureClassifier assigned, or null when none is registered or it did not recognise the failure. PublicMessage is the message of a plain TrainException and null for anything else, including a type derived from TrainException. The response is written with Trax's own JSON options (enums as integers), not the host's, so a host configured to write enums as strings does not change the wire. The reading side also accepts names, and reads a class it does not know as Unclassified. Uses in-band errors to distinguish from infrastructure failures

Shared Requirements

The remote process must:

  • Reference the same train assemblies passed to AddMediator(). Input types are matched by fully-qualified name against the registered trains' input types.
  • Connect to the same Postgres database. Metadata, manifests, and state are shared across all processes.
  • Register the effect system. AddTrax() with UsePostgres() is required.

Package

dotnet add package Trax.Scheduler

See Also