diff --git a/Trax.Samples.slnx b/Trax.Samples.slnx
index db56303..8f0ce54 100644
--- a/Trax.Samples.slnx
+++ b/Trax.Samples.slnx
@@ -13,6 +13,11 @@
+
+
+
+
+
diff --git a/docker-compose.yml b/docker-compose.yml
index 05a5668..fafd046 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -16,3 +16,16 @@ services:
- default
volumes:
- ./init-databases.sh:/docker-entrypoint-initdb.d/init-databases.sh
+
+ rabbitmq:
+ container_name: trax_rabbitmq
+ image: rabbitmq:4-management
+ restart: always
+ environment:
+ RABBITMQ_DEFAULT_USER: guest
+ RABBITMQ_DEFAULT_PASS: guest
+ ports:
+ - "5672:5672"
+ - "15672:15672"
+ networks:
+ - default
diff --git a/samples/DataPipeline/Trax.Samples.Flowthru.Spaceflights.Scheduler/Program.cs b/samples/DataPipeline/Trax.Samples.Flowthru.Spaceflights.Scheduler/Program.cs
index e1646ff..179cad3 100644
--- a/samples/DataPipeline/Trax.Samples.Flowthru.Spaceflights.Scheduler/Program.cs
+++ b/samples/DataPipeline/Trax.Samples.Flowthru.Spaceflights.Scheduler/Program.cs
@@ -33,7 +33,6 @@
using Trax.Scheduler.Configuration;
using Trax.Scheduler.Extensions;
using Trax.Scheduler.Services.Scheduling;
-using Trax.Scheduler.Trains.ManifestManager;
var builder = WebApplication.CreateBuilder(args);
@@ -81,7 +80,7 @@
.SaveTrainParameters()
.AddStepProgress()
)
- .AddMediator(typeof(ManifestNames).Assembly, typeof(ManifestManagerTrain).Assembly)
+ .AddMediator(typeof(ManifestNames).Assembly)
.AddScheduler(scheduler =>
{
scheduler
diff --git a/samples/DistributedWorkers/Trax.Samples.EnergyHub.Hub/Program.cs b/samples/DistributedWorkers/Trax.Samples.EnergyHub.Hub/Program.cs
index fe7170c..525d8ea 100644
--- a/samples/DistributedWorkers/Trax.Samples.EnergyHub.Hub/Program.cs
+++ b/samples/DistributedWorkers/Trax.Samples.EnergyHub.Hub/Program.cs
@@ -49,6 +49,7 @@
using Trax.Api.Extensions;
using Trax.Api.GraphQL.Extensions;
using Trax.Dashboard.Extensions;
+using Trax.Effect.Broadcaster.RabbitMQ.Extensions;
using Trax.Effect.Data.Extensions;
using Trax.Effect.Data.Postgres.Extensions;
using Trax.Effect.Extensions;
@@ -67,7 +68,6 @@
using Trax.Scheduler.Configuration;
using Trax.Scheduler.Extensions;
using Trax.Scheduler.Services.Scheduling;
-using Trax.Scheduler.Trains.ManifestManager;
var builder = WebApplication.CreateBuilder(args);
@@ -75,6 +75,10 @@
builder.Configuration.GetConnectionString("TraxDatabase")
?? throw new InvalidOperationException("Connection string 'TraxDatabase' not found.");
+var rabbitMqConnectionString =
+ builder.Configuration.GetConnectionString("RabbitMQ")
+ ?? throw new InvalidOperationException("Connection string 'RabbitMQ' not found.");
+
builder.Services.AddLogging(logging => logging.AddConsole());
builder.Services.AddTrax(trax =>
@@ -85,24 +89,23 @@
.AddJson()
.SaveTrainParameters()
.AddStepProgress()
+ .UseBroadcaster(b => b.UseRabbitMq(rabbitMqConnectionString))
)
- .AddMediator(typeof(ManifestNames).Assembly, typeof(ManifestManagerTrain).Assembly)
+ .AddMediator(typeof(ManifestNames).Assembly)
.AddScheduler(scheduler =>
{
// ── Key: scheduling only, no local execution ──────────────────
// PostgresJobSubmitter is the default — omit UseLocalWorkers() to schedule without executing locally
// Jobs accumulate until the Worker process picks them up.
- scheduler
- .AddMetadataCleanup(cleanup =>
- {
- cleanup.AddTrainType();
- cleanup.AddTrainType();
- cleanup.AddTrainType();
- cleanup.AddTrainType();
- cleanup.AddTrainType();
- cleanup.AddTrainType();
- })
- .JobDispatcherPollingInterval(TimeSpan.FromSeconds(2));
+ scheduler.AddMetadataCleanup(cleanup =>
+ {
+ cleanup.AddTrainType();
+ cleanup.AddTrainType();
+ cleanup.AddTrainType();
+ cleanup.AddTrainType();
+ cleanup.AddTrainType();
+ cleanup.AddTrainType();
+ });
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 1. INTERVAL + DEPENDENCY CHAIN
diff --git a/samples/DistributedWorkers/Trax.Samples.EnergyHub.Hub/Trax.Samples.EnergyHub.Hub.csproj b/samples/DistributedWorkers/Trax.Samples.EnergyHub.Hub/Trax.Samples.EnergyHub.Hub.csproj
index e1d511c..0d5a76a 100644
--- a/samples/DistributedWorkers/Trax.Samples.EnergyHub.Hub/Trax.Samples.EnergyHub.Hub.csproj
+++ b/samples/DistributedWorkers/Trax.Samples.EnergyHub.Hub/Trax.Samples.EnergyHub.Hub.csproj
@@ -19,5 +19,6 @@
+
diff --git a/samples/DistributedWorkers/Trax.Samples.EnergyHub.Hub/appsettings.json b/samples/DistributedWorkers/Trax.Samples.EnergyHub.Hub/appsettings.json
index 6461d1e..8905b26 100644
--- a/samples/DistributedWorkers/Trax.Samples.EnergyHub.Hub/appsettings.json
+++ b/samples/DistributedWorkers/Trax.Samples.EnergyHub.Hub/appsettings.json
@@ -1,6 +1,7 @@
{
"ConnectionStrings": {
- "TraxDatabase": "Host=localhost;Port=5432;Database=trax;Username=trax;Password=trax123"
+ "TraxDatabase": "Host=localhost;Port=5432;Database=trax;Username=trax;Password=trax123",
+ "RabbitMQ": "amqp://guest:guest@localhost:5672"
},
"Kestrel": {
"Endpoints": {
diff --git a/samples/DistributedWorkers/Trax.Samples.EnergyHub.Worker/Program.cs b/samples/DistributedWorkers/Trax.Samples.EnergyHub.Worker/Program.cs
index 133c4f5..ce6fafc 100644
--- a/samples/DistributedWorkers/Trax.Samples.EnergyHub.Worker/Program.cs
+++ b/samples/DistributedWorkers/Trax.Samples.EnergyHub.Worker/Program.cs
@@ -23,6 +23,7 @@
// so multiple worker instances can run safely without duplicate execution.
// ─────────────────────────────────────────────────────────────────────────────
+using Trax.Effect.Broadcaster.RabbitMQ.Extensions;
using Trax.Effect.Data.Postgres.Extensions;
using Trax.Effect.Extensions;
using Trax.Effect.Provider.Json.Extensions;
@@ -31,7 +32,6 @@
using Trax.Mediator.Extensions;
using Trax.Samples.EnergyHub;
using Trax.Scheduler.Extensions;
-using Trax.Scheduler.Trains.ManifestManager;
var builder = WebApplication.CreateBuilder(args);
@@ -39,16 +39,27 @@
builder.Configuration.GetConnectionString("TraxDatabase")
?? throw new InvalidOperationException("Connection string 'TraxDatabase' not found.");
+var rabbitMqConnectionString =
+ builder.Configuration.GetConnectionString("RabbitMQ")
+ ?? throw new InvalidOperationException("Connection string 'RabbitMQ' not found.");
+
builder.Services.AddLogging(logging => logging.AddConsole());
// ── Register Trax Effect + Mediator (trains, bus, discovery, execution) ──
// The worker must reference the same train assemblies as the scheduler so it
// can resolve and execute any train type that gets dispatched.
+// UseBroadcaster() publishes lifecycle events to RabbitMQ so the Hub's
+// GraphQL subscriptions are notified when queued trains complete.
builder.Services.AddTrax(trax =>
trax.AddEffects(effects =>
- effects.UsePostgres(connectionString).AddJson().SaveTrainParameters().AddStepProgress()
+ effects
+ .UsePostgres(connectionString)
+ .AddJson()
+ .SaveTrainParameters()
+ .AddStepProgress()
+ .UseBroadcaster(b => b.UseRabbitMq(rabbitMqConnectionString))
)
- .AddMediator(typeof(ManifestNames).Assembly, typeof(ManifestManagerTrain).Assembly)
+ .AddMediator(typeof(ManifestNames).Assembly)
);
// ── Register standalone worker ───────────────────────────────────────────
diff --git a/samples/DistributedWorkers/Trax.Samples.EnergyHub.Worker/Trax.Samples.EnergyHub.Worker.csproj b/samples/DistributedWorkers/Trax.Samples.EnergyHub.Worker/Trax.Samples.EnergyHub.Worker.csproj
index cd3ef4d..158da86 100644
--- a/samples/DistributedWorkers/Trax.Samples.EnergyHub.Worker/Trax.Samples.EnergyHub.Worker.csproj
+++ b/samples/DistributedWorkers/Trax.Samples.EnergyHub.Worker/Trax.Samples.EnergyHub.Worker.csproj
@@ -15,5 +15,6 @@
+
diff --git a/samples/DistributedWorkers/Trax.Samples.EnergyHub.Worker/appsettings.json b/samples/DistributedWorkers/Trax.Samples.EnergyHub.Worker/appsettings.json
index 8b2ff47..19ed6f3 100644
--- a/samples/DistributedWorkers/Trax.Samples.EnergyHub.Worker/appsettings.json
+++ b/samples/DistributedWorkers/Trax.Samples.EnergyHub.Worker/appsettings.json
@@ -1,6 +1,7 @@
{
"ConnectionStrings": {
- "TraxDatabase": "Host=localhost;Port=5432;Database=trax;Username=trax;Password=trax123"
+ "TraxDatabase": "Host=localhost;Port=5432;Database=trax;Username=trax;Password=trax123",
+ "RabbitMQ": "amqp://guest:guest@localhost:5672"
},
"Kestrel": {
"Endpoints": {
diff --git a/samples/EphemeralWorkers/Trax.Samples.ContentShield.Api/Program.cs b/samples/EphemeralWorkers/Trax.Samples.ContentShield.Api/Program.cs
new file mode 100644
index 0000000..e59c3e3
--- /dev/null
+++ b/samples/EphemeralWorkers/Trax.Samples.ContentShield.Api/Program.cs
@@ -0,0 +1,121 @@
+// ─────────────────────────────────────────────────────────────────────────────
+// ContentShield — API (GraphQL + Dashboard, ephemeral HTTP dispatch)
+//
+// A single process that serves the GraphQL API and hosts the Trax dashboard.
+// There are NO scheduled jobs — all work is triggered by GraphQL mutations.
+// Queued mutations are dispatched via HTTP to the ephemeral Runner process
+// using UseRemoteWorkers(). The Runner simulates a Lambda/serverless function.
+//
+// This demonstrates the Ephemeral Workers pattern:
+// - Query trains (LookupModerationResult) run synchronously on this process
+// - Queued trains (ReviewContent, SendViolationNotice, GenerateModerationReport)
+// are POSTed to the Runner via HTTP — no background_job table, no DB polling
+// - Run mutations (GenerateModerationReport) are also offloaded to the Runner
+// via UseRemoteRun() — the API blocks until the Runner returns the output
+//
+// GraphQL schema (auto-generated from train attributes):
+// Queries: lookupModerationResult — [TraxQuery]
+// Mutations: queueReviewContent — [TraxMutation(Queue)]
+// queueSendViolationNotice — [TraxMutation(Queue)]
+// runGenerateModerationReport — [TraxMutation(RunAndQueue)]
+// queueGenerateModerationReport — [TraxMutation(RunAndQueue)]
+// Subscriptions: onTrainStarted, onTrainCompleted, onTrainFailed
+//
+// Prerequisites:
+// 1. Start Postgres: cd Trax.Samples && docker compose up -d
+// 2. Pack local: ./pack-local.sh
+// 3. Start runner: dotnet run --project samples/EphemeralWorkers/Trax.Samples.ContentShield.Runner
+// 4. Start API: dotnet run --project samples/EphemeralWorkers/Trax.Samples.ContentShield.Api
+//
+// Endpoints:
+// Dashboard: http://localhost:5204/trax
+// GraphQL IDE: http://localhost:5204/trax/graphql (Banana Cake Pop)
+//
+// Try it:
+// # Look up a moderation result (runs on API)
+// curl -X POST http://localhost:5204/trax/graphql \
+// -H "Content-Type: application/json" \
+// -d '{"query":"{ discover { lookupModerationResult(input: {contentId: \"test-001\"}) { contentId moderationStatus classification threatScore } } }"}'
+//
+// # Queue a content review (dispatched to Runner via HTTP)
+// curl -X POST http://localhost:5204/trax/graphql \
+// -H "Content-Type: application/json" \
+// -d '{"query":"mutation { dispatch { queueReviewContent(input: {contentId: \"test-002\", contentType: \"video\", contentBody: \"suspicious video content\"}) { workQueueId externalId } } }"}'
+//
+// # Generate a moderation report (offloaded to Runner via UseRemoteRun, blocks until done)
+// curl -X POST http://localhost:5204/trax/graphql \
+// -H "Content-Type: application/json" \
+// -d '{"query":"mutation { dispatch { runGenerateModerationReport(input: {reportPeriod: \"Daily\"}) { totalReviewed totalFlagged topViolationTypes falsePositiveRate } } }"}'
+// ─────────────────────────────────────────────────────────────────────────────
+
+using Trax.Api.Extensions;
+using Trax.Api.GraphQL.Extensions;
+using Trax.Dashboard.Extensions;
+using Trax.Effect.Broadcaster.RabbitMQ.Extensions;
+using Trax.Effect.Data.Extensions;
+using Trax.Effect.Data.Postgres.Extensions;
+using Trax.Effect.Extensions;
+using Trax.Effect.Provider.Json.Extensions;
+using Trax.Effect.Provider.Parameter.Extensions;
+using Trax.Effect.StepProvider.Progress.Extensions;
+using Trax.Mediator.Extensions;
+using Trax.Samples.ContentShield.Trains.ContentReview.ReviewContent;
+using Trax.Scheduler.Extensions;
+
+var builder = WebApplication.CreateBuilder(args);
+
+var connectionString =
+ builder.Configuration.GetConnectionString("TraxDatabase")
+ ?? throw new InvalidOperationException("Connection string 'TraxDatabase' not found.");
+
+var rabbitMqConnectionString =
+ builder.Configuration.GetConnectionString("RabbitMQ")
+ ?? throw new InvalidOperationException("Connection string 'RabbitMQ' not found.");
+
+builder.Services.AddLogging(logging => logging.AddConsole());
+
+builder.Services.AddTrax(trax =>
+ trax.AddEffects(effects =>
+ effects
+ .UsePostgres(connectionString)
+ .AddDataContextLogging()
+ .AddJson()
+ .SaveTrainParameters()
+ .AddStepProgress()
+ .UseBroadcaster(b => b.UseRabbitMq(rabbitMqConnectionString))
+ )
+ .AddMediator(typeof(ReviewContentTrain).Assembly)
+ .AddScheduler(scheduler =>
+ {
+ // ── Ephemeral dispatch only — no scheduled jobs ──────────────────
+ // UseRemoteWorkers replaces the default PostgresJobSubmitter with
+ // HttpJobSubmitter. When a GraphQL queue* mutation is called, the
+ // JobDispatcher POSTs the job directly to the Runner via HTTP.
+ // No cron schedules, no intervals, no manifests — purely on-demand.
+ scheduler.UseRemoteWorkers(remote =>
+ remote.BaseUrl = "http://localhost:5205/trax/execute"
+ );
+
+ // ── Remote run offloading ────────────────────────────────────────
+ // UseRemoteRun replaces the default LocalRunExecutor with
+ // HttpRunExecutor. When a GraphQL run* mutation is called, the
+ // request is POSTed to the Runner and blocks until the train
+ // completes. Without this, runs execute in-process on this API.
+ scheduler.UseRemoteRun(remote => remote.BaseUrl = "http://localhost:5205/trax/run");
+ })
+);
+
+// ── Register GraphQL API ────────────────────────────────────────────────
+// Trains annotated with [TraxQuery] or [TraxMutation] get typed GraphQL
+// fields auto-generated. [TraxBroadcast] trains emit subscription events.
+builder.AddTraxDashboard();
+builder.Services.AddTraxGraphQL();
+builder.Services.AddHealthChecks().AddTraxHealthCheck();
+
+var app = builder.Build();
+
+app.UseTraxDashboard();
+app.UseTraxGraphQL();
+app.MapHealthChecks("/trax/health");
+
+app.Run();
diff --git a/samples/EphemeralWorkers/Trax.Samples.ContentShield.Api/Trax.Samples.ContentShield.Api.csproj b/samples/EphemeralWorkers/Trax.Samples.ContentShield.Api/Trax.Samples.ContentShield.Api.csproj
new file mode 100644
index 0000000..08e1d37
--- /dev/null
+++ b/samples/EphemeralWorkers/Trax.Samples.ContentShield.Api/Trax.Samples.ContentShield.Api.csproj
@@ -0,0 +1,24 @@
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/EphemeralWorkers/Trax.Samples.ContentShield.Api/appsettings.json b/samples/EphemeralWorkers/Trax.Samples.ContentShield.Api/appsettings.json
new file mode 100644
index 0000000..a0f4aa5
--- /dev/null
+++ b/samples/EphemeralWorkers/Trax.Samples.ContentShield.Api/appsettings.json
@@ -0,0 +1,19 @@
+{
+ "ConnectionStrings": {
+ "TraxDatabase": "Host=localhost;Port=5432;Database=trax;Username=trax;Password=trax123",
+ "RabbitMQ": "amqp://guest:guest@localhost:5672"
+ },
+ "Kestrel": {
+ "Endpoints": {
+ "Http": {
+ "Url": "http://localhost:5204"
+ }
+ }
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/samples/EphemeralWorkers/Trax.Samples.ContentShield.Runner/Program.cs b/samples/EphemeralWorkers/Trax.Samples.ContentShield.Runner/Program.cs
new file mode 100644
index 0000000..c4f8289
--- /dev/null
+++ b/samples/EphemeralWorkers/Trax.Samples.ContentShield.Runner/Program.cs
@@ -0,0 +1,80 @@
+// ─────────────────────────────────────────────────────────────────────────────
+// ContentShield — Ephemeral Runner (simulates Lambda/serverless execution)
+//
+// A minimal HTTP endpoint that receives job requests from the API via HTTP POST
+// and executes trains to completion. This process has no scheduler, no polling,
+// no dashboard — it only runs trains that are dispatched to it.
+//
+// This demonstrates the ephemeral/serverless worker pattern:
+// 1. The API dispatches jobs via UseRemoteWorkers()
+// 2. HttpJobSubmitter POSTs a RemoteJobRequest to this endpoint
+// 3. This runner deserializes the input, runs JobRunnerTrain, and returns
+// 4. No background_job table — jobs arrive directly over HTTP
+//
+// In production, this would be an AWS Lambda, Azure Function, or Cloud Run
+// service that spins up on demand to handle each job request.
+//
+// Prerequisites:
+// 1. Start Postgres: cd Trax.Samples && docker compose up -d
+// 2. Pack local: ./pack-local.sh
+// 3. Start runner: dotnet run --project samples/EphemeralWorkers/Trax.Samples.ContentShield.Runner
+// 4. Start API: dotnet run --project samples/EphemeralWorkers/Trax.Samples.ContentShield.Api
+//
+// Endpoints:
+// POST http://localhost:5205/trax/execute (receives RemoteJobRequest JSON — queue path)
+// POST http://localhost:5205/trax/run (receives RemoteRunRequest JSON — synchronous run path)
+// ─────────────────────────────────────────────────────────────────────────────
+
+using Trax.Effect.Broadcaster.RabbitMQ.Extensions;
+using Trax.Effect.Data.Postgres.Extensions;
+using Trax.Effect.Extensions;
+using Trax.Effect.Provider.Json.Extensions;
+using Trax.Effect.Provider.Parameter.Extensions;
+using Trax.Effect.StepProvider.Progress.Extensions;
+using Trax.Mediator.Extensions;
+using Trax.Samples.ContentShield.Trains.ContentReview.ReviewContent;
+using Trax.Scheduler.Extensions;
+
+var builder = WebApplication.CreateBuilder(args);
+
+var connectionString =
+ builder.Configuration.GetConnectionString("TraxDatabase")
+ ?? throw new InvalidOperationException("Connection string 'TraxDatabase' not found.");
+
+var rabbitMqConnectionString =
+ builder.Configuration.GetConnectionString("RabbitMQ")
+ ?? throw new InvalidOperationException("Connection string 'RabbitMQ' not found.");
+
+builder.Services.AddLogging(logging => logging.AddConsole());
+
+// ── Register Trax Effect + Mediator (trains, bus, discovery, execution) ──
+// The runner must reference the same train assemblies as the API so it can
+// resolve and execute any train type that gets dispatched.
+// UseBroadcaster publishes lifecycle events to RabbitMQ so the API's
+// GraphQL subscriptions are notified when queued trains complete.
+builder.Services.AddTrax(trax =>
+ trax.AddEffects(effects =>
+ effects
+ .UsePostgres(connectionString)
+ .AddJson()
+ .SaveTrainParameters()
+ .AddStepProgress()
+ .UseBroadcaster(b => b.UseRabbitMq(rabbitMqConnectionString))
+ )
+ .AddMediator(typeof(ReviewContentTrain).Assembly)
+);
+
+// ── Register job runner endpoint ──────────────────────────────────────────
+// AddTraxJobRunner() registers JobRunnerTrain and minimal supporting services.
+// No scheduler, no polling, no dashboard — just the execution pipeline.
+builder.Services.AddTraxJobRunner();
+
+var app = builder.Build();
+
+// Maps POST /trax/execute — receives RemoteJobRequest, runs JobRunnerTrain (queue path)
+app.UseTraxJobRunner();
+
+// Maps POST /trax/run — receives RemoteRunRequest, runs train synchronously and returns output
+app.UseTraxRunEndpoint();
+
+app.Run();
diff --git a/samples/EphemeralWorkers/Trax.Samples.ContentShield.Runner/Trax.Samples.ContentShield.Runner.csproj b/samples/EphemeralWorkers/Trax.Samples.ContentShield.Runner/Trax.Samples.ContentShield.Runner.csproj
new file mode 100644
index 0000000..2b0c11b
--- /dev/null
+++ b/samples/EphemeralWorkers/Trax.Samples.ContentShield.Runner/Trax.Samples.ContentShield.Runner.csproj
@@ -0,0 +1,21 @@
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/EphemeralWorkers/Trax.Samples.ContentShield.Runner/appsettings.json b/samples/EphemeralWorkers/Trax.Samples.ContentShield.Runner/appsettings.json
new file mode 100644
index 0000000..2547367
--- /dev/null
+++ b/samples/EphemeralWorkers/Trax.Samples.ContentShield.Runner/appsettings.json
@@ -0,0 +1,19 @@
+{
+ "ConnectionStrings": {
+ "TraxDatabase": "Host=localhost;Port=5432;Database=trax;Username=trax;Password=trax123",
+ "RabbitMQ": "amqp://guest:guest@localhost:5672"
+ },
+ "Kestrel": {
+ "Endpoints": {
+ "Http": {
+ "Url": "http://localhost:5205"
+ }
+ }
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/LookupModerationResult/ILookupModerationResultTrain.cs b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/LookupModerationResult/ILookupModerationResultTrain.cs
new file mode 100644
index 0000000..b8ed92c
--- /dev/null
+++ b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/LookupModerationResult/ILookupModerationResultTrain.cs
@@ -0,0 +1,6 @@
+using Trax.Effect.Services.ServiceTrain;
+
+namespace Trax.Samples.ContentShield.Trains.ContentReview.LookupModerationResult;
+
+public interface ILookupModerationResultTrain
+ : IServiceTrain;
diff --git a/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/LookupModerationResult/LookupModerationResultInput.cs b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/LookupModerationResult/LookupModerationResultInput.cs
new file mode 100644
index 0000000..f81c53d
--- /dev/null
+++ b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/LookupModerationResult/LookupModerationResultInput.cs
@@ -0,0 +1,6 @@
+namespace Trax.Samples.ContentShield.Trains.ContentReview.LookupModerationResult;
+
+public record LookupModerationResultInput
+{
+ public required string ContentId { get; init; }
+}
diff --git a/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/LookupModerationResult/LookupModerationResultTrain.cs b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/LookupModerationResult/LookupModerationResultTrain.cs
new file mode 100644
index 0000000..f5b0d7f
--- /dev/null
+++ b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/LookupModerationResult/LookupModerationResultTrain.cs
@@ -0,0 +1,21 @@
+using LanguageExt;
+using Trax.Effect.Attributes;
+using Trax.Effect.Services.ServiceTrain;
+using Trax.Samples.ContentShield.Trains.ContentReview.LookupModerationResult.Steps;
+
+namespace Trax.Samples.ContentShield.Trains.ContentReview.LookupModerationResult;
+
+///
+/// Lightweight lookup of a content moderation result. Runs synchronously on the
+/// API process — does not go through the scheduler or ephemeral Runner.
+///
+[TraxQuery(Description = "Looks up a content moderation result")]
+[TraxBroadcast]
+public class LookupModerationResultTrain
+ : ServiceTrain,
+ ILookupModerationResultTrain
+{
+ protected override async Task> RunInternal(
+ LookupModerationResultInput input
+ ) => Activate(input).Chain().Resolve();
+}
diff --git a/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/LookupModerationResult/ModerationResult.cs b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/LookupModerationResult/ModerationResult.cs
new file mode 100644
index 0000000..d389745
--- /dev/null
+++ b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/LookupModerationResult/ModerationResult.cs
@@ -0,0 +1,10 @@
+namespace Trax.Samples.ContentShield.Trains.ContentReview.LookupModerationResult;
+
+public record ModerationResult
+{
+ public required string ContentId { get; init; }
+ public required string ModerationStatus { get; init; }
+ public required string Classification { get; init; }
+ public double ThreatScore { get; init; }
+ public DateTimeOffset ReviewedAt { get; init; }
+}
diff --git a/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/LookupModerationResult/Steps/FetchModerationResultStep.cs b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/LookupModerationResult/Steps/FetchModerationResultStep.cs
new file mode 100644
index 0000000..f881296
--- /dev/null
+++ b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/LookupModerationResult/Steps/FetchModerationResultStep.cs
@@ -0,0 +1,31 @@
+using Microsoft.Extensions.Logging;
+using Trax.Core.Step;
+
+namespace Trax.Samples.ContentShield.Trains.ContentReview.LookupModerationResult.Steps;
+
+///
+/// Fetches a moderation result from the database. Returns a simulated result
+/// for demonstration purposes.
+///
+public class FetchModerationResultStep(ILogger logger)
+ : Step
+{
+ public override async Task Run(LookupModerationResultInput input)
+ {
+ logger.LogInformation(
+ "Fetching moderation result for content {ContentId}",
+ input.ContentId
+ );
+
+ await Task.Delay(50);
+
+ return new ModerationResult
+ {
+ ContentId = input.ContentId,
+ ModerationStatus = "Reviewed",
+ Classification = "safe",
+ ThreatScore = 0.12,
+ ReviewedAt = DateTimeOffset.UtcNow.AddMinutes(-15),
+ };
+ }
+}
diff --git a/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/ReviewContent/IReviewContentTrain.cs b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/ReviewContent/IReviewContentTrain.cs
new file mode 100644
index 0000000..24841b4
--- /dev/null
+++ b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/ReviewContent/IReviewContentTrain.cs
@@ -0,0 +1,5 @@
+using Trax.Effect.Services.ServiceTrain;
+
+namespace Trax.Samples.ContentShield.Trains.ContentReview.ReviewContent;
+
+public interface IReviewContentTrain : IServiceTrain;
diff --git a/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/ReviewContent/ReviewContentInput.cs b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/ReviewContent/ReviewContentInput.cs
new file mode 100644
index 0000000..756b6ce
--- /dev/null
+++ b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/ReviewContent/ReviewContentInput.cs
@@ -0,0 +1,8 @@
+namespace Trax.Samples.ContentShield.Trains.ContentReview.ReviewContent;
+
+public record ReviewContentInput
+{
+ public required string ContentId { get; init; }
+ public required string ContentType { get; init; }
+ public required string ContentBody { get; init; }
+}
diff --git a/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/ReviewContent/ReviewContentOutput.cs b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/ReviewContent/ReviewContentOutput.cs
new file mode 100644
index 0000000..6dc9769
--- /dev/null
+++ b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/ReviewContent/ReviewContentOutput.cs
@@ -0,0 +1,10 @@
+namespace Trax.Samples.ContentShield.Trains.ContentReview.ReviewContent;
+
+public record ReviewContentOutput
+{
+ public required string ContentId { get; init; }
+ public required string Classification { get; init; }
+ public double ThreatScore { get; init; }
+ public bool IsFlagged { get; init; }
+ public string? FlagReason { get; init; }
+}
diff --git a/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/ReviewContent/ReviewContentTrain.cs b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/ReviewContent/ReviewContentTrain.cs
new file mode 100644
index 0000000..122b5b5
--- /dev/null
+++ b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/ReviewContent/ReviewContentTrain.cs
@@ -0,0 +1,32 @@
+using LanguageExt;
+using Trax.Effect.Attributes;
+using Trax.Effect.Services.ServiceTrain;
+using Trax.Samples.ContentShield.Trains.ContentReview.ReviewContent.Steps;
+
+namespace Trax.Samples.ContentShield.Trains.ContentReview.ReviewContent;
+
+///
+/// Reviews user-submitted content for policy violations. Classifies the content,
+/// scores its threat level, and flags violations. When flagged, activates the
+/// dormant SendViolationNotice train to notify the content owner.
+///
+/// Dispatched to the ephemeral Runner via HTTP (UseRemoteWorkers).
+///
+[TraxMutation(
+ Operations = GraphQLOperation.Queue,
+ Description = "Reviews content for policy violations"
+)]
+[TraxBroadcast]
+public class ReviewContentTrain
+ : ServiceTrain,
+ IReviewContentTrain
+{
+ protected override async Task> RunInternal(
+ ReviewContentInput input
+ ) =>
+ Activate(input)
+ .Chain()
+ .Chain()
+ .Chain()
+ .Resolve();
+}
diff --git a/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/ReviewContent/Steps/ClassifyContentStep.cs b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/ReviewContent/Steps/ClassifyContentStep.cs
new file mode 100644
index 0000000..b41a458
--- /dev/null
+++ b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/ReviewContent/Steps/ClassifyContentStep.cs
@@ -0,0 +1,32 @@
+using Microsoft.Extensions.Logging;
+using Trax.Core.Step;
+
+namespace Trax.Samples.ContentShield.Trains.ContentReview.ReviewContent.Steps;
+
+///
+/// Classifies content into a category (safe, spam, hate-speech, violence, etc.)
+/// using a simulated ML model.
+///
+public class ClassifyContentStep(ILogger logger)
+ : Step
+{
+ public override async Task Run(ReviewContentInput input)
+ {
+ logger.LogInformation(
+ "Classifying {ContentType} content {ContentId}",
+ input.ContentType,
+ input.ContentId
+ );
+
+ await Task.Delay(300);
+
+ var category = input.ContentType == "video" ? "violence" : "safe";
+ logger.LogInformation(
+ "Content {ContentId} classified as: {Category}",
+ input.ContentId,
+ category
+ );
+
+ return input;
+ }
+}
diff --git a/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/ReviewContent/Steps/FlagContentStep.cs b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/ReviewContent/Steps/FlagContentStep.cs
new file mode 100644
index 0000000..069e412
--- /dev/null
+++ b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/ReviewContent/Steps/FlagContentStep.cs
@@ -0,0 +1,47 @@
+using Microsoft.Extensions.Logging;
+using Trax.Core.Step;
+
+namespace Trax.Samples.ContentShield.Trains.ContentReview.ReviewContent.Steps;
+
+///
+/// Evaluates the threat score and flags content that exceeds the threshold.
+///
+public class FlagContentStep(ILogger logger)
+ : Step
+{
+ public override async Task Run(ReviewContentInput input)
+ {
+ // Simulate threat scoring — video content scores high, text scores low
+ var threatScore = input.ContentType == "video" ? 0.85 : 0.25;
+ var classification = threatScore > 0.7 ? "violence" : "safe";
+ var isFlagged = threatScore > 0.7;
+ string? flagReason = isFlagged ? "Threat score exceeds moderation threshold" : null;
+
+ if (isFlagged)
+ {
+ logger.LogWarning(
+ "Content {ContentId} FLAGGED — score {ThreatScore:F2}, classification: {Classification}",
+ input.ContentId,
+ threatScore,
+ classification
+ );
+ }
+ else
+ {
+ logger.LogInformation(
+ "Content {ContentId} passed moderation — score {ThreatScore:F2}",
+ input.ContentId,
+ threatScore
+ );
+ }
+
+ return new ReviewContentOutput
+ {
+ ContentId = input.ContentId,
+ Classification = classification,
+ ThreatScore = threatScore,
+ IsFlagged = isFlagged,
+ FlagReason = flagReason,
+ };
+ }
+}
diff --git a/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/ReviewContent/Steps/ScoreContentStep.cs b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/ReviewContent/Steps/ScoreContentStep.cs
new file mode 100644
index 0000000..20f81d0
--- /dev/null
+++ b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/ContentReview/ReviewContent/Steps/ScoreContentStep.cs
@@ -0,0 +1,28 @@
+using Microsoft.Extensions.Logging;
+using Trax.Core.Step;
+
+namespace Trax.Samples.ContentShield.Trains.ContentReview.ReviewContent.Steps;
+
+///
+/// Assigns a threat score (0.0–1.0) to the content based on classification signals.
+/// Scores above 0.7 trigger flagging in the next step.
+///
+public class ScoreContentStep(ILogger logger)
+ : Step
+{
+ public override async Task Run(ReviewContentInput input)
+ {
+ logger.LogInformation("Scoring threat level for content {ContentId}", input.ContentId);
+
+ await Task.Delay(200);
+
+ var score = input.ContentType == "video" ? 0.85 : 0.25;
+ logger.LogInformation(
+ "Content {ContentId} threat score: {ThreatScore:F2}",
+ input.ContentId,
+ score
+ );
+
+ return input;
+ }
+}
diff --git a/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Notices/SendViolationNotice/ISendViolationNoticeTrain.cs b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Notices/SendViolationNotice/ISendViolationNoticeTrain.cs
new file mode 100644
index 0000000..271972a
--- /dev/null
+++ b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Notices/SendViolationNotice/ISendViolationNoticeTrain.cs
@@ -0,0 +1,6 @@
+using Trax.Effect.Services.ServiceTrain;
+
+namespace Trax.Samples.ContentShield.Trains.Notices.SendViolationNotice;
+
+public interface ISendViolationNoticeTrain
+ : IServiceTrain;
diff --git a/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Notices/SendViolationNotice/SendViolationNoticeInput.cs b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Notices/SendViolationNotice/SendViolationNoticeInput.cs
new file mode 100644
index 0000000..b2299d5
--- /dev/null
+++ b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Notices/SendViolationNotice/SendViolationNoticeInput.cs
@@ -0,0 +1,8 @@
+namespace Trax.Samples.ContentShield.Trains.Notices.SendViolationNotice;
+
+public record SendViolationNoticeInput
+{
+ public required string ContentId { get; init; }
+ public required string ViolationType { get; init; }
+ public required string UserId { get; init; }
+}
diff --git a/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Notices/SendViolationNotice/SendViolationNoticeOutput.cs b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Notices/SendViolationNotice/SendViolationNoticeOutput.cs
new file mode 100644
index 0000000..731de29
--- /dev/null
+++ b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Notices/SendViolationNotice/SendViolationNoticeOutput.cs
@@ -0,0 +1,7 @@
+namespace Trax.Samples.ContentShield.Trains.Notices.SendViolationNotice;
+
+public record SendViolationNoticeOutput
+{
+ public required string NoticeId { get; init; }
+ public DateTimeOffset DeliveredAt { get; init; }
+}
diff --git a/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Notices/SendViolationNotice/SendViolationNoticeTrain.cs b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Notices/SendViolationNotice/SendViolationNoticeTrain.cs
new file mode 100644
index 0000000..ec4f3e2
--- /dev/null
+++ b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Notices/SendViolationNotice/SendViolationNoticeTrain.cs
@@ -0,0 +1,27 @@
+using LanguageExt;
+using Trax.Effect.Attributes;
+using Trax.Effect.Services.ServiceTrain;
+using Trax.Samples.ContentShield.Trains.Notices.SendViolationNotice.Steps;
+
+namespace Trax.Samples.ContentShield.Trains.Notices.SendViolationNotice;
+
+///
+/// Sends a violation notice to the content owner. Dormant dependent — activated
+/// by FlagContentStep when content is flagged. Composes the notice from a template
+/// and delivers it via email/push notification.
+///
+/// Dispatched to the ephemeral Runner via HTTP (UseRemoteWorkers).
+///
+[TraxMutation(
+ Operations = GraphQLOperation.Queue,
+ Description = "Sends a violation notice to the content owner"
+)]
+[TraxBroadcast]
+public class SendViolationNoticeTrain
+ : ServiceTrain,
+ ISendViolationNoticeTrain
+{
+ protected override async Task> RunInternal(
+ SendViolationNoticeInput input
+ ) => Activate(input).Chain().Chain().Resolve();
+}
diff --git a/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Notices/SendViolationNotice/Steps/ComposeNoticeStep.cs b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Notices/SendViolationNotice/Steps/ComposeNoticeStep.cs
new file mode 100644
index 0000000..caaa71f
--- /dev/null
+++ b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Notices/SendViolationNotice/Steps/ComposeNoticeStep.cs
@@ -0,0 +1,30 @@
+using Microsoft.Extensions.Logging;
+using Trax.Core.Step;
+
+namespace Trax.Samples.ContentShield.Trains.Notices.SendViolationNotice.Steps;
+
+///
+/// Composes a violation notice from a template based on the violation type.
+///
+public class ComposeNoticeStep(ILogger logger)
+ : Step
+{
+ public override async Task Run(SendViolationNoticeInput input)
+ {
+ logger.LogInformation(
+ "Composing {ViolationType} violation notice for content {ContentId}, user {UserId}",
+ input.ViolationType,
+ input.ContentId,
+ input.UserId
+ );
+
+ await Task.Delay(100);
+
+ logger.LogInformation(
+ "Notice composed from template: violation-{ViolationType}",
+ input.ViolationType
+ );
+
+ return input;
+ }
+}
diff --git a/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Notices/SendViolationNotice/Steps/DeliverNoticeStep.cs b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Notices/SendViolationNotice/Steps/DeliverNoticeStep.cs
new file mode 100644
index 0000000..57574aa
--- /dev/null
+++ b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Notices/SendViolationNotice/Steps/DeliverNoticeStep.cs
@@ -0,0 +1,31 @@
+using Microsoft.Extensions.Logging;
+using Trax.Core.Step;
+
+namespace Trax.Samples.ContentShield.Trains.Notices.SendViolationNotice.Steps;
+
+///
+/// Delivers the composed violation notice via email and push notification.
+///
+public class DeliverNoticeStep(ILogger logger)
+ : Step
+{
+ public override async Task Run(SendViolationNoticeInput input)
+ {
+ logger.LogInformation(
+ "Delivering violation notice to user {UserId} for content {ContentId}",
+ input.UserId,
+ input.ContentId
+ );
+
+ await Task.Delay(150);
+
+ var noticeId = $"notice-{Guid.NewGuid():N}";
+ logger.LogInformation("Notice {NoticeId} delivered successfully", noticeId);
+
+ return new SendViolationNoticeOutput
+ {
+ NoticeId = noticeId,
+ DeliveredAt = DateTimeOffset.UtcNow,
+ };
+ }
+}
diff --git a/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Reports/GenerateModerationReport/GenerateModerationReportInput.cs b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Reports/GenerateModerationReport/GenerateModerationReportInput.cs
new file mode 100644
index 0000000..76c88d2
--- /dev/null
+++ b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Reports/GenerateModerationReport/GenerateModerationReportInput.cs
@@ -0,0 +1,6 @@
+namespace Trax.Samples.ContentShield.Trains.Reports.GenerateModerationReport;
+
+public record GenerateModerationReportInput
+{
+ public required string ReportPeriod { get; init; }
+}
diff --git a/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Reports/GenerateModerationReport/GenerateModerationReportOutput.cs b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Reports/GenerateModerationReport/GenerateModerationReportOutput.cs
new file mode 100644
index 0000000..131f29d
--- /dev/null
+++ b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Reports/GenerateModerationReport/GenerateModerationReportOutput.cs
@@ -0,0 +1,9 @@
+namespace Trax.Samples.ContentShield.Trains.Reports.GenerateModerationReport;
+
+public record GenerateModerationReportOutput
+{
+ public int TotalReviewed { get; init; }
+ public int TotalFlagged { get; init; }
+ public required string[] TopViolationTypes { get; init; }
+ public double FalsePositiveRate { get; init; }
+}
diff --git a/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Reports/GenerateModerationReport/GenerateModerationReportTrain.cs b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Reports/GenerateModerationReport/GenerateModerationReportTrain.cs
new file mode 100644
index 0000000..6d6371c
--- /dev/null
+++ b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Reports/GenerateModerationReport/GenerateModerationReportTrain.cs
@@ -0,0 +1,24 @@
+using LanguageExt;
+using Trax.Effect.Attributes;
+using Trax.Effect.Services.ServiceTrain;
+using Trax.Samples.ContentShield.Trains.Reports.GenerateModerationReport.Steps;
+
+namespace Trax.Samples.ContentShield.Trains.Reports.GenerateModerationReport;
+
+///
+/// Generates a summary report of moderation activity for the specified period.
+/// Scheduled daily at midnight. Can also be run on-demand via GraphQL.
+///
+[TraxMutation(
+ Operations = GraphQLOperation.RunAndQueue,
+ Description = "Generates a moderation activity report"
+)]
+[TraxBroadcast]
+public class GenerateModerationReportTrain
+ : ServiceTrain,
+ IGenerateModerationReportTrain
+{
+ protected override async Task> RunInternal(
+ GenerateModerationReportInput input
+ ) => Activate(input).Chain().Chain().Resolve();
+}
diff --git a/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Reports/GenerateModerationReport/IGenerateModerationReportTrain.cs b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Reports/GenerateModerationReport/IGenerateModerationReportTrain.cs
new file mode 100644
index 0000000..1e907df
--- /dev/null
+++ b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Reports/GenerateModerationReport/IGenerateModerationReportTrain.cs
@@ -0,0 +1,6 @@
+using Trax.Effect.Services.ServiceTrain;
+
+namespace Trax.Samples.ContentShield.Trains.Reports.GenerateModerationReport;
+
+public interface IGenerateModerationReportTrain
+ : IServiceTrain;
diff --git a/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Reports/GenerateModerationReport/Steps/AggregateMetricsStep.cs b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Reports/GenerateModerationReport/Steps/AggregateMetricsStep.cs
new file mode 100644
index 0000000..97981ff
--- /dev/null
+++ b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Reports/GenerateModerationReport/Steps/AggregateMetricsStep.cs
@@ -0,0 +1,27 @@
+using Microsoft.Extensions.Logging;
+using Trax.Core.Step;
+
+namespace Trax.Samples.ContentShield.Trains.Reports.GenerateModerationReport.Steps;
+
+///
+/// Aggregates moderation metrics from the database for the requested period.
+///
+public class AggregateMetricsStep(ILogger logger)
+ : Step
+{
+ public override async Task Run(
+ GenerateModerationReportInput input
+ )
+ {
+ logger.LogInformation(
+ "Aggregating moderation metrics for period: {ReportPeriod}",
+ input.ReportPeriod
+ );
+
+ await Task.Delay(250);
+
+ logger.LogInformation("Metrics aggregated: 1,247 items reviewed, 83 flagged");
+
+ return input;
+ }
+}
diff --git a/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Reports/GenerateModerationReport/Steps/FormatReportStep.cs b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Reports/GenerateModerationReport/Steps/FormatReportStep.cs
new file mode 100644
index 0000000..03fe3f8
--- /dev/null
+++ b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trains/Reports/GenerateModerationReport/Steps/FormatReportStep.cs
@@ -0,0 +1,28 @@
+using Microsoft.Extensions.Logging;
+using Trax.Core.Step;
+
+namespace Trax.Samples.ContentShield.Trains.Reports.GenerateModerationReport.Steps;
+
+///
+/// Formats the aggregated metrics into a structured report output.
+///
+public class FormatReportStep(ILogger logger)
+ : Step
+{
+ public override async Task Run(
+ GenerateModerationReportInput input
+ )
+ {
+ logger.LogInformation("Formatting {ReportPeriod} moderation report", input.ReportPeriod);
+
+ await Task.Delay(100);
+
+ return new GenerateModerationReportOutput
+ {
+ TotalReviewed = 1247,
+ TotalFlagged = 83,
+ TopViolationTypes = ["violence", "spam", "hate-speech"],
+ FalsePositiveRate = 0.042,
+ };
+ }
+}
diff --git a/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trax.Samples.ContentShield.csproj b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trax.Samples.ContentShield.csproj
new file mode 100644
index 0000000..275b4bc
--- /dev/null
+++ b/samples/EphemeralWorkers/Trax.Samples.ContentShield/Trax.Samples.ContentShield.csproj
@@ -0,0 +1,18 @@
+
+
+ net10.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/LocalWorkers/Trax.Samples.GameServer.Scheduler/Program.cs b/samples/LocalWorkers/Trax.Samples.GameServer.Scheduler/Program.cs
index ab6be24..770103c 100644
--- a/samples/LocalWorkers/Trax.Samples.GameServer.Scheduler/Program.cs
+++ b/samples/LocalWorkers/Trax.Samples.GameServer.Scheduler/Program.cs
@@ -39,7 +39,6 @@
using Trax.Scheduler.Configuration;
using Trax.Scheduler.Extensions;
using Trax.Scheduler.Services.Scheduling;
-using Trax.Scheduler.Trains.ManifestManager;
var builder = WebApplication.CreateBuilder(args);
@@ -58,7 +57,7 @@
.SaveTrainParameters()
.AddStepProgress()
)
- .AddMediator(typeof(ManifestNames).Assembly, typeof(ManifestManagerTrain).Assembly)
+ .AddMediator(typeof(ManifestNames).Assembly)
.AddScheduler(scheduler =>
{
// ── Global Configuration ────────────────────────────────────────
@@ -75,7 +74,6 @@
cleanup.AddTrainType();
cleanup.AddTrainType();
})
- .JobDispatcherPollingInterval(TimeSpan.FromSeconds(2))
.UseLocalWorkers();
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
diff --git a/templates/content/Trax.Samples.Scheduler/Program.cs b/templates/content/Trax.Samples.Scheduler/Program.cs
index 02f8c46..9409064 100644
--- a/templates/content/Trax.Samples.Scheduler/Program.cs
+++ b/templates/content/Trax.Samples.Scheduler/Program.cs
@@ -22,7 +22,6 @@
using Trax.Samples.Scheduler.Trains.HelloWorld;
using Trax.Scheduler.Extensions;
using Trax.Scheduler.Services.Scheduling;
-using Trax.Scheduler.Trains.ManifestManager;
var builder = WebApplication.CreateBuilder(args);
@@ -40,7 +39,7 @@
trax.AddEffects(effects =>
effects.UsePostgres(connectionString).AddJson().SaveTrainParameters().AddStepProgress()
)
- .AddMediator(typeof(Program).Assembly, typeof(ManifestManagerTrain).Assembly)
+ .AddMediator(typeof(Program).Assembly)
.AddScheduler(scheduler =>
{
scheduler.UseLocalWorkers();