Ticket Lifecycle Hooks

2 views Markdown

The eight hooks over creating, closing, transferring and locking a ticket.

Overview

The path of a ticket lives here: it opens, its status moves, it is transferred, locked, and finally deleted or resolved on its own.

Two status hooks look alike but are separate: one reports a change made by staff, the other an automatic resolution nobody touched.

Reference

Stopping a ticket opened by staff

gateticket.admin_create
AdminTickets before the write

Runs when staff open a ticket on behalf of a customer, before anything is written.

Parameters 3
$set_requestarrayThe ticket record to be written: title, status, department, priority and assignment.
$messagestringThe first message, with placeholders already resolved.
$udataarrayThe customer who will own the ticket.
Return 1
string|nullA non-empty text blocks the operation and is shown as the error. An empty return lets it carry on.
Listener
Hook::add('gate:ticket.admin_create', 10, function ($set_request, $message, $udata) {
    if (Acme::blacklisted((int) ($udata['id'] ?? 0))) return 'No ticket can be opened for this account.';

    return null;
});

Following a ticket being created

actionticket.created
AdminTickets first message included

Runs after a ticket is created.

Parameters 1
$ticketarrayThe full ticket, with the first message in it: owner, title, status, department, priority and assignment.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:ticket.created', 10, function ($ticket) {
    // The first message is not a separate parameter; it sits in the ticket.
    Acme::classify((int) ($ticket['id'] ?? 0), $ticket['message'] ?? '');
});

Following a status change

actionticket.status_changed
AdminTickets five base states

Runs after the status of a ticket changes.

Parameters 4
$ticketarrayThe snapshot from before the change.
$old_statusstringThe previous base state: open, waiting, in progress, answered or solved.
$new_statusstringThe new base state.
$new_cstatusintThe new custom state. An operator may define their own states; a zero means none. A rule looking only at the base state never sees them.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:ticket.status_changed', 10,
    function ($ticket, $old_status, $new_status, $new_cstatus) {
        // Custom states are a second axis beside the base state.
        if ($new_status === 'solved') Acme::stopSlaClock((int) ($ticket['id'] ?? 0));
    });

Following an automatic resolution

actionticket.auto_resolved
cronjobs with nobody touching it

Runs when a ticket left without a reply closes itself. Nobody closed it: silence did.

Parameters 3
$ticket_idintThe id of the resolved ticket.
$ticketarrayThe current row, its status already written as solved.
$delayed_dayintHow many days passed since the last reply.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:ticket.auto_resolved', 10, function ($ticket_id, $ticket, $delayed_day) {
    // Nobody closed it: silence did.
    Acme::surveyLater($ticket_id, $delayed_day);
});

Following staff opening a ticket

actionticket.viewed
AdminTickets already marked read

Runs when staff open a ticket.

Parameters 2
$ticketarrayThe loaded ticket. The unread mark is already cleared: you cannot answer "was it new" from here.
$idintThe ticket id.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:ticket.viewed', 10, function ($ticket, $id) {
    // The unread mark is already cleared.
    Acme::trackHandling($id);
});

Following a ticket being transferred

actionticket.transferred
AdminTickets owner change

Runs after a ticket is transferred to another customer.

Parameters 3
$ticketIdintThe id of the transferred ticket.
$fromUserIdintThe previous owner.
$toUserIdintThe new owner. Its history moves with it: a conversation the previous owner can no longer see now sits with the new one.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:ticket.transferred', 10, function ($ticketId, $fromUserId, $toUserId) {
    // The history moved with it.
    Acme::reindexOwner($ticketId, $toUserId);
});

Following a lock change

actionticket.lock_changed
AdminTickets replies close

Runs when a ticket is locked or unlocked. A customer cannot reply to a locked ticket.

Parameters 2
$ticketarrayThe record before the change.
$lockedintThe new lock state: one for locked, zero for open.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:ticket.lock_changed', 10, function ($ticket, $locked) {
    // A customer cannot reply to a locked ticket.
    if ($locked === 1) Acme::notifyLocked((int) ($ticket['user_id'] ?? 0));
});

Following a ticket being deleted

actionticket.deleted
AdminTickets the id only

Runs after a ticket is deleted.

Parameters 1
$ticket_idintThe id of the deleted ticket. No record is passed: if you need its content you must have kept it before the deletion.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:ticket.deleted', 10, function ($ticket_id) {
    // No record is passed: keep the content beforehand if you need it.
    Acme::dropFromIndex('ticket', $ticket_id);
});

Pitfalls

In the view hook the unread mark is already cleared

The hook runs when staff open a ticket, but the unread flag is cleared before it. You cannot answer "was this new" from here; track the first opening separately.

The delete hook carries only the id

Unlike other deletion hooks no ticket record is passed. If you need its title, owner or content you must have kept it beforehand; by the time the hook runs there is nowhere left to read it from.

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.