AddLifecycleHook

Registers a lifecycle hook that fires on train state transitions. Use lifecycle hooks to trigger side effects when trains start, complete, fail, or are cancelled — without coupling your train logic to the side effect.

Signatures

Register a hook directly (recommended):

public static TraxEffectBuilder AddLifecycleHook<T>(
    this TraxEffectBuilder builder,
    bool toggleable = true
) where T : class // ITrainLifecycleHook or ITrainLifecycleHookFactory

When T implements ITrainLifecycleHook, a factory is created internally — no need to write a factory class. When T implements ITrainLifecycleHookFactory, it is registered directly (advanced).

Register with an existing factory instance:

public static TraxEffectBuilder AddLifecycleHook<TFactory>(
    this TraxEffectBuilder builder,
    TFactory factory,
    bool toggleable = true
) where TFactory : class, ITrainLifecycleHookFactory
ParameterTypeRequiredDefaultDescription
builderTraxEffectBuilderYesThe effect configuration builder
factoryTFactoryNoAn existing factory instance (when not using DI to create it)
toggleableboolNotrueWhether the hook can be enabled/disabled at runtime via IEffectRegistry

ITrainLifecycleHook

public interface ITrainLifecycleHook
{
    Task OnStarted(Metadata metadata, CancellationToken ct) => Task.CompletedTask;
    Task OnCompleted(Metadata metadata, CancellationToken ct) => Task.CompletedTask;
    Task OnFailed(Metadata metadata, Exception exception, CancellationToken ct) => Task.CompletedTask;
    Task OnCancelled(Metadata metadata, CancellationToken ct) => Task.CompletedTask;
}

All methods have default implementations that return Task.CompletedTask. Override only the events you care about.

MethodWhen it fires
OnStartedAfter the train's metadata is persisted and before RunInternal executes
OnCompletedAfter a successful run, after output is persisted
OnFailedAfter a failed run (exception that is not OperationCanceledException), after failure is persisted
OnCancelledAfter cancellation (OperationCanceledException), after cancellation is persisted

Accessing Train Output

metadata.Output is always available as serialized JSON in OnCompleted hooks, regardless of whether SaveTrainParameters() is configured. When SaveTrainParameters() is not configured, the output is serialized in-memory before hooks fire but is not persisted to the database. This means GraphQL subscriptions and custom hooks can always read metadata.Output without requiring SaveTrainParameters().

ITrainLifecycleHookFactory (Advanced)

public interface ITrainLifecycleHookFactory
{
    ITrainLifecycleHook Create();
}

Most users do not need to implement this interface — AddLifecycleHook<THook>() generates a factory automatically. Use a custom factory only if you need non-standard creation logic. The factory is a singleton; it creates a new hook instance per train execution.

Error Handling

Lifecycle hook exceptions are caught and logged, never propagated. A failing hook will never cause a train to fail. This is intentional — side effects like posting to Grafana or sending Slack notifications should not affect train reliability.

Example: Custom Slack Notification Hook

public class SlackNotificationHook(ISlackClient slack) : ITrainLifecycleHook
{
    public async Task OnFailed(Metadata metadata, Exception exception, CancellationToken ct) =>
        await slack.PostAsync($"Train {metadata.Name} failed: {exception.Message}", ct);
}

Registration:

builder.Services.AddTrax(trax => trax
    .AddEffects(effects => effects
        .UsePostgres(connectionString)
        .AddLifecycleHook<SlackNotificationHook>()
    )
    .AddMediator(ServiceLifetime.Scoped, typeof(Program).Assembly)
);

No factory class needed — Trax creates one internally and resolves your hook's constructor dependencies from DI.

Built-in Hooks

HookPackageDescription
GraphQLSubscriptionHookTrax.Api.GraphQLPublishes lifecycle events to GraphQL subscriptions. Automatically registered by AddTraxGraphQL().
BroadcastLifecycleHookTrax.EffectPublishes lifecycle events to a message bus for cross-process delivery. Automatically registered by UseBroadcaster().

Per-Train Lifecycle Hooks

In addition to global hooks registered via AddLifecycleHook<T>(), individual trains can override lifecycle methods directly on ServiceTrain. This is useful when a hook only makes sense for one specific train and you want to use the train's own injected dependencies.

public class BanPlayerTrain(ILogger<BanPlayerTrain> logger)
    : ServiceTrain<BanPlayerInput, Unit>, IBanPlayerTrain
{
    protected override async Task<Either<Exception, Unit>> RunInternal(BanPlayerInput input) =>
        Activate(input).Chain<ApplyBanJunction>().Resolve();
 
    protected override Task OnFailed(Metadata metadata, Exception exception, CancellationToken ct)
    {
        logger.LogWarning("Ban failed for train {TrainName}: {Message}",
            metadata.Name, exception.Message);
        return Task.CompletedTask;
    }
}

No registration needed — just override the method. The available methods match the global hook interface:

MethodSignature
OnStartedprotected virtual Task OnStarted(Metadata metadata, CancellationToken ct)
OnCompletedprotected virtual Task OnCompleted(Metadata metadata, CancellationToken ct)
OnFailedprotected virtual Task OnFailed(Metadata metadata, Exception exception, CancellationToken ct)
OnCancelledprotected virtual Task OnCancelled(Metadata metadata, CancellationToken ct)

All default to no-op (Task.CompletedTask). Override only the ones you need.

Global vs Per-Train Hooks

Global (AddLifecycleHook<T>)Per-Train (override)
ScopeFires for every trainFires only for the overriding train
RegistrationAddLifecycleHook<T>() in builderNo registration — just override
DI accessConstructor injection on the hook classConstructor injection on the train itself
Execution orderFires firstFires after global hooks
Error handlingCaught and loggedCaught and logged
Use caseCross-cutting concerns (metrics, audit logs)Train-specific business logic (alerts, cleanup)

Package

dotnet add package Trax.Effect