Orphan Manifest Cleanup
When you remove a schedule definition from your startup configuration (e.g., delete a .Schedule(...) call from Program.cs), the scheduler automatically deletes the corresponding manifest and all its related data from the database on the next startup. This prevents stale manifests from continuing to fire after their code has been removed.
How It Works
┌──────────────────────────────────────────────────────────────────┐
│ SchedulerStartupService (IHostedService) │
│ │
│ 1. Seed all PendingManifests (upsert) │
│ 2. Collect all configured ExternalIds │
│ 3. Query DB for this app's manifests NOT in configured set │
│ 4. Delete orphaned manifests + related data │
│ 5. Clean up orphaned ManifestGroups │
└──────────────────────────────────────────────────────────────────┘At startup, after seeding all configured manifests via upsert, the scheduler compares the set of ExternalIds defined in code against the manifests in the database that this application owns. Any of those whose ExternalId is not in the configured set is considered orphaned and is deleted along with its:
- WorkQueue entries (pending dispatches)
- DeadLetter records (failed executions)
- Metadata records of finished runs (execution history)
What the prune leaves alone:
| Case | What happens |
|---|---|
| A manifest another application owns, or one with no owner | Never deleted by this host's prune (see Ownership). |
| The host's application name cannot be found | Nothing is pruned, and a warning says why. |
| The host declares no manifests | Nothing is pruned, and the log says why. An API or worker host that calls AddScheduler only to reach ITraxScheduler or the operations service has no basis for calling another host's manifests orphaned. |
An orphan has a Pending or InProgress run | The manifest and the run are kept. The next start prunes the manifest once the run has finished. A prune never deletes an unfinished run. |
| A run of the orphan started a nested train | The nested run is kept, with its ParentId cleared, as metadata cleanup does. |
| Another manifest depends on the orphan | Its DependsOnManifestId is set to null. |
Orphan pruning deletes manifests in batches (500 per batch) to keep SQL IN(...) clauses small and avoid command timeouts on large prune operations. Each batch runs in one transaction: it clears the references above, then deletes WorkQueues, DeadLetters, Metadata, and finally the manifests. A batch that fails rolls back, is logged, and the prune moves on to the next one. A failure of the prune as a whole is logged too, and the host starts anyway: pruning is housekeeping.
After manifest pruning, any ManifestGroup with no remaining manifests is also deleted.
Ownership
Every manifest the scheduler writes, whether seeded from the builder or created through ITraxScheduler at runtime, records the application that wrote it in Manifest.Owner: the host environment's IHostEnvironment.ApplicationName, or the entry assembly's name when no IHostEnvironment is registered. The prune considers only manifests whose owner is this application's name, so several applications can schedule against one database without deleting each other's manifests. Hosts of one application (instances behind a load balancer, say) share the name, so they should declare the same schedules.
A manifest with no owner is never deleted by the prune. Every manifest written by an earlier Trax version starts out that way, and gets its owner the next time the application that declares it seeds it. One the application no longer declares is therefore never seeded again and never pruned: delete such manifests by hand, from the dashboard or the database. When no application name can be found, the prune deletes nothing and logs a warning.
Configuration
Orphan manifest cleanup is enabled by default. No additional configuration is needed, simply remove a schedule definition from your code and restart the application.
Disabling Cleanup
If you create manifests dynamically at runtime via ITraxScheduler (outside of the startup configuration), disable orphan pruning to prevent those manifests from being deleted on restart:
services.AddTrax(trax => trax
.AddScheduler(scheduler => scheduler
.PruneOrphanedManifests(false) // Disable orphan cleanup
.Schedule<IMyTrain>(
"my-job",
new MyInput(),
Every.Minutes(5))
)
);Examples
Removing a Single Schedule
// Before: two schedules defined
scheduler
.Schedule<IHelloWorldTrain>(
"hello-world",
new HelloWorldInput { Name = "Scheduler" },
Every.Seconds(20))
.Schedule<IGoodbyeWorldTrain>(
"goodbye-world",
new GoodbyeWorldInput { Name = "Scheduler" },
Every.Minutes(1));
// After: "goodbye-world" removed from code
scheduler
.Schedule<IHelloWorldTrain>(
"hello-world",
new HelloWorldInput { Name = "Scheduler" },
Every.Seconds(20));
// On next startup:
// - "hello-world" is upserted (no change)
// - "goodbye-world" is deleted from the databaseRemoving All Schedules
// Before: schedules defined
scheduler
.Schedule<IMyTrain>("my-job", new MyInput(), Every.Minutes(5));
// After: all schedules removed
services.AddTrax(trax => trax
.AddScheduler()
);
// On next startup:
// - No manifests are seeded
// - Nothing is pruned: a host that declares no manifests leaves the table aloneTo remove every manifest, delete them from the dashboard or the database. The scheduler will not treat "this host declares nothing" as "delete everything".
Interaction with ScheduleMany PrunePrefix
Orphan manifest cleanup and ScheduleMany's PrunePrefix are complementary:
- PrunePrefix operates within a single
ScheduleManybatch during seeding, removing items that were in a previous deployment but not in the current batch. It matches by external ID prefix (and, for a named batch, group) only; it does not check a manifest's owner. It runs in a separate database context after the main seeding transaction commits, so a prune failure does not roll back the upserted manifests. A named batch prunes only within its own group. It follows the same rules as the orphan prune for unfinished runs, nested runs and transactions. - Orphan manifest cleanup operates after all seeding is complete, removing any of this application's manifests not in the configured set, including entire
Scheduledefinitions that were removed.
Both features compose correctly. PrunePrefix may delete some manifests during seeding, and orphan cleanup catches any remaining orphans afterward.
Remarks
- Orphan pruning runs once at startup as part of
SchedulerStartupService, before the polling services begin. It does not run continuously. - Both single manifests (
.Schedule(...)) and batch manifests (.ScheduleMany(...)) are tracked. The scheduler knows the full set of ExternalIds that each builder call will create, including all items in a batch. - Deletion follows FK-safe ordering: self-referencing
DependsOnManifestIdis cleared first, then WorkQueue, DeadLetter, and Metadata records, and finally the manifest itself. - When a host declares no schedules (empty configuration), or its application name cannot be found, nothing is pruned.