Skip to content

Checklists and SOPs Developer Guide

Ed Mozley edited this page Sep 16, 2026 · 2 revisions

Checklists & SOPs β€” developer guide

How FreeITSM stores reusable Standard Operating Procedures and attaches them to tickets. Shipped in 2.0.0, proposed in #138 and contributed as PR #141 by Santhosh Srinivasan (Sandy) β€” FreeITSM's first community-contributed module.

For the changes made during integration, and why, see Checklists module β€” house style. The administrator-facing page is not written yet.


1. The shape of it, in one idea

A knowledge article is something you read. A task is something scheduled and delegated. An SOP is neither: it is a list of things that must be ticked off on this ticket, by whoever is holding it, before it can finish.

That gap is the whole justification for the module, and it was argued out in the proposal before a line was written. Worth reading #138 for it:

  • Knowledge is passive. You cannot tick a paragraph, and nothing records who read it.
  • Tasks are for distinct, scheduled, delegated work. Ten sub-tasks for a routine ten-step onboarding floods the global Tasks board, generates its own calendar entries and notifications, and makes the analyst context-switch out of the ticket to tick a box.

So: a lightweight checklist that lives inside the ticket, with per-step attribution and a completion gate.


2. The data model

Six tables. Two are lookup lists, two are the reusable template, two are the copy attached to a ticket.

checklist_categories      id, name                     β€” grouping for templates
checklist_roles           id, name                     β€” "who usually does this"

checklist_templates       id, title, category, scope, keywords, description, …
checklist_template_items  id, template_id, title, suggested_role,
                              is_mandatory, requires_input, input_placeholder, sort_order

ticket_checklists         id, ticket_id, template_id, title
ticket_checklist_items    id, ticket_checklist_id, title, suggested_role,
                              is_mandatory, is_completed, completed_by_id,
                              completed_by_name, completed_datetime,
                              requires_input, input_placeholder, response_value, sort_order

πŸ”‘ The template/instance split is the best decision in the module

ticket_checklist_items copies the step's text. It does not point at checklist_template_items.

That looks like duplication and it is exactly right. A ticket's checklist is a record of what somebody was asked to do at the time. If the rows pointed at the template, then editing the template next year would silently rewrite the history of every ticket that ever used it β€” and an SOP checklist exists precisely so that history can be trusted.

The same reasoning is why completed_by_name is stored alongside completed_by_id: the analyst's account may be renamed or removed, and the record should still say who ticked the box.

⚠️ Do not "normalise" this later. It will look like a redundancy to somebody eventually. It is a snapshot, and snapshots are supposed to diverge from their source.

scope

ticket | task | both. Only the ticket half is implemented; the task half is declared and unused, which is honest β€” see What is not built yet.

⚠️ No foreign keys on a grown install

database/freeitsm.sql declares three foreign keys with ON DELETE CASCADE. Database Verification does not create foreign keys at all, so an installation that grew through upgrades has none of them.

Both are supported configurations, so nothing may rely on a cascade. Every delete path removes its children explicitly, inside a transaction:

$conn->beginTransaction();
$conn->prepare("DELETE FROM ticket_checklist_items WHERE ticket_checklist_id = ?")->execute([$chkId]);
$conn->prepare("DELETE FROM ticket_checklists      WHERE id = ?")->execute([$chkId]);
$conn->commit();

This is a general rule in FreeITSM, not a checklists one: Database Integrity.


3. Attaching a checklist to a ticket

Three routes, all landing in the same place:

Route Where
The analyst picks one the SOP panel in the ticket reading pane
Suggested by relevance suggest_template scores templates against the ticket's subject and first five notes
A workflow action attach_checklist, dispatched by the workflow engine

The relevance scoring

api/tickets/ticket_checklists.php, case "suggest_template". Deliberately simple and readable rather than clever:

  • template title appearing in the ticket text: +5
  • each matching keyword: +2
  • anything scoring below 2 is not offered at all

The score is then turned into a confidence percentage for display. There is no index and no full-text search here β€” the template library is tens of rows, not thousands, and a LIKE sweep over it costs nothing. If a site ever has thousands of SOPs this is the first thing to revisit.


4. Completion, attribution and the audit trail

Ticking a step (toggle_item) writes four things: the flag, the analyst id, the analyst's name as it is now, and the timestamp.

UPDATE ticket_checklist_items
   SET is_completed = 1, response_value = ?, completed_by_id = ?,
       completed_by_name = ?, completed_datetime = UTC_TIMESTAMP()
 WHERE id = ?

πŸ”΄ UTC_TIMESTAMP(), never NOW() and never PHP's date(). FreeITSM stores UTC at rest (#126) and renders in the viewer's zone. PHP and MySQL do not necessarily agree on the local zone β€” on the reference dev box PHP runs Europe/London while MySQL runs UTC, so date('Y-m-d H:i:s') stored timestamps an hour in the future during BST. See Date and time formats.

Steps that ask for a value. requires_input makes the tick prompt for something β€” an asset tag, a serial number, a note β€” stored in response_value. This is the feature that turns a checklist into a record: "laptop issued" is an assertion, "laptop issued, asset tag A-4471" is evidence.


5. The closure rule

A ticket can be closed with mandatory steps outstanding. The override is recorded.

Where this lives matters more than what it does:

// includes/services/checklists.php
ChecklistsService::assertClosureAllowed($conn, $ticketId, $tenantId);  // may throw
ChecklistsService::recordClosureOverride($conn, $ctx, $ticketId);      // writes the note

Both are called from TicketsService::updateTicket(), which is the choke point for the inbox, bulk actions, assign/schedule, and the v1 REST API. Putting the rule in the browser would mean the API ignored it, and a compliance control the API ignores is not a control.

πŸ”΄ And from WorkflowEngine::action_set_ticket_status(), which writes the status itself and does not go through updateTicket(). Until the mandatory fields work, this gate did not apply to workflows at all, although the help card said it did. A workflow acts as ActorContext(0, null, 'workflow'). ticket_notes.analyst_id cannot hold 0 (it has a foreign key), so a workflow close writes its override as a Workflow Note in ticket_audit, the same place the engine's own notes go.

Tickets β†’ Settings β†’ Checklists chooses the behaviour:

Setting Behaviour
Warn, and record it (default) the close proceeds; an internal note names the skipped steps, who closed it and through which interface
Refuse the close ServiceError('validation', 'mandatory_steps_outstanding', …) and the ticket does not move

πŸ”΄ The refusal is checked BEFORE the status is written. Checked afterwards, a block install would throw having already closed the ticket: the caller sees an error while the database disagrees with it. The check sits in the status-resolution branch of updateTicket, ahead of the UPDATE.

Why warn is the default: assets/js/inbox.js already carried the house position, written for #83 about unfinished tasks β€”

"closing a ticket that still has unfinished tasks WARNS, it never blocks … a warning that cannot be shown must not become a block that cannot be cleared."

A hard block traps any ticket whose remaining step has become impossible: the person who owned it has left, the step is obsolete, and there is no way out but SQL. Recording the exception is also the better compliance answer β€” an auditor does not want a system where the bad outcome was impossible, they want one where it is attributable.

Either way the note is written. The setting decides whether it is allowed, never whether it is logged.


6. Where the code lives

checklists/index.php            the template library (cards, filters, search)
checklists/edit/index.php       the template editor β€” a SCREEN, not a modal
checklists/settings/index.php   categories, suggested roles, left-panel preference
checklists/api.php              template get / save / delete
checklists/ticket_view.js       the SOP panel inside a ticket
api/checklists/*.php            category + role CRUD, over the service
api/tickets/ticket_checklists.php   attach, suggest, toggle, remove
includes/services/checklists.php    the closure rule and the lookup rules
database/demo-data/checklists.json  five worked SOPs for evaluation

The editor is a screen because a form editor is

forms/edit/ is a full screen for the same shape of problem: a parent with an unbounded list of children, each child having attributes of its own. A 680px modal at 88vh is a poor place to drag a fifteen-step procedure into order β€” the drop target is regularly off-screen, and a scrolling modal body will not auto-scroll during a drag without a lot of bespoke work. A screen also gets a URL, so ?id=N is linkable and the browser's back button behaves.

The drag-and-drop is forms' implementation, copied deliberately β€” same handle-gated dragstart, same midpoint drop calculation, same .dragging / .drag-over-top / .drag-over-bottom class names.

⚠️ The handle gate is the part that is easy to leave out. Without it the whole row is draggable, and click-dragging to select text inside a step's title starts a reorder instead:

let dragAllowed = false;
document.addEventListener('mousedown', e => { dragAllowed = !!e.target.closest('.step-drag'); });
function onStepDragStart(e, i) { if (!dragAllowed) { e.preventDefault(); return; } … }

7. What is not built yet

Declared honestly in the proposal and still true:

  • the Tasks half of scope β€” the column accepts task and both; nothing consumes it
  • per-step approval by a watcher or manager
  • enforced sequential dependencies (the panel shows the next step but does not prevent ticking a later one)
  • per-step routing, sub-SLAs, branching checklists, CMDB links at step level
  • version history for templates

8. Related

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally