Skip to content

SLA Management

Ed Mozley edited this page May 17, 2026 · 1 revision

SLA Management

Module: Tickets β€” Settings β†’ SLA tab Engine: includes/sla.php (compute-on-read) Cron worker: cron/sla_breach_check.php In-app help: tickets/help-sla.php (full guide with worked examples)

Business-hours-aware Service Level Agreement tracking for tickets β€” per-priority response and resolution targets, per-priority business calendars, pause-the-clock statuses, configurable behaviour for edge cases, and per-department breach notification rules fired by a scheduled cron worker.

For a scenario-driven walkthrough with four worked examples (UK single-office, tiered SaaS, 24/7 follow-the-sun, school-aligned charity), see the Four service desks deep dive on freeitsm.co.uk.

Architecture β€” four design choices

Choice Why
Business hours, not wall clock The industry standard. A P2 raised at 16:30 with a 1-hour response target doesn't breach at 17:30 if the office closes at 17:00; the clock resumes at 09:00 the next working day.
Pause-able Statuses flagged pauses_sla = 1 stop the clock (e.g. Awaiting Customer, Awaiting Vendor). Resumes when the ticket moves back to a working status.
Compute-on-read The engine recomputes SLA state from the ticket audit log every time it's accessed. No stored counters that drift. Changing a business calendar later recomputes every historical ticket against the new hours next time it's viewed.
Admin-controlled enforcement cutoff The sla_enforce_from setting means SLAs only apply to tickets created on or after a chosen datetime. Existing tickets aren't suddenly retroactively breached when SLAs are turned on.

Business Calendars

Each calendar defines when the service desk is open. Configurable in Settings β†’ SLA β†’ Business Calendars.

Field Purpose
name Human-readable identifier (e.g. UK Office, 24/7 Global)
timezone IANA identifier (Europe/London, America/New_York, UTC) β€” DST handled automatically via PHP DateTimeZone
is_default Exactly one calendar is the default; new priorities inherit it unless otherwise specified
is_active Toggle without deleting

Weekly hours are stored separately in sla_calendar_hours (one row per weekday, ISO 8601: 1=Mon, 7=Sun) with start_time and end_time. Absence of a row for a weekday = closed that day. Holidays in sla_calendar_holidays override the weekly pattern.

Multiple calendars are supported. Common patterns:

  • Single 9-5 office β€” one calendar, Mon-Fri 09:00-17:00, local bank holidays
  • Tiered VIP β€” one calendar for standard customers, a second with extended hours for VIPs
  • 24/7 operation β€” one calendar with every weekday ticked and 00:00-23:59 hours, no holidays
  • Term-time (e.g. school IT) β€” one calendar with all school holiday days entered

Priorities & SLA Targets

Each priority can have:

Field Purpose
sla_response_minutes Time from ticket creation to first analyst action
sla_resolution_minutes Time from ticket creation to ticket closure
sla_calendar_id Which calendar this priority's clocks run against (NULL = use default)

Leaving both target fields blank disables SLA tracking for that priority β€” useful for an Informational or Backlog priority. Per-priority calendars enable patterns like "VIP P1 uses 24/7 calendar; Standard P1 uses 9-5 calendar".

Pause-SLA Statuses

The Statuses tab in Settings has a dedicated Pause SLA column. Any status flagged here will pause the clock when a ticket is in that status. The SLA engine walks the ticket's status-change audit log (ticket_audit with field_name = 'status') and excludes minutes spent in paused statuses from the consumption calculation.

Default seeded paused statuses:

  • Awaiting Response (customer owes us information)
  • Awaiting Vendor (waiting on third-party support)

Common additions:

  • On Hold (Change Window) β€” scheduled for a future maintenance window
  • On Hold (Parts) β€” waiting on hardware delivery
  • Maintenance Window β€” planned outage scope

Generic On Hold is often deliberately left non-pausing to force analysts to be specific about why a ticket is parked. See the Watchtower section below for the paused-too-long safety net.

Behaviour Settings

Global toggles stored in system_settings. Defaults shown in bold.

Mid-flight priority change

What happens to the SLA clock when a ticket's priority is changed mid-flight (e.g. P3 β†’ P1).

Value Effect
forward Accrued business minutes are preserved and re-targeted. A ticket with 2h used against a P3's 8h target is now 2h used against the new P1's 1h target β†’ already breached.
recompute Clock reset and the new target applied retroactively from ticket creation. Clean recalculation.
reset New clock starts fresh from the moment of priority change. Most forgiving but customers may feel cheated.

Reopen behaviour

What happens to the SLA clock when a closed ticket is reopened.

Value Effect
reset Fresh response and resolution clocks from the reopen moment. Reopen is effectively a new ticket.
continue Resume from the elapsed time at closure. Original targets continue to apply.

First-response definition

Which event stops the response clock.

Value Effect
either First analyst action of any kind: outbound email, status change, or assignment.
status_change Only a status change away from the default counts. Stricter β€” analyst has to have actually worked the ticket.
outbound_email Only an email reply to the requester counts. Strictest β€” customer must have heard from us.

Warning threshold

sla_warning_threshold_percent β€” how far through the target before the inbox pill flips amber and warning-trigger breach notifications fire. Default 80 (60-min target β†’ amber at 48 min).

Breach Notifications

Configurable per-department email rules fired by the cron worker. Rules are configured in Settings β†’ SLA β†’ Breach Notifications.

Each rule has four dimensions, all independent:

1. Scope

Value Behaviour
Default (department_id IS NULL) Rule applies to tickets in every department
Specific department (department_id = N) Rule applies only to that department. A specific rule wins over the default for tickets in that department.

2. Trigger

Value When
warning Target percent β‰₯ warning threshold AND < 100
breach Elapsed > target

If the cron is down when a warning would have fired and the ticket then breaches, the warning will still fire (catch-up) before the breach in the same run β€” so analysts always get the early-warning email even after an outage.

3. Target

Value Behaviour
response Watch the response SLA only
resolution Watch the resolution SLA only
both Watch both (one rule covers both, but a separate target-specific rule wins over a both rule for that target)

4. Recipients

Any combination of:

Type Source
Ticket's assignee analysts.email via tickets.assigned_analyst_id
Members of the ticket's department teams All active analysts in any team linked to the ticket's department (department_teams ↔ analyst_teams)
Specific analyst Picked from the analyst list
Custom email addresses Free-text, comma/semicolon/newline separated. Useful for shared inboxes, Slack/Teams email bridges

Dedup

Each (ticket_id, target_type, trigger_type) combination fires at most once. Tracked in sla_notifications_sent with a UNIQUE key. The cron worker checks this table before sending. Reopened tickets keep the dedup rows β€” clear them manually to re-fire:

DELETE FROM sla_notifications_sent WHERE ticket_id = 12345;

Cron Worker

cron/sla_breach_check.php walks every open SLA-tracked ticket on a schedule (every 5 minutes is the sweet spot), decides which warnings/breaches should fire, and sends the emails via the ticket's originating mailbox (Microsoft Graph or Gmail β€” same OAuth tokens as inbound mail polling).

Invocation

Mode Command Auth
CLI (recommended) php cron/sla_breach_check.php None β€” filesystem perms gate it
HTTP curl http://host/freeitsm-app/cron/sla_breach_check.php?token=<TOKEN> Shared-secret token via ?token=

Get the token:

SELECT setting_value FROM system_settings WHERE setting_key = 'sla_cron_token';

Auto-generated as bin2hex(random_bytes(16)) on first install (32 hex chars, ~128 bits of entropy). Rotate by UPDATE-ing the row.

Security layers (HTTP only)

Layer Default Behaviour
Shared-secret token random per install Constant-time comparison via hash_equals()
Per-IP failed-auth lockout 10 attempts / 1 hour >10 wrong-token requests from one IP in the past hour β†’ 1-hour 429 lockout
Min interval between successful runs 30s (sla_cron_min_interval_seconds) Even a valid request returns 429 if it arrives sooner than this after the last successful run. Defeats accidental double-scheduling and runaway loops.

Activity log

Every invocation (accepted or rejected) is logged to sla_cron_runs with started_at, ended_at, duration_ms, invocation (cli/http), client_ip, outcome (ok / rate_limited / auth_failed / config_missing / error), per-run counts, and notes. Auto-pruned after sla_cron_log_retention_days (default 30). Even at 1-minute cadence the table stays small (~43k rows / 30 days).

View the last 20 runs in the Cron Activity panel at the bottom of the SLA settings tab.

Setup

Windows Task Scheduler β€” see docs/sla-cron-setup.md for the full walkthrough. In short: create a task with trigger "Daily, every 1 day, repeat every 5 minutes for 1 day", action "Start a program", program = path to php.exe, arguments = path to sla_breach_check.php.

Linux cron:

*/5 * * * * /usr/bin/php /var/www/freeitsm-app/cron/sla_breach_check.php >> /var/log/freeitsm-sla-cron.log 2>&1

Remote schedulers (cron-job.org, EasyCron) β€” use the HTTP form with the token.

Watchtower Safety Net

The Watchtower dashboard surfaces "tickets paused over N hours" as an amber attention row on the Tickets card. Catches the ITIL antipattern of analysts parking tickets in On Hold to escape the SLA clock when stuck or busy. Threshold configurable via system_settings.watchtower_paused_too_long_hours (default 24 β€” allows overnight pauses, flags anything sitting through a full working day):

UPDATE system_settings
   SET setting_value = '8'
 WHERE setting_key = 'watchtower_paused_too_long_hours';

Anchor for "how long has it been paused" is the latest ticket_audit row with field_name = 'status'. Falls back to tickets.created_datetime if no status-change history exists.

Settings Reference

All SLA settings live in system_settings (key/value).

Key Default Purpose
sla_enforce_from NULL Datetime cutoff. NULL = SLA enforcement disabled entirely. Set to a past datetime to turn on, future to schedule.
sla_priority_change_behaviour forward forward / recompute / reset
sla_reopen_behaviour reset reset / continue
sla_first_response_definition either either / status_change / outbound_email
sla_warning_threshold_percent 80 Amber pill + warning-trigger notifications fire at this % of target
sla_notify_assignee_at_warning 1 Legacy flag (notification rules supersede this)
sla_notify_lead_at_breach 1 Legacy flag (notification rules supersede this)
sla_cron_token random Shared secret for HTTP cron invocation
sla_cron_min_interval_seconds 30 Min seconds between successful cron runs
sla_cron_log_retention_days 30 How long to keep sla_cron_runs rows
watchtower_paused_too_long_hours 24 Threshold for the Watchtower paused-too-long attention row

Key Tables

Table Purpose
sla_calendars Business calendars (name, timezone, is_default, is_active)
sla_calendar_hours Weekly working pattern per calendar (weekday, start_time, end_time)
sla_calendar_holidays Per-calendar holiday dates (override weekly pattern)
ticket_priorities Adds sla_response_minutes, sla_resolution_minutes, sla_calendar_id columns
ticket_statuses Adds pauses_sla boolean column
sla_notification_rules Per-department breach notification rules (scope, trigger, target, recipients)
sla_notifications_sent Dedup log (one row per ticket+target+trigger that fired)
sla_cron_runs Per-invocation audit log (timing, outcome, counts, client IP)

See Also

  • Tickets β€” parent module
  • Watchtower β€” paused-too-long safety net
  • Four service desks deep dive β€” scenario-driven walkthrough on freeitsm.co.uk
  • In-app help: tickets/help-sla.php (full guide with three worked examples including cross-timezone "fair" options)
  • Setup guide: docs/sla-cron-setup.md (Windows Task Scheduler + Linux cron + remote schedulers)

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally