Staff Reply Hooks

1 views Markdown

The eight hooks over staff replies, internal notes and assignment.

Overview

Every point where staff touch a ticket lives here: writing a reply, editing it, deleting it, adding an internal note and assigning.

There are three reply hooks and they should not be confused: the one staff write, the one a customer writes and the one a scheduled task writes. The last carries its parameters in a single array.

Reference

Stopping a staff reply

gateticket.reply
AdminTickets signature attached

Runs before a staff reply is sent.

Parameters 3
$ticketarrayThe ticket being replied to.
$messagestringThe final text. It carries markup and the signature is already attached: allow for that if you measure length.
$admin_idintThe staff member writing it.
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.reply', 10, function ($ticket, $message, $admin_id) {
    // The signature is already in: allow for it when measuring length.
    if (Acme::containsSecret($message)) return 'The reply holds a value that must not be shared.';

    return null;
});

Following a staff reply

actionticket.reply_added
AdminTickets hydrated reply

Runs after a staff reply is added.

Parameters 3
$ticketarrayThe fresh ticket after the reply.
$reply_idintThe id of the reply added.
$replyarrayThe whole reply: its text, author, attachments and address.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:ticket.reply_added', 10, function ($ticket, $reply_id, $reply) {
    Acme::stopSlaClock((int) ($ticket['id'] ?? 0));
});

Following an automatic reply

actionticket.replied
cronjobs one context array

Runs when a scheduled task adds a reply to a ticket. Unlike its siblings the parameters arrive in a single array.

Parameters 1
$contextarrayEverything is here: the source, the current ticket and the reply added. Expect no separate parameters; this hook carries one array.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:ticket.replied', 10, function ($context) {
    // One array: different from its siblings.
    $ticket = $context['request'] ?? [];
    Acme::noteAutoReply((int) ($ticket['id'] ?? 0));
});

Following a reply being edited

actionticket.reply_updated
AdminTickets after the edit

Runs after a reply is edited.

Parameters 3
$ticketIdintThe ticket id.
$replyIdintThe id of the edited reply.
$replyarrayThe updated reply. The previous version is not passed: to compare, you must have kept a copy yourself.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:ticket.reply_updated', 10, function ($ticketId, $replyId, $reply) {
    // The previous version is not passed.
    Acme::reindexReply($replyId, $reply['message'] ?? '');
});

Following a reply being deleted

actionticket.reply_deleted
AdminTickets after deletion

Runs after a reply is deleted.

Parameters 3
$ticketIdintThe ticket id.
$replyIdintThe id of the deleted reply.
$replyarrayThe deleted reply record: its owner and whether it was from staff.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:ticket.reply_deleted', 10, function ($ticketId, $replyId, $reply) {
    Acme::dropFromIndex('reply', $replyId);
});

Stopping an internal note

gateticket.note_add
AdminTickets the customer never sees it

Runs before staff add an internal note. Notes are never shown to the customer.

Parameters 3
$ticketarrayThe ticket the note goes on.
$messagestringThe note content.
$admin_idintThe staff member adding it.
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.note_add', 10, function ($ticket, $message, $admin_id) {
    if (Acme::tooLong($message)) return 'The note is too long.';

    return null;
});

Following an internal note

actionticket.note_added
AdminTickets the note may be empty

Runs after an internal note is added.

Parameters 3
$ticketarrayThe ticket the note went on.
$admin_idintThe staff member who added it.
$notearrayThe note data: its text, whether it is pinned, and attachments. ? It can arrive empty when the note could not be read. Test before reaching into it.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:ticket.note_added', 10, function ($ticket, $admin_id, $note) {
    // The note CAN be empty.
    if (!$note) return;

    Acme::mirrorNote((int) ($ticket['id'] ?? 0), $note['message'] ?? '');
});

Following an assignment

actionticket.assigned
AdminTickets zero means unassigned

Runs when a ticket is assigned to staff or unassigned.

Parameters 3
$ticketarrayThe ticket before the assignment.
$old_assigned_idintThe previous assignee; a zero means none.
$new_assigned_idintThe new assignee; a zero means it was unassigned.
Return 1
voidThe return is ignored.
Listener
Hook::add('action:ticket.assigned', 10,
    function ($ticket, $old_assigned_id, $new_assigned_id) {
        // Zero means it was unassigned.
        if (!$new_assigned_id) { Acme::backToPool((int) ($ticket['id'] ?? 0)); return; }

        Acme::notifyAgent($new_assigned_id, (int) ($ticket['id'] ?? 0));
    });

Pitfalls

The note data can be empty

The third parameter of the internal note hook is empty when the note could not be read. A listener reaching straight into it fails there; test on the first line.

The automatic reply hook carries one array

Its siblings give the ticket, the reply id and the reply as separate parameters; the scheduled-task reply carries all of it in a single context array. A listener expecting the same signature works with empty values here.

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.