# Staff Reply Hooks

https://dev.wisecp.com/uk/staff-reply-hooks

The nine 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.

There is one more question, separate from the content: **whose name** goes on the message. The author hook settles that before the message is written.

## Reference

### Changing the author name and signature

filterticket.reply_author

`AdminTickets` values by reference also runs on preview

Changes the **author name the customer sees** on a staff message, and the signature attached to it, before the message is written.

Parameters 3

&$namestringThe name shown on the reply. Only the name changes: the real staff record stays, and the **avatar is read from that account**. When the name and the account diverge, the staff thread shows someone else's photo.

&$signaturestringThe signature appended to the message; you give plain text and the core escapes it. Leave it empty and no signature is added.

$ctxarrayContext; you cannot change it. Its keys: `ticket_id` (still zero on the ticket creation form), `lang` (the language the ticket was opened in), `user_id` (the ticket owner; zero for a guest ticket), `staff_id`, `source`, `explicit_name`, `ai`. `source` takes five values. Write paths: `panel`, `panel-create`, `api`. Preview paths: `panel-form`, `panel-create-form`.

Return 1

voidThe return is ignored; you change both values **in place**. When `explicit_name` is true the caller chose the author deliberately; leave the name alone.

Listener PHP

```php
Hook::add('filter:ticket.reply_author', 10, function (&$name, &$signature, $ctx) {
    // The caller named its own author: API author_name, an AI persona. Leave it alone.
    if (!empty($ctx['explicit_name'])) return;

    // The preview paths run on every page load: cause no side effects here.
    $agent = Acme::pickAgent((int) ($ctx['ticket_id'] ?? 0), (string) ($ctx['lang'] ?? ''));
    if (!$agent) return;

    $name      = $agent['name'];
    $signature = $agent['signature'];
});
```

### 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|null**A non-empty text blocks the operation** and is shown as the error. An empty return lets it carry on.

Listener PHP

```php
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 PHP

```php
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 PHP

```php
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 PHP

```php
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 PHP

```php
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|null**A non-empty text blocks the operation** and is shown as the error. An empty return lets it carry on.

Listener PHP

```php
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 PHP

```php
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 PHP

```php
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 author hook also runs on preview**
> 
> The signature box on the reply form takes its value from this hook. The hook then runs **again** when the reply is written. That has two consequences. Your listener also runs on every page load, so it must not bump a counter or write a record there. And if you set the signature unconditionally, the box is a preview rather than an input: an edit the operator makes in it is **overwritten** at write time.

> **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.

## Related Articles

- Support Ticket Hooks
- [Customer Account Hooks](https://dev.wisecp.com/en/customer-account-hooks)
- [How Hooks Work](https://dev.wisecp.com/en/how-hooks-work)
