How Versus Desk records, routes, and resolves incidents — the INC ticket model, a declarative state machine, business-hours SLAs, CI links into live inventory, and automatic incident intake from Observe alerts, all on one hash-chained audit trail.
An incident in Versus Desk is a record of something broken — a failing virtual server, a fired alert, a user report — that an operator drives to resolution. Incidents live in the same service desk as changes, problems, and requests, share one ticket model and one audit trail, and link directly to the devices, certificates, and virtual servers already in your Versus inventory rather than to a separate CMDB.
This page covers what an incident is, the lifecycle it moves through, how SLAs are timed, how incidents open themselves from monitoring alerts, and how an operator works one from the API.
Every ticket type — incident, problem, change, request — is a row on a shared Ticket table, with type-specific fields on a one-to-one extension. Incidents carry an IncidentExt.
Core fields on the ticket:
| Field | Meaning |
|---|---|
number | Human ID, INC-000001, allocated monotonically per prefix inside the create transaction (gaps from rolled-back writes are acceptable, not backfilled). |
short_description / description | The one-line summary and full detail. |
state | Where the incident is in its lifecycle. Validated by the state machine, never a database enum. |
priority | 1 (highest) to 4, derived from impact (1–3) and urgency (1–3). |
assignment_group / assignee | Who owns the work — a queue and, once picked up, a person. |
requester_user / requester_portal | Who reported it — a full Versus user or a lightweight portal identity. |
origin | How it was created: manual, observe_alert, workflow, catalog, email, api, or sync. |
opened_at / resolved_at / closed_at | Lifecycle timestamps. |
Incident-specific fields on IncidentExt:
major_incident — flags a high-severity incident for special handling.category / subcategory — tenant-editable classification (not a fixed enum).problem — a link to the Problem ticket this incident rolls up into.caused_by_change — a link to a Change that introduced the fault.resolution_code / resolution_notes — required to resolve (see below).reopen_count — incremented every time a resolved incident is reopened.pending_reason — why the incident is on hold: awaiting_user, awaiting_vendor, awaiting_change, or scheduled.Incidents move through a declarative state machine — one engine drives all four ticket types, and transitions are data, not hardcoded buttons. The incident path is:
New ──assign──▶ Assigned ──start──▶ In Progress ──resolve──▶ Resolved ──close──▶ Closed
│ ▲ │
pend│ │unpend reopen│
▼ │ ▼
Pending In Progress
Each transition fires an event and may enforce a guard:
assign (New → Assigned) requires an assignee or an assignment group.start (Assigned → In Progress) picks the work up.pend (In Progress → Pending) requires a valid pending_reason.unpend (Pending → In Progress) resumes work.resolve (In Progress or Pending → Resolved) requires both resolution_code and resolution_notes — you cannot close out an incident without recording how it was fixed. The resolution is written and resolved_at stamped as part of the transition.close (Resolved → Closed) finalizes it and stamps closed_at.reopen (Resolved → In Progress) clears resolved_at and increments reopen_count.Transitions are concurrency-safe: the service layer takes a row lock on the ticket (select_for_update), re-reads the current state, re-checks the guard, applies the change, and commits — all in one transaction. An illegal transition (wrong state for the event, or a failed guard) is rejected with 409 Conflict and changes nothing.
The UI does not hardcode which buttons to show. It reads the ticket's available_events — the set of events legal from the current state — and renders exactly those actions.
Every transition writes two records: a TicketTransition row (the human-readable "what happened" log) and an entry on the append-only, hash-chained audit trail (ItsmAuditEvent), so the full history of an incident is tamper-evident.
Priority is a 1–4 value (1 is highest). It is set from impact (how much is affected, 1–3) and urgency (how time-sensitive, 1–3), both defaulting to 2. Priority is what SLA policies and queue views sort and filter on, and it is what the alert-intake path maps incoming severities onto.
An incident's clock is an SLA instance attached from a matching SLA definition. Two metrics are tracked:
An SlaDefinition declares a target_minutes, an optional business-hours calendar, a precedence, a warning_threshold_pct (default 75%), and an applies_to matcher on ticket type, priority, and assignment group. When an incident is created — and again whenever a relevant field changes — Versus re-evaluates the definitions and attaches the highest-precedence match per metric, without disturbing an instance already running.
The due time is computed against a BusinessCalendar — an IANA time zone, per-weekday open hours, and a holiday list. A four-hour resolution target opened at 4pm on a Friday lands the following business morning, not at 8pm, correctly skipping closed hours, weekends, and holidays. A definition with no calendar is treated as 24×7.
A single scheduler tick (run on a short cadence by the check_itsm_gates job) scans running instances by their materialized due_at:
sla_warn.due_at it records a breach (sla_breach).Both are routed through Observe's alerting layer as alert events — escalation reuses the platform's existing notification routing rather than being a separate system. See Observe Alerting.
resolve, recording met or breached.pend pauses running instances; unpend resumes them and pushes due_at forward by exactly the paused duration, so a hold awaiting a vendor doesn't burn the clock.You can list an incident's instances at GET /api/itsm/tickets/{number}/sla/, and filter the queue to everything currently breached with ?sla=breached.
Incidents don't only come from people. When an Observe alert fires, it can open an incident automatically. The intake path is fingerprint-deduplicated:
origin = observe_alert and origin_ref carrying the alert rule UID and fingerprint. The alert severity maps to priority: critical → 1, high → 2, medium → 3, low → 4.When the alert later resolves, Versus adds an internal comment to the incident rather than auto-closing it — a resolved alert doesn't mean the operator is finished, so closing stays a deliberate agent action. The intake runs in-process (Observe and the desk share one application core), so there is no webhook to configure between them.
An incident links to the things it affects through CI links, which point directly into existing Versus inventory — there is no separate CMDB to keep in sync. A CI link references a device, vip (virtual server), certificate, rule, or service (application) by type and id.
Because the link is indexed by (ci_type, ci_id), Versus can answer "every ticket touching this device" instantly — the same index the change-management risk engine reads to weigh recent incidents on a device a change is about to touch. See Change Management.
Incidents also link to other tickets: problem_incident (rolls an incident up to a problem), caused_by, duplicate_of, related, and fixes, alongside the problem and caused_by_change foreign keys on IncidentExt.
Incidents are the incident ticket type on the ticket endpoint, addressed by number. Reading requires the itsm.view permission; creating and editing require itsm.agent.
Create an incident:
POST /api/itsm/tickets/
Authorization: Bearer …
Content-Type: application/json
{
"ticket_type": "incident",
"short_description": "vs_app01 pool members all down",
"priority": 2,
"impact": 1,
"urgency": 2,
"assignment_group_id": "…",
"ci_links": [ { "ci_type": "vip", "ci_id": "…", "ci_label": "vs_app01" } ]
}
Drive it through its lifecycle with the transition action — the event must be legal from the current state, and guarded events carry their required data in fields:
POST /api/itsm/tickets/INC-000001/transition/
{ "event": "assign", "fields": { "assignee_id": "…" } }
POST /api/itsm/tickets/INC-000001/transition/
{ "event": "start" }
POST /api/itsm/tickets/INC-000001/transition/
{
"event": "resolve",
"fields": {
"resolution_code": "config_error",
"resolution_notes": "Re-enabled the disabled pool members; monitor now green."
}
}
Add a comment (public to the requester, or internal to agents only), which also stops the response SLA when public:
POST /api/itsm/tickets/INC-000001/comments/
{ "body": "Failover confirmed healthy.", "visibility": "public" }
List and filter the queue with standard query parameters — ticket_type, state, assignment_group, assignee, priority — plus desk shortcuts: q (text search), ci_type+ci_id (tickets touching one CI), sla=breached, and saved views view=mine|submitted|group|open|closed.
On close, incidents can trigger a CSAT survey to the requester, closing the feedback loop on the resolution.
Incidents are one corner of Versus Desk, and they connect outward:
caused_by_change.origin, and change risk scoring reads incident history on shared CIs (Change Management).Every incident action — transition, comment, assignment, SLA warning, SLA breach — lands on the same tamper-evident audit chain, so an incident's whole life is reconstructable after the fact.