Background Jobs
The Background Jobs tab of the System page (/system/jobs) is the live and historical view of every scheduled job Tagnet runs. The current release covers the Retention engine; future categories (data exports, periodic health checks, scheduled reports) will appear here under the same framework, added as new entries in the Run job menu described below.
Every run, whether triggered by the cron scheduler or by an operator using Run job, produces a durable record on this page. The record includes who initiated it, when it started and finished, the structured outcome, and any error message. This is the authoritative record of whether a job ran and whether it worked.
Access
The Background Jobs page requires the Manage Policies permission, the same permission that governs editing on the Policies page, so anyone who can edit a policy can also see and manually trigger the jobs that enforce it and review their history.
Page layout
The tab has an action bar showing how many runs are active, with the Run job menu at its right. Below it are the active runs (while any are executing) and the run history, a single card with the search and filter bar across its top:
Active runs
Only visible when at least one job is currently executing. Each active run shows the job type and key, the current progress message, the trigger (Scheduled or Manual), how long it has been running, and a live progress bar. The panel polls the database every 3 seconds while the page is open. Click any active row to open its own detail page, which has its own Refresh button and keeps polling while the run is in flight.
Search and filters
The search box matches the job type or key (for example, retention). The filter button opens the filter panel. Type and Trigger (Scheduled, Manual, Event) narrow the grid to the job types and triggers you pick, Outcome to one or more of Running, Completed, Failed, Cancelled, and Started to runs that began between the From and To days (both included, in your time zone; leave either blank for an open range). Filters combine, so Trigger Manual with last week's dates shows last week's manual runs. Each active filter shows as a chip under the search box, and closing the chip removes it. The result count sits at the right of the bar. The search, filters and current page are remembered if you navigate away and return.
History grid
Server-side paged grid of past runs, newest first. Columns: started time (friendly), job type, key, trigger, outcome chip, and duration. Click any row to open the run's detail page at a stable, shareable URL (e.g., /background-jobs/1234).
The rows-per-page selector at the bottom of the grid is shared with other paged grids in the system. Change it once and it persists across the application.
Run detail page
Opens when you click an active or historical row. Shows:
- Header: job type / key with an outcome chip
- Trigger and Initiated By (the user who ran it manually via Run job, or
system (scheduled)for cron-driven runs) - Started, Completed, Duration
- Correlation ID for cross-referencing application logs
- Progress (when still running): percent and current message
- Error (when Failed): the exception message that ended the run
- Retention results (for retention runs): total rows deleted, VACUUM status with a skip reason if it did not run, and a table with one row per data set each policy maintains: the policy, the data (table or payload columns), the operation (Delete removes rows; Truncate clears payload columns and keeps the row, so its counts are not part of the total), how long the data is kept, and how many rows were affected. Rows that changed data are bold; zero counts are dimmed. A policy that was skipped (disabled or missing) shows one row with the reason. Runs recorded before this format was introduced show "Legacy result format; see the raw JSON for details." in place of their rows.
Other job types display their structured output as pretty-printed JSON until they get rich rendering of their own.
Triggering and cancelling runs
Trigger a run using the Run job menu in the bar under the tab strip. It's the central place to run any manually-triggerable job: the current release lists Retention, which enforces every enabled retention policy (see Policies) in one pass, so triggering it here rather than from an individual policy correctly conveys its scope. Future job categories add their own entries to the same menu. Manual and scheduled runs share the same code path, the same skip-if-active rule, and the same history; the menu item is disabled while a run of that job is already active.
Skip-if-active. If a run of the same job is already pending or in flight, a second trigger is rejected immediately rather than queued. This applies symmetrically to manual and scheduled triggers: a slow scheduled run cannot accumulate a backlog of duplicates, and an operator opening Run job twice quickly only enqueues once.
Cancel a run is not yet exposed through the UI. If a run hangs, the host process restart will mark it as Failed with "Run was interrupted by host shutdown or crash; recovered on next startup." so it stops blocking subsequent runs.
Schedule and configuration
Schedules are configured in appsettings.json under the Scheduling section and can be overridden per-deployment via appsettings.Production.json written by the WPF admin console. A typical entry:
"Scheduling": {
"Jobs": {
"Retention.All": {
"Cron": "0 2 * * *",
"TimeZone": "UTC"
}
},
"JobRunHistoryRetentionDays": 90,
"DisableAllScheduledJobs": false
}
| Setting | Meaning |
|---|---|
Jobs.<key>.Cron | Standard 5-field cron expression. Example: "0 2 * * *" = every day at 02:00. |
Jobs.<key>.TimeZone | IANA (e.g., America/New_York) or Windows (Eastern Standard Time) time zone id. Defaults to UTC when omitted. |
JobRunHistoryRetentionDays | How long to keep run rows. The scheduler trims older rows once a day. Default 90. |
DisableAllScheduledJobs | Killswitch for cron-fired runs. Manual triggers are unaffected. |
A misconfigured cron expression or unknown time zone is logged once at warning level and the affected job is skipped until the value is corrected. Configuration changes are picked up at runtime within ~60 seconds without a service restart.
Misfire behavior
If the host is offline when a scheduled run was due (overnight reboot, host crash, deployment), the retention job uses run-on-recovery: on the next scheduler tick after startup, it computes the next occurrence relative to the last successful run and fires immediately if that occurrence is already in the past. A missed retention slot is owed because the cost is real (more data on disk than the policy promises). Future jobs that prefer "just wait for the next slot" can opt for skip-and-wait at registration time.
SOC 2 use
Background job history complements the Security Events log. A policy edit produces a PolicyChanged security event; the next retention run produces a Background Job Run row. Together they answer "the policy was changed on X; did the system actually act on the new value, and when?"
The run history table is auto-trimmed (JobRunHistoryRetentionDays, default 90 days). For longer retention, increase that setting in configuration.