History and Audit Trail#
The ticket_history table is a complete, append-only log of everything that ever happened to a ticket. Every state change, owner change, queue transfer, note, email sent, or attribute update writes a row here. It is the primary source for SLA reporting, response-time analysis, and compliance audit trails.
The ticket_history Table#
Column |
Type |
Description |
|---|---|---|
|
BIGINT |
Primary key. |
|
VARCHAR(200) |
Human-readable description of the event (e.g. |
|
SMALLINT |
The category of event. Joins to |
|
BIGINT |
The ticket this event belongs to. Joins to |
|
BIGINT |
The article associated with this event, if any (e.g. when an email is sent). NULL otherwise. |
|
SMALLINT |
The ticket type at the time of the event. Joins to |
|
INTEGER |
The queue the ticket was in at the time of the event. Joins to |
|
INTEGER |
The ticket owner at the time of the event. Joins to |
|
SMALLINT |
The ticket priority at the time of the event. Joins to |
|
SMALLINT |
The ticket state at the time of the event. Joins to |
|
DATETIME |
When the event occurred. This is the timestamp to use for all time-based history queries. |
|
INTEGER |
Agent who triggered the event. Joins to |
Note
The name column stores a structured string whose format depends on the history type. For state changes it looks like %%open%%new%% (old state %% new state). For email events it may contain the recipient address. For reporting purposes, rely on history_type_id/ticket_history_type.name to filter event types, and use create_time for timing — avoid parsing name unless you need its specific content.
ticket_history_type — resolves ticket_history.history_type_id#
Name |
When it is written |
|---|---|
|
Ticket was created. |
|
Ticket state changed. |
|
Owner changed. |
|
Responsible agent changed. |
|
Priority changed. |
|
Ticket moved to a different queue. |
|
Ticket title changed. |
|
Ticket type changed. |
|
Customer contact or organization changed. |
|
Associated service changed. |
|
Associated SLA changed. |
|
Agent sent an email. |
|
Customer sent an email (inbound). |
|
Agent sent a reply. |
|
Agent created a phone-call note (outbound). |
|
Agent created a phone-call note (inbound from customer). |
|
Note added to the ticket. Whether it is internal or customer-visible is determined by |
|
Customer sent a reply to an existing ticket (inbound email or message creating a new article). |
|
The pending-until time was set or changed. |
|
A dynamic field value on the ticket was changed. |
|
Ticket was merged into another ticket. |
|
Ticket was locked. |
|
Ticket was unlocked. |
|
Solution escalation clock started. |
|
Solution escalation clock stopped (resolved before deadline). |
|
First-response escalation clock started. |
|
First-response escalation clock stopped. |
|
Update escalation clock started. |
|
Update escalation clock stopped. |
Common History Queries#
When was a ticket first responded to?
-- First agent response per ticket (first EmailAgent or SendAnswer event)
SELECT
th.ticket_id,
MIN(th.create_time) AS first_response_at
FROM ticket_history th
JOIN ticket_history_type tht ON tht.id = th.history_type_id
WHERE tht.name IN ('EmailAgent', 'SendAnswer', 'PhoneCallAgent')
GROUP BY th.ticket_id;
Average first-response time per queue (in minutes):
SELECT
q.name AS queue,
ROUND(AVG(
TIMESTAMPDIFF(MINUTE, t.create_time, fr.first_response_at)
)) AS avg_first_response_min
FROM ticket t
JOIN queue q ON q.id = t.queue_id
JOIN (
SELECT th.ticket_id, MIN(th.create_time) AS first_response_at
FROM ticket_history th
JOIN ticket_history_type tht ON tht.id = th.history_type_id
WHERE tht.name IN ('EmailAgent', 'SendAnswer', 'PhoneCallAgent')
GROUP BY th.ticket_id
) fr ON fr.ticket_id = t.id
WHERE t.create_time >= DATE_SUB(NOW(), INTERVAL 30 DAY)
AND t.archive_flag = 0
GROUP BY q.id, q.name
ORDER BY avg_first_response_min;
Full state-change timeline for one ticket:
SELECT
th.create_time,
tht.name AS event_type,
th.name AS detail,
CONCAT(u.first_name, ' ', u.last_name) AS agent
FROM ticket_history th
JOIN ticket_history_type tht ON tht.id = th.history_type_id
JOIN users u ON u.id = th.create_by
WHERE th.ticket_id = :ticket_id
ORDER BY th.create_time;
Tickets that escalated in the last 7 days (by history event):
SELECT DISTINCT
t.tn,
t.title,
th.create_time AS escalated_at,
q.name AS queue
FROM ticket_history th
JOIN ticket_history_type tht ON tht.id = th.history_type_id
JOIN ticket t ON t.id = th.ticket_id
JOIN queue q ON q.id = t.queue_id
WHERE tht.name IN (
'EscalationSolutionTimeStart',
'EscalationResponseTimeStart',
'EscalationUpdateTimeStart'
)
AND th.create_time >= DATE_SUB(NOW(), INTERVAL 7 DAY);