-
Notifications
You must be signed in to change notification settings - Fork 15
Configurable Lookups Developer Guide
FreeITSM lets administrators rename almost every lookup value: ticket statuses, priorities, task and change statuses, service impact levels, morning-check statuses, ticket origins. Every one of those tables has a name column that exists to be edited.
Code that asks "give me the row called 'Open'" is therefore asking a question the database is entitled to answer with nothing β and a lookup that matches nothing counts nothing, silently, as a confident zero.
This page collects the pattern, the places it was found, and what to write instead.
This is not a hypothetical. Between #70 and #79 the same fault has been found in more than thirty places across five modules β and three of them were wrong in English, on a stock installation, with nothing renamed.
Three separate mechanisms turn a missed name into a plausible number rather than an error:
| Shape | What a miss produces |
|---|---|
(SELECT id FROM t WHERE name = 'Open') in an INSERT |
NULL written to a nullable column β no error |
WHERE name IN ('Urgent','High') in a COUNT |
0 β indistinguishable from "none right now" |
data.statuses['Fail'] || 0 in JavaScript |
0 β the || swallows undefined
|
None of them throws. All of them produce a number that looks like an answer. That is what makes this class worth a page: the failure mode is not a crash, it is quiet, confident wrongness, and it can persist for months.
The worst example found so far is not subtle. Watchtower's Morning Checks card counted statuses named 'OK', 'Warning' and 'Fail'. FreeITSM has never shipped those names β the statuses are Green, Amber and Red. So all three counts were permanently zero, the "checks failed" alert could never fire, and the green "all checks completed and passing" line β which was conditioned on those same empty counts β appeared on a morning when every check was red.
Every lookup table carries flags that exist precisely so code does not have to know names.
| You want | Ask for | Not |
|---|---|---|
| The status a new record starts in | is_default = 1 |
name = 'Open' |
| Anything finished | is_closed = 1 |
name IN ('Closed','Resolved') |
| Anything still live | is_closed = 0 |
name NOT IN ('Closed','Cancelled') |
| A serious priority | ranked above the default | name IN ('Urgent','High','Critical') |
| A record not yet submitted | still in the default status | name = 'Draft' |
| Time that counts against uptime | counts_as_downtime = 1 |
name = 'Major Outage' |
| A colour for a badge | the row's colour
|
a CSS class picked by name |
Some questions have no flag, and can still be answered from data rather than words.
"High priority" = ranked above the default priority.
tp.display_order > COALESCE(
(SELECT display_order FROM ticket_priorities WHERE is_default = 1 LIMIT 1),
(SELECT MIN(display_order) FROM ticket_priorities))On stock data this selects exactly High, Critical and Urgent β the same three the hardcoded list named, and the same number. But it survives renaming, and a priority somebody adds above Normal is now included, instead of being silently left out of the very tile meant to catch it.
"Not yet submitted" = still in the status it started in.
is_default marks the starting status β Draft for changes. So cs.is_default = 0 separates submitted and waiting on somebody from still being written, without naming a status.
SELECT id FROM ticket_statuses
WHERE is_active = 1
ORDER BY is_default DESC, display_order, id
LIMIT 1-
is_activefilters β a deactivated status is absent from the dropdown, so starting records in one reproduces the original symptom by another route. -
is_closedis deliberately not filtered β an administrator who makes a closed status the default has said what they meant. - It can only return nothing if the table is empty.
Sometimes the question really is a judgement nothing records. Which morning-check status counts as a failure? Nothing in FreeITSM knows. morningChecks_Statuses carries a label, a colour, a sort order and an active flag β and no notion of pass or fail.
Two rules for that case:
Claim only what you know. The card now says "all checks completed", never "all passing", and shows green only when every check sits in the most favourable status defined. Saying less is better than saying something confident and wrong.
Give the judgement a home, and make it optional. Watchtower Settings lets an administrator name the statuses that need attention. Crucially, the default is still correct without it β see Β§5.
The most instructive mistake in this whole sweep was made while fixing it.
Having removed the hardcoded 'Major Outage' test, the red-versus-amber decision was pointed at the impact level's counts_as_downtime flag. That reads perfectly well β until somebody wants a degraded service drawn amber, because that flag decides their uptime percentages. The advice written at the time was "turn it off if you don't want degraded showing red", which would have had an administrator falsify their uptime reporting to change a dashboard colour.
counts_as_downtime answers "does time at this level count against uptime". It does not answer "should the dashboard shout". They are different questions and now have different answers, stored separately.
Test for it: if changing a display preference would require editing a value that some other module reports on, the display preference needs its own storage.
| Card | Was | Now |
|---|---|---|
| Tickets | headline = the three statuses named Open, In Progress, On Hold | every open status; the total was reading 88 against a true 100 |
| Tickets | tp.name IN ('Urgent','High','Critical') |
ranked above the default priority |
| Changes | cs.name NOT IN ('Closed','Cancelled') |
is_closed = 0. There has never been a change status called Closed β of four finished statuses that list caught one, so completed and failed changes counted as upcoming work |
| Changes | cs.name IN ('Submitted','Pending Approval') |
approval_datetime IS NULL and not still in the default status |
| Changes | cs.name = 'In Progress' |
the scheduled work window, which already says exactly that |
| Tasks |
ts.name = 'In Progress' / 'To Do'
|
every open task status; Blocked was invisible |
| Morning Checks |
statuses['OK' | 'Warning' | 'Fail'] β names that never existed
|
one figure per status, from morningChecks_Statuses
|
| Service Status |
'Major Outage' || 'Partial Outage' decided red vs amber |
the levels marked serious; a renamed total outage was drawn amber |
| Service Status | three names picked one of four badge styles | the level's own colour
|
| Browser extension |
statuses['Fail'], and tickets/tasks summed by name |
shared server-side counts |
Earlier, in Service Status β #70
Renaming Operational split the status board in two, because a healthy service has no stored impact level β it is derived, and six places derived it by writing the word Operational into the answer.
Tickets β #79
Seven creation paths resolved the starting status by the name Open.
When the Watchtower faults were found, the obvious fix was a settings screen: let the administrator tick which statuses each figure should count. That was built β but second, and deliberately.
If ticking boxes is what makes the numbers right, then:
- every existing installation stays wrong until somebody finds the screen;
- a fresh installation is wrong until somebody configures it;
- and a dashboard showing nothing reads as "nothing needs attention", which is the most dangerous thing it can say untruthfully.
So the queries were made correct with no configuration at all β count every status, read every flag β and the settings screen exists only to trim what is already true. Leave it alone and the numbers are right.
This shows up in the storage as watchtower_items.is_customised, which separates "not configured" from "configured to show nothing". Without that column an empty selection is indistinguishable from an untouched one, and would silently mean "all".
Three greps, in order of how much they find.
# 1. SQL comparing a user-editable name to a literal
grep -rnE "(name|Label|Status) *(=|IN|<>|!=) *\(?'" --include=*.php .
# 2. JavaScript comparing to a capitalised literal, or indexing by one
grep -rnE "(===|!==|==) *'[A-Z]|\['[A-Z][a-z]" --include=*.js --include=*.php .
# 3. The known vocabulary, anywhere
grep -rnE "'(Open|Closed|In Progress|On Hold|To Do|Done|Blocked|Normal|High|Urgent|Critical|Fail|Warning|OK|Green|Amber|Red|Major Outage|Partial Outage|Operational|Draft|Submitted|Approved|Cancelled|Completed)'" --include=*.php --include=*.js .Grep 3 will also match comments and seed data β read the hits, do not count them.
-
App-written keys.
ticket_audit.field_name = 'status'is an English literal, but the audit log writes it as a fixed internal key that no administrator can edit and nothing translates. Same fordirection = 'Inbound'andworkflow_executions.status = 'failed'. -
Seed data.
INSERT IGNORE INTO ticket_statuses β¦ ('Open', β¦)is where the names come from. Creating them by name is fine; finding them by name is not.
The distinction is simple: does an administrator have a screen where they can change this value? If yes, never match on it.
The check that actually settles it is a whole-payload diff across a rename.
- Capture the endpoint's output.
- Rename every configurable lookup β
UPDATE ticket_statuses SET name = CONCAT('DE-', name)and the same for priorities, task and change statuses, impact levels, morning-check labels, origins. - Capture it again.
- Diff every numeric and boolean field. All must be identical.
- Confirm the labels did change β otherwise the test proves nothing except that you read no names at all, including the ones you should be displaying.
Applied to Watchtower this compared 77 numeric and boolean fields, all identical, while Open β DE-Open, Green β DE-Green and High/Critical/Urgent β DE-High/DE-Critical/DE-Urgent all followed the rename.
Step 5 is the control. Without it a page that renders nothing at all passes.
- A new ticket arrived with no status β the report that started this sweep
- Renaming an impact level β the same fault a month earlier
- Watchtower settings β where the judgements that have no flag now live
- Watchtower settings β Developer Guide β the storage model for a view preference
- Internationalisation β the other half of "this text is not a key"
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- Theming & Dark Mode
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
- MobileβFriendly
-
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
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- 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: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- Problem Management
- Tasks
- Assets
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
- Forms
- 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
- β³ 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)