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:

CaseWhat happens
A manifest another application owns, or one with no ownerNever deleted by this host's prune (see Ownership).
The host's application name cannot be foundNothing is pruned, and a warning says why.
The host declares no manifestsNothing 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 runThe 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 trainThe nested run is kept, with its ParentId cleared, as metadata cleanup does.
Another manifest depends on the orphanIts 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 database

Removing 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 alone

To 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 ScheduleMany batch 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 Schedule definitions 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 DependsOnManifestId is 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.

SDK Reference

PruneOrphanedManifests | Schedule