Repository navigation
Background Job Health Design
Status: proposal - nothing here is built yet. This page sets out what I plan to build for issue #182, and why, written before any code so the approach can be checked first. Comments on the issue are welcome. The open decisions are in section 7. It is designed alongside #181, and section 4 says how the two fit.
The request, from issue #182 (raised by @Dr4inder):
"A scheduler outage can otherwise leave the application looking quiet while expected work, notifications or synchronisation never occur."
Several parts of FreeITSM rely on background jobs that the server runs on a timer - a cron entry on Linux, a scheduled task on Windows: sending SLA warnings, delivering webhooks, creating repeating tasks, syncing calendars and directories, checking domains. If the timer stops - a server rebuild, a moved install, a Docker container with no scheduler - nothing tells you. No error appears; things just quietly stop happening. The first sign is usually somebody asking why an SLA warning never came.
The request is for one place that shows every background job: which ones this install needs, when each last worked, whether any is overdue, failing or stuck - plus a health address an outside monitoring tool can check, so an alarm can go off without relying on FreeITSM itself.
- FreeITSM does not become a scheduler. The jobs keep running exactly as they do now - from cron, Windows Task Scheduler, or a monitoring tool calling their web address. This work only records each run and reads those records.
- "Last success" means the work finished. A job that starts and crashes, is turned away by its lock or minimum interval, fails its token check, or is a dry run, never refreshes it. A run that finds nothing to do is a success.
- Health is worked out when you look. Whether a job is overdue is calculated at the moment the page or the health address is opened - so a stopped job shows as overdue because it has stopped, not because it ran again to say so.
- Honest about what it cannot see. FreeITSM cannot inspect the server's crontab. A job with no recorded run is "never seen" - not "the cron entry is missing", which it cannot know.
Mapped from the code (3.9.0). There are 13 background jobs and no shared way of recording them:
| Job | What depends on it | What it records about its own runs |
|---|---|---|
| SLA breach check | SLA warning and breach emails | β
A proper run log (sla_cron_runs): start, finish, outcome, counts |
| Webhook deliveries | Outbound webhooks | |
| Scheduled workflows | Contract and warranty expiry workflows, planned maintenance, project alerts and digests | |
| Integration poll | Jira / tracker status and comments | |
| Repeating tasks | Tasks on a fixed schedule | |
| Attachment reading | Searching inside attachments | |
| Calendar sync pull | Changes made in analysts' calendars | β Nothing at job level |
| Domains | Lookups, checks, alerts | |
| LMS reminders | Training reminder emails | |
| Proxmox sync | Proxmox inventory | |
| VMware Cloud Director sync | vCloud inventory | |
| Documents maintenance | Document search, file tidy-up | β Exit code only |
| Directory sync | LDAP / AD / CardDAV people | β
A run log per provider (directory_sync_runs) |
The useful pieces that already exist - the SLA run log, the directory-sync per-provider log, the webhook page's running / stale / down judgement, the per-server sync status columns - are the model for what follows. Reporting β System logs describes itself as covering "background processes, scheduled tasks", but no job writes to it yet.
Docker: the image runs no scheduler at all, so on a fresh Docker install every job is "never seen" until the host is set up to run them - worth saying plainly on the page and in the docs.
Not a background job: fetching email into tickets is done by the browser while the ticket list is open, not by a scheduled task. It has its own mailbox health warnings already, and it stays outside this work (section 8).
A new job_runs table and a small shared helper that every job calls at the start and the end of its work. One row per run:
| Column | Holds |
|---|---|
| job | which job (a fixed key such as task_recurrence) |
| trigger | cron (command line), web (its token address), page (run in passing by a page load) or manual (Run now) |
| started / finished | UTC times, and the duration |
| outcome | ok, partial (some sources failed), failed, skipped (lock, minimum interval or token refused), or running while it works |
| counts | what it did - "12 sent, 0 failed", "3 occurrences created" |
| summary | a short message written by the job, never a raw error that could carry a password or token |
Jobs that work through several sources - tracker connections, directory providers, analysts' calendars, Proxmox and vCloud servers, domains - also write one source result each (job_run_sources: which source, ok or failed, a short message). That is the issue's point that "partial failures should remain visible rather than being hidden by an overall successful invocation": one failing directory provider makes the run partial, named, even though the other three worked.
The SLA job's existing log becomes the first user of the shared recorder; its own table stays for its own screen. The existing "last run" settings stay too, so nothing that reads them breaks - but health no longer reads them, because five of them are written before the work starts.
Retention: ok, partial and failed runs are kept for 13 months by default, long enough for a yearly audit and for #181's report (section 4); skipped runs, which can arrive every minute, for 30 days.
A job registry - one file, like the currency list - naming every job with:
- what depends on it, in plain words;
- when it is needed: the same test the job itself uses - repeating tasks only when a series runs on a fixed schedule, the tracker poll only when a connection is active, LMS reminders only when they are switched on;
- its expected interval (from the docs: SLA 5 minutes, webhooks 1 minute, repeating tasks daily...) and a grace period;
- its longest normal run, beyond which a run still marked running is stuck.
An administrator can change a job's expected interval, because installs schedule differently, and can mark a job not used here on purpose.
Each job's health is then worked out on reading:
| Health | Means |
|---|---|
| Not needed | The feature it serves is not in use on this install |
| Not used here | An administrator has said so |
| Never seen | Needed, but no run has ever been recorded |
| Healthy | Last success within its interval plus grace |
| Overdue | Last success older than that |
| Failing | Its most recent run failed (the last success is still shown) |
| Partial | Its most recent run worked for some sources and not others, which are named |
| Stuck | A run started longer ago than its longest normal run and never finished |
Page loads are not proof of a schedule. Domains, LMS reminders and project alerts also run in passing when somebody opens their pages, which can keep a job looking alive while its cron entry has gone. So a job whose recent runs were all page or manual shows "Healthy - but only page loads have run it; no scheduled run seen since {date}".
Reporting β System logs β Background jobs - a third tab beside User logins and Email imports, where the logs already live.
- At the top, the health of every job: name, what depends on it, health, last success, last run and its outcome, expected interval, and how it was triggered. Unhealthy jobs first.
- Below, the run history: every run, filterable by job, outcome and period, each opening to its counts, message and source results.
- For "never seen" and "overdue", a short note on what to check - with a link to Scheduled tasks, which says how to set each one up on Linux, Windows and Docker.
Who can open the tab is a decision for section 7.
A log page only helps somebody who looks at it, and the point of the issue is that a stopped job is silent. So:
- a Watchtower card, shown only while a job is unhealthy, naming the jobs and what they affect;
- a notice on the System page for administrators, in the same way as the other System warnings.
Both are worked out when the page is opened, so neither depends on the scheduler being alive.
GET /api/v1/jobs/health on the REST API, using an API key that holds a new "Background job health - read" permission. It returns, for every job: its health, last success, last run and outcome, expected interval, and the summary and failing sources - and an overall status.
- It never runs a job and never returns passwords, tokens or connection details.
-
For simple uptime monitors,
?strict=1answers HTTP 503 whenever anything needed is unhealthy, and 200 otherwise - so a tool that only looks at status codes still raises the alarm. - If FreeITSM or its database is down, the address fails - an error, never a cheerful "healthy" - which is itself the alarm (acceptance criterion 6).
- Job health is install-wide, so a key that can see only some companies is not enough: the key's analyst must be an administrator or reach every company.
These would make the health page wrong, so they come first:
- Five jobs write "last run" before doing any work (webhooks, scheduled workflows, the tracker poll, repeating tasks, attachment reading) - so a crashed or empty run looks healthy. The recorder replaces this for health.
- The scheduled workflows job swallows failures in its parts (planned maintenance, project alerts, digests) and still says OK. Each part becomes a source result.
- The SLA job records a run that sent some emails but hit errors as plain success. It becomes partial.
- LMS reminders can never be run by web address: its token is never created, and is not read back correctly. Fixed, and added to the token tool.
- Tracker poll errors are written to every linked ticket rather than to the connection, and System β Integrations does not show when a connection last polled.
- Calendar sync's health treats "every pull is failing" the same as "never run", and warns on installs that only use subscription links.
- Two warnings say mail is collected by a scheduled task, which does not exist.
- Calendar sync, Proxmox and vCloud jobs have no protection against overlapping runs.
- The docs disagree on how many jobs there are ("nine", "four") and on intervals; Scheduled tasks is missing four jobs and has no Docker section.
- Jobs that fail exit with success (webhooks, workflows, tracker poll), so a scheduler cannot tell either. They will exit non-zero.
The issue makes the point itself: "a healthy scheduler does not prove that a control was completed, and a completed task does not prove that the scheduler is healthy." The two designs are built to answer each other:
- #181's report can say why. It shows a repeating task's occurrence as Missing or Created late. With #182's run history for the repeating-tasks job, it can add the cause: "Missing - the repeating-tasks job did not run between 2 and 9 March", or "Created late - caught up on 9 March after the job was overdue". That is the auditor's next question, answered on the same page.
- #181's history links to the run. When the scheduler creates an occurrence, #181's task history records created by the repeat - on schedule / catch-up; it now also records which job run created it.
- Retention is set for audits. Run history is kept for 13 months so a yearly evidence report can still read it.
#181's design has been updated with these three points. Build order: #182's recorder (Part A) should land before #181's report, so the report has run history to read.
| # | Criterion | Where |
|---|---|---|
| 1 | Identify the required jobs, their cadence and last successful processing | Parts B, C |
| 2 | A stopped repeating-task scheduler turns unhealthy after its interval and grace, with no new record | Part B (worked out on reading) |
| 3 | Never seen, overdue, failed, partial, stuck and intentionally inactive are distinguishable | Part B |
| 4 | Skipped and failed runs keep the previous success; empty successful runs count | Parts A, B |
| 5 | Diagnostics name the job or source without secrets | Part A (summaries written by the job, source results) |
| 6 | An outside monitor can detect unhealthy jobs on its own | Part E |
| 7 | Existing cron, command-line and web paths keep working; no second scheduler, no duplicate runs | Section 1, Part A |
- Record - the recorder and registry (Parts A, B) and the fixes (Part F). Every job starts writing proper history from this release.
- See - the Background jobs tab (Part C), the Watchtower card and System notice (Part D).
- Monitor - the health address and its permission (Part E).
Phase 1 also unblocks #181's report (section 4).
| # | Decision | My recommendation |
|---|---|---|
| 1 | Where the page lives | Reporting β System logs β Background jobs, beside the existing log tabs - with the Watchtower card and System notice so nobody has to go looking. |
| 2 | Who can open it | A new permission, "View background job health", rather than everyone with Reporting: job messages name mailboxes, directories and servers. It also fits #181's Auditor role. |
| 3 | Expected intervals | Built-in defaults from the docs, changeable per job by an administrator, plus not used here. |
| 4 | Telling somebody by email or the bell when a job turns unhealthy | Not in the first version. Sending that message would itself need a background job - the very thing that may have stopped. The health address and an outside monitor are the reliable alarm; Watchtower and the System notice cover people already in FreeITSM. |
| 5 | Retention | 13 months for real runs, 30 days for skipped ones - long enough for a yearly audit. |
| 6 | Docker | Document the host's cron for Docker installs in Scheduled tasks. A scheduler inside the container could come later, as its own change. |
- No second scheduler and no new way to run jobs - the existing Run now buttons stay as they are.
- No monitoring service, database or job framework - the issue asks for none, and outside tools consume the health address.
- Email fetching stays browser-driven with its existing mailbox health warnings; it is not a background job today.
- Nothing here proves that the work a job should have produced was done - that is #181's job, for repeating tasks.
- Scheduled tasks - how each job is set up
- Recurring task evidence - design (#181)
- Webhooks Β· REST API
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
-
Architecture
- β³ π§Ή Root folder tidy (URL routing)
- β³ π Database Integrity (FKs & constraints)
- β³ π οΈ Database Verification β Developer Guide
- β³ π§© Service Layer (UI + API share code)
- β³ πͺ€ Code traps (the TRAP: comments)
- β³ π Commenting for the Engineering wiki
- β³ π οΈ Engineering wiki scanner - Developer Guide
- β³ π Service-Layer progress
- π§ͺ Developer tests
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- π Date & Time Formats
- π± Currencies
- π§ The sidebar (optional module rail)
- π Display order (modules, Watchtower cards) β Developer Guide
- Theming & Dark Mode
- ποΈ Recent β getting back to what you were doing
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
-
MobileβFriendly
- β³ π« Mobile: Tickets
- β³ π» Mobile: Assets
- β³ π Mobile: Calendar
- β³ π Mobile: Knowledge
- β³ π¦ Mobile: Service Status
- β³ πΌ Mobile: Watchtower
- β³ π§© Mobile: Problem Management
- β³ π Mobile: Change Management
- β³ πΏ Mobile: Software
- β³ β Mobile: Tasks
- β³ π Mobile: Forms
- β³ π Mobile: Contracts
- β³ π Mobile: Domains
- β³ π Mobile: People
- β³ π Mobile: Projects
- β³ ποΈ Mobile: Files
- β³ π Mobile: LMS
- β³ πΊοΈ Mobile: CMDB
- β³ πΊοΈ Mobile: Network Mapper
- β³ π§ Mobile: Process Mapper
- β³ βοΈ Mobile: Workflow
- β³ π₯οΈ Mobile: System
- β³ π Mobile: Reporting
- β³ π Mobile: System Wiki
- β³ π Mobile: Self-Service Portal
- β³ π§° Mobile: Techniques & Tricks
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- β³ π‘οΈ CSRF protection (S4) β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- π CardDAV contact sync
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π REST API: Domains
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ π·οΈ REST API: Cost centres
- β³ π§βπΌ REST API: Analysts
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ π Rota copy and paste β Developer Deep Dive
- β³ β Checklists & SOPs
- β³ βοΈ Mandatory fields
- β³ π§Ύ Custom ticket fields
- β³ π·οΈ Ticket categories
- β³ π₯ Assigning tickets to a team, and escalation
- β³ π’ One board across every company
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ π Attachment size limits and large emails
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
-
β³
βοΈ Telegram channel - β³ β CSAT company scope and filters β Developer Guide
- β³ π₯ Microsoft Teams channel
- β³ π¨οΈ Mattermost channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ β Record previews
- β³ π Ticket notes: internal or shared
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ π¨ Telling the analyst a ticket is theirs
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ π Confidential tickets
- β³ π₯ Portal managers
- β³ π Who has seen a ticket
- β³ π Reading long tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ ποΈ The folder pane
- β³ π½ Just my tickets, or no closed ones
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- β³ π Scheduled work in your own calendar
- Problem Management
- Tasks
- π Projects
-
Assets
- β³ π’ Moving an asset between companies
- β³ π Shared asset locations
- β³ π§βπΌ Assigning assets to analysts
- β³ π Warranty and lease alerts
- β³ π Leases
- β³ π Saved table views
- β³ π¨οΈ Recording anything, and importing it
- β³ π·οΈ QR asset labels
- β³ π Who holds what, and handover documents
- β³ π₯οΈ The inventory agent (PowerShell)
- β³ ποΈ Proxmox VE servers
- β³ βοΈ VMware Cloud Director servers
- β³ π Linking equipment to tickets
- β³ βοΈ Follow-up tasks on a ticket
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
-
Forms
- β³ π¨ The form designer β Developer Guide
- β³ π Layout & the grid β Developer Guide
- β³ ποΈ Collections β grouping submissions
- β³ π Submissions as PDFs
- β³ β‘ What happens next β a form's own actions
- β³ π οΈ Sections & conditional logic β Developer Guide
- β³ π οΈ Lookup fields β Developer Guide
- β³ π‘οΈ Catalogue request approvals
- People
- Domains
- ποΈ Files
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ π’ One board across every company
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)
- What this is
-
π Bugs resolved
- β³ π Repeating tasks ignored company scope
- β³ π¬ #183 Mailbox checks overlapped
- β³ π #188 Transfer lock shown off when unknown
- β³ β #184 Subtask names and Overdue
- β³ π§© #186 Teams: the buttons did nothing
- β³ π Text in a change could run script
- β³ π AI settings changed without the AI permission
- β³ πͺ #178 Deleting an analyst failed
- β³ π« A team with no departments showed 0 tickets
- β³ π Projects: adding a phase did nothing on Save
- β³ π Leavers could still sign in
- β³ πΌοΈ Logo and courses broke on Apache with PHP-FPM
- β³ π’ Chat tickets ignored your ticket numbering
- β³ π Dates shown as a dash, or in server time
- β³ π Assets β Users showed people from other companies
- β³ π Restricted analysts could read other modules' data
- β³ πΌοΈ Replies with a picture in the thread failed to send
- β³ π Reply attachments never reached the customer
- β³ π οΈ Outbound email attachments β Developer Guide
- β³ π A global SSO provider was missing from the portal
- β³ π Behind a proxy, the SSO redirect said http
- β³ βοΈ The portal tagline moved when you saved it
- β³ π¨ The portal settings screen forgot what you saved
- β³ π‘οΈ The approvals inbox said "Error" and nothing else
- β³ π A table's answers were missing from the PDF
- β³ β A single-select column let you tick every option
- β³ π The portal ignored a form's field widths
- β³ π The tasks board stopped taking clicks
- β³ ποΈ #121 The index list is out of date after upgrading
- β³ π #133 The calendar subscription was empty
- β³ π #131 Tasks always reopened on the board
- β³ π₯ #129 Every page returned HTTP 500 after upgrading
- β³ π³ #127 A PHP warning above the System page
- β³ π #126 Notes stamped with the server's clock
- β³ π Storing every date in UTC
- β³ πͺ The portal was down for everyone signed in
- β³ βοΈ #120 Workflow notes could never be written
- β³ βοΈ #123 Three errors when running Database Verification
- β³ π #122 The description box was a stub in the corner
- β³ π£ Demo data deleted real accounts
- β³ π #117 Sign-in redirected to the wrong address
- β³ π¨ #108 The priority dot was invisible
- β³ β±οΈ #116 Time logged from the right-click menu
- β³ π #114 API keys refused by our own guard
- β³ ποΈ #110 Assigning a task told nobody
- β³ πͺ #107 Signed out while still working
- β³ π #103 "Share with Requester" reached nobody
- β³ π #102 Search found nothing for hyphens
- β³ πͺ #101 Source code editor opened behind
- β³ βοΈ #88 Subtasks could not be ticked off
- β³ π» #84 Asset deep link selected nothing
- β³ π« #79 A new ticket arrived with no status
- β³ π§ #79 A ticket from email did not say so
- β³ π #78 Bell opened to nothing
- β³ π¬ #77 Mail only collected from Inbox
- β³ π #74 The default password could not be changed
- β³ π¦ #70 Renaming an impact level
- β³ π€ #67 App-only mailboxes could not send
- β³ π #45 Verify only ever worked for Microsoft
- β³ π #45 IMAP reported as not authenticated
- β³ βοΈ An email template stopped escaping itself
- β³ π The portal dashboard showed the wrong time
- β³ π’ The folder said 99 and the list showed 96