Skip to content

Scheduled Jobs ​

Scheduled Jobs are the realm's recurring background tasks — garbage collection, retention sweeps, periodic housekeeping. Each job ships with a sensible default schedule baked into the build; admins can override the cron expression, tweak per-job parameters, disable scheduled runs (manual runs remain available), trigger an out-of-band run, or read the last 50 executions per job — all from one page.

Surface ​

SurfacePathRequired permission
List + grid/admin/scheduled-jobsscheduled-job:read
Detail modal (Schedule / Configuration / History)/admin/scheduled-jobs#<job-key>scheduled-job:read to view, scheduled-job:write to save / trigger

The realm:admin role bypasses both; granular delegation works by handing out scheduled-job:read and/or scheduled-job:write from the modgud App catalog.

Per-tenant

Every realm job has its own Quartz job + trigger. Run history (JobRunHistoryEntry) and per-job overrides (JobConfig) live in the owning realm's Marten DB. Changing or manually starting a job affects that realm only.

Registered jobs ​

Fourteen job definitions ship with Modgud today:

  • Twelve are realm jobs. Each active realm gets an independent Quartz job and trigger, so one customer can run at 18:00, another at 21:00, and another can disable its cron and run manually.
  • Two are system jobs: system-job-run-history-retention and platform-audit-prune. Each exists exactly once because it operates on a deployment-wide store, and is visible/configurable only in the realm that currently holds the Control-Plane role.

The Control-Plane realm is still a realm, so it also owns its own copies of all twelve realm jobs.

System-job configuration and history live in the non-tenanted global store, not in the Control-Plane realm's database. Transferring the Control-Plane role therefore moves visibility and authority, but not the system job's data or schedule.

inbox-retention — Inbox Retention ​

Applies this realm's per-kind inbox retention policy.

  • Default cron: 0 0 3 * * ? (03:00 UTC daily)
  • Parameters: none — retention rules are configured separately under Inbox Settings.
  • What it does: loads the owning realm's InboxRetentionSettings doc, dismisses or hard-deletes items per the configured policy, and reports per-reason counts in the run summary.
  • On failure: the failure is written to that realm's history and an inbox notification fires there (see Failure notification).

job-run-history-retention — Job-Run-History Retention ​

Trims the per-tenant JobRunHistoryEntry document table so it doesn't grow unbounded.

  • Default cron: 0 30 3 * * ? (03:30 UTC daily)
  • Parameters:
    • Max. age in days — runs older than this are deleted. Default 30. Leave blank to disable the age sweep.
    • Max. entries per job — keep only the N newest entries per job key. Default unlimited.
  • What it does: two independent passes in this realm (age cutoff + per-key count cap), summed and reported.
  • On failure: logged + inbox-notified.

Two independent caps

The age sweep and the per-job count cap run independently. Use one, the other, or both. Both blank = the job runs and deletes nothing.

dcr-gc — DCR Garbage Collector ​

Soft-deletes Dynamic Client Registration clients whose modgud:dcr:last_used_at has aged past the realm's configured TTL.

  • Default cron: 0 0 4 * * ? (04:00 UTC daily — after the two retention jobs)
  • Parameters: none — TTL lives on Realm Settings → Dynamic Client Registration (GcTtlDays, default 90).
  • What it does: when DCR is enabled in this realm, finds DCR-registered clients whose last-used timestamp is older than now − GcTtlDays and soft-deletes them via the OAuth application aggregate. A realm with DCR disabled is skipped after a single indexed lookup.
  • On failure: logged + inbox-notified. Soft delete means client_id history stays intact for forensics.

pending-registration-sweep — Pending registration sweep ​

Hard-deletes expired pending registrations — sign-ups (web, native OTP, invite code) whose verification link or code was never redeemed — and prunes rate-limit counters idle for two days.

  • Default cron: 0 10 * * * ? (ten past every hour)
  • Parameters: none — lifetimes are fixed (10 minutes for codes, 24 hours for links).
  • What it does: deletes every pending registration past its expiry (and any consumed record a crash left behind), drops rate-limit counters nobody touched for two days, and forgets trusted-device records nobody logged in from for 90 days. These records are plain documents, not users: after the sweep nothing identifying the person remains. See Realm Settings → Self-Registration.
  • On failure: logged + inbox-notified; the next hourly run catches up.

unconfirmed-registration-reaper — Unconfirmed registration reaper ​

Erases the "ghost" accounts the pre-ADR-0018 sign-up paths created before the proof: passwordless users whose registration code was never redeemed.

  • Default cron: 0 30 4 * * ? (04:30 UTC daily)
  • Parameters: dryRun (default true — only logs the candidates), olderThanDays (default 7).
  • What it does: matches accounts that are unconfirmed, have no password, no passkey, no external login, a sign-up code that was issued and never redeemed (an account no code was ever sent to is not a ghost), and whose stream is older than olderThanDays; erases them through the normal permanent-erase path (masking + archiving), never a raw delete. Anything an admin created with a password, or that ever signed in, is outside the signature. Leave it in dry-run until the logged list looks right, then set dryRun=false.
  • On failure: logged + inbox-notified.

staffing-sweep — Position Staffing Sweep ​

Cleans up after position terminals: deletes staffing ceremonies that were begun but never redeemed, and ends staffing sessions that ran past their absolute end — the shift ceiling a refresh can never move.

  • Default cron: 0 */5 * * * ? (every 5 minutes — a silent terminal's session outlives its shift ceiling by at most one sweep interval)
  • Parameters: none.
  • What it does: when the PositionTerminals feature flag is off, the run is skipped and says so in its result. Otherwise, in its owning realm, it deletes every staffing ceremony past its expiry, then ends each active staffing session whose absolute end has passed, with the reason Expired, the same way a force-lock does: the terminal's tokens are revoked and the end is written to the security log. A terminal that is still in contact is already caught by its next refresh; the sweep catches terminals that went silent. Ended sessions are kept as audit records, not deleted. The run reports how many sessions it ended. Idempotent.
  • On failure: that realm's run fails and is written to its own history; no other realm's run is affected, and the next run five minutes later picks up whatever was left.

signing-key-janitor — Signing Key Janitor ​

Hard-deletes per-realm OAuth/OIDC signing keys whose rotation overlap window has elapsed.

  • Default cron: 0 0 5 * * ? (05:00 UTC daily — after the GC + retention jobs)
  • Parameters: none — the overlap window is a fixed 30 days.
  • What it does: in its owning realm, deletes signing keys where RetiredAt + 30 days < now, and every revoked key (see Revoking a key). Active keys and unrevoked keys still inside their overlap window are left untouched. This is the one realm job whose trigger remains scheduled while a realm is deactivated, because soft-delete retains that realm's database and private key material. See Realm Settings → Signing Keys for the rotation that produces these retired keys.
  • On failure: logged + inbox-notified.

account-lifecycle-sweep — Account Lifecycle Sweep ​

Drives this realm's account-deletion deadlines: sends "about to be deleted" reminders, erases self-service deletion requests whose grace period has passed, and auto-purges admin recycle-bin users past their retention deadline (when auto-purge is enabled for the realm). Also prunes used/expired registration invite codes as a hygiene side effect.

  • Default cron: 0 30 3 * * ? (03:30 UTC daily)
  • Parameters: none — deadlines and lead times come from Realm Settings → Account Deletion.
  • What it does: runs the self-service reminder/erasure sweep, the admin recycle-bin auto-purge sweep, and the invite-code prune in the owning realm, then reports counts for each. See Users → recycle bin & permanent erase for the lifecycle this job enforces.
  • On failure: that realm's run fails and is written to its own history; no other realm's run is affected.

backchannel-logout-retry — Back-channel logout retry ​

Retries logout-token deliveries to relying parties whose logout URI did not accept the first, immediate attempt (see logout propagation).

  • Default cron: 0 * * * * ? (every minute)
  • Parameters: none — the schedule is fixed: about 1, 5 and 30 minutes after the previous failure, then the delivery is given up.
  • What it does: sweeps the realm's pending deliveries whose next attempt is due, mints a fresh logout token for each and POSTs it. A delivered or given-up row is removed; every attempt is a security-log entry and updates the client's "last delivery" status. An attempt interrupted before it finished (shutdown, signing-key or database failure) is picked up again by a later sweep; a last attempt interrupted three times is given up with the outcome failed:interrupted.
  • On failure: that realm's run fails and is written to its own history; no other realm's run is affected.

session-prune — Session Prune ​

Removes expired browser/SSO and native OAuth client-session documents from this realm.

  • Default cron: 0 15 4 * * ? (04:15 UTC daily)
  • Parameters: none — expiry is determined from each session's idle and absolute lifetime.
  • What it does: deletes UserSession and ClientSession rows whose idle or absolute expiry has passed. Runtime cookie and refresh-token validation already rejects an expired row, so pruning is storage hygiene rather than the enforcement boundary. An expired session still ends its relying-party sessions: the sweep emits the end marker that drives back-channel logout with reason expired, and removes any session-grant row left without a session.
  • On failure: that realm's run fails and is written to its own history; no other realm's run is affected.

raw-claims-retention — Raw Claims Retention ​

Enforces each login provider's raw-claims retention (Aufbewahrung in Tagen on the provider, shown when Store raw claims is on).

  • Default cron: 0 45 3 * * ? (03:45 UTC daily)
  • Parameters: none — the retention is configured per login provider; a provider without one keeps its raw claims as long as the link exists.
  • What it does: for every provider with a retention, clears the last raw-claims snapshot of each external-identity link captured before now − days, and removes the raw claims from the expired login events of that provider's link streams (unlinked ones included). Email, display name and script output in those events stay; a GDPR erase still masks them. Expired snapshots are already withheld from the admin views before the job runs.
  • On failure: that realm's run fails and is written to its own history; no other realm's run is affected.

security-audit-prune — Security Audit Prune ​

Hard-deletes this realm's structured Security events after its configured retention window.

This is a realm job: every realm has its own trigger, configuration and run history.

  • Default cron: 0 0 2 * * ? (02:00 UTC daily)
  • Parameters: none on the job. Retention is configured under Realm settings → Logs (default 7 days, range 1–365).
  • What it does: deletes only expired RealmSecurityAuditEvent documents from the owning physical realm DB.
  • On failure: only that realm's run fails.

platform-audit-prune — Platform Audit Prune ​

Hard-deletes PII-free deployment events from the Global Store. This is a deployment-wide system job, visible only in the Control Plane.

  • Default cron: 0 15 2 * * ? (02:15 UTC daily)
  • Parameter: retentionDays (default 365, range 1–3650)
  • What it does: deletes expired PlatformAuditEvent documents only.

system-job-run-history-retention — System Job-Run-History Retention ​

Trims only the execution history of deployment-wide system jobs in the non-tenanted global store.

This is itself a deployment-wide system job: it appears only in the current Control-Plane realm and has only one Quartz trigger. It is deliberately separate from job-run-history-retention, because a realm-owned job must never read or mutate platform metadata.

  • Default cron: 0 45 3 * * ? (03:45 UTC daily)
  • Parameters:
    • Max. age in days — runs older than this are deleted. Default 30. Leave blank to disable the age sweep.
    • Max. entries per job — keep only the N newest entries per system-job key. Default unlimited.
  • What it does: applies the same two independent retention caps as the realm job, but exclusively inside the global store.
  • On failure: logged + inbox-notified through the current Control-Plane realm.

Job-detail modal ​

Double-click any row (or open /admin/scheduled-jobs#<job-key>) to get a three-tab modal.

TabWhat it shows
ScheduleCron expression input (placeholder shows the registration default), enabled toggle, Run now button, and the computed Next run timestamp.
ConfigurationOne field per JobParameterField declared by the job, grouped by Section when set. Empty value = fall back to the schema's Default. Tab is hidden for jobs with no tunable parameters — currently every job except the realm and system job-history-retention jobs.
HistoryLast 50 runs, newest first. Success runs show duration + optional one-line summary. Failed runs show the first-line error message and an expandable stack trace. Manual triggers carry a manual tag.

The modal's footer Save button persists Schedule + Configuration in one shot; the trigger button on the Schedule tab is independent.

Manual trigger ("Run now") ​

The Run now button on the Schedule tab fires the job off-schedule, immediately. Two things happen as a result:

  • A new history entry appears with ManualTrigger = true, surfaced in the History tab with a manual tag.
  • The triggering admin gets a ManualJobCompleted inbox item with the run summary or error message — handy when the job is slow and you don't want to babysit the modal.

The scheduled cron is unaffected — the job's next regular run still fires per its schedule.

Cron overrides ​

The cron field on the Schedule tab is a Quartz 7-field expression (sec min hour day-of-month month day-of-week year). When the field is empty the job uses the registration default; when set, the override is persisted and applied to the live scheduler immediately. Realm-job overrides live in that realm's Marten DB; system-job overrides live only in the non-tenanted global store.

The endpoint validates the expression server-side (CronExpression.IsValidExpression) and returns 400 with a clear error if it parses wrong — you won't see a runtime scheduler failure later.

Failure notification ​

When any run completes with an exception, a ScheduledJobFailed item drops into the inbox of every admin (the same recipient set as other admin notifications). The dedup key is derived from the job key, so repeated failures of the same job collapse onto one bell entry per admin — fix the root cause once, dismiss once, done.

The notification links straight to /admin/scheduled-jobs#<job-key> so the History tab is one click away.

See Inbox for the notification slice in general.

Permissions ​

PermissionWhat it grants
scheduled-job:readList all jobs, view a single job, fetch run history.
scheduled-job:writeSave schedule / parameter overrides, trigger a job manually. Implies :read is also needed to see anything.

Both are seeded in the modgud App permission catalog. realm:admin bypasses both per Modgud's standard 3-tier model.

Released under the Apache-2.0 License.