# Ticket Lifecycle Hooks

https://dev.wisecp.com/es/ticket-lifecycle-hooks

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

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

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

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

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

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

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

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

## 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)
