# Events

URL: https://better-helpdesk.com/docs/app/events/

Every HelpdeskEvent kind passed to onEvent, its payload, and what raises it.



`onEvent(event: HelpdeskEvent): Promise<void>` in the [configuration](/docs/reference/configuration) is called once a change is stored. `HelpdeskEvent` is a union told apart by `kind`, exported as a type from `better-helpdesk`. Related guide: [React to events](/docs/app/react-to-events).

The call is awaited inside the request that made the change, including the inbound email webhook and `runJobs`. What it throws is logged as `[helpdesk] onEvent {kind} failed` and does not change the response.

`Conversation` and `Message` are the stored rows, exported as types. Their fields are the columns of [`conversation`](/docs/database/tables#conversation) and [`message`](/docs/database/tables#message) in camel case.

## conversation.created [#conversationcreated]

<TypeTable
  type="{
  kind: { type: &#x22;'conversation.created'&#x22;, required: true },
  conversation: { type: 'Conversation', required: true, description: 'The new conversation.' },
  message: { type: 'Message', required: true, description: &#x22;The customer's first message.&#x22; },
}"
/>

Raised by:

* `POST widget/conversations`
* An inbound email that does not thread into an existing conversation
* `helpdesk.createConversation(request, input)` called from host code
* `POST agent/conversations` and `helpdesk.openConversation()`, where `message` is the agent's opening message

## message.created [#messagecreated]

<TypeTable
  type="{
  kind: { type: &#x22;'message.created'&#x22;, required: true },
  conversation: { type: 'Conversation', required: true, description: 'The conversation as it stands after the message.' },
  message: { type: 'Message', required: true },
  internal: { type: 'boolean', required: true, description: <><code>true</code> for an internal note, which only agents see.</> },
}"
/>

Raised by:

* An agent's reply or internal note, `POST agent/conversations/{id}/messages`, and each reply or note of `POST agent/conversations/bulk`
* `helpdesk.addNote()` from host code
* A customer's message on an existing conversation, `POST widget/conversations/{id}/messages`
* An inbound email that threads into an existing conversation

A customer message on a resolved conversation reopens it and raises only `message.created`; its `conversation.status` is `open`. The first message of a new conversation raises `conversation.created` instead. An automated inbound email (an out-of-office or a bounce) raises nothing. When it is signed and answers a conversation, it is stored there as an internal system note; otherwise it is dropped.

## sla.warning and sla.breach [#slawarning-and-slabreach]

<TypeTable
  type="{
  kind: { type: &#x22;'sla.warning' | 'sla.breach'&#x22;, required: true },
  conversation: { type: 'Conversation', required: true },
  target: { type: &#x22;'response' | 'resolution' | 'reply'&#x22;, required: true, description: <>Which target the notice is about; <code>reply</code> is the next-reply target.</> },
  due: { type: 'Date', required: true, description: 'The deadline.' },
}"
/>

Raised by `runJobs`, once per target per conversation: the warning once the configured share of the target has passed, the breach once the deadline has, each at the run that finds it. A conversation whose deadline is already past when the run looks gets the breach alone. The same run writes a timeline row and queues the `agent-sla` email. [Set service levels](/docs/inbox/sla) says what to hang on these, such as a PagerDuty incident.

## conversation.updated [#conversationupdated]

<TypeTable
  type="{
  kind: { type: &#x22;'conversation.updated'&#x22;, required: true },
  conversation: { type: 'Conversation', required: true, description: 'The conversation after the change.' },
  before: {
    type: 'Partial<Conversation>',
    required: true,
    description: <>The fields the change wrote whose value differs, as they were. A change that alters nothing raises no event. Fields written alongside, such as <code>resolvedAt</code> or <code>waitingSince</code>, appear too.</>,
  },
  agentId: {
    type: 'string | null',
    required: true,
    description: <>The agent who made the change, as the <code>agent</code> row's id; <code>null</code> when a customer or <code>runJobs</code> made it.</>,
  },
}"
/>

Raised by:

| Change                                                                 | `agentId`                    |
| ---------------------------------------------------------------------- | ---------------------------- |
| `PATCH agent/conversations/{id}`                                       | The agent                    |
| `helpdesk.changeConversation()` from host code                         | The agent named, else `null` |
| `POST agent/conversations/bulk`, once per conversation changed         | The agent                    |
| Accepting an AI suggestion, `POST agent/conversations/{id}/suggestion` | The agent                    |
| The customer resolving it, `PATCH widget/conversations/{id}`           | `null`                       |
| The customer rating it, in the widget or from an email link            | `null`                       |
| `runJobs` waking a snoozed conversation                                | `null`                       |

## Changes that raise no event [#changes-that-raise-no-event]

* Writes through `helpdesk.store`, so a handler that writes there cannot set itself off.
* Merging conversations, adding a participant, dismissing an AI suggestion, and the suggestion that triage stores.
* A customer sharing a conversation with their company, or no longer sharing it.
* Conversations unassigned because their agent was removed from the team.
* Changes to contacts, companies, deals and activities.

## Timeline rows [#timeline-rows]

A `conversation.updated` also writes rows to [`conversation_event`](/docs/database/tables#conversation_event) for each of these fields that changed: `status`, `priority`, `tier`, `assigneeId`, `type`, `inbox`, `title`, `tags`, `snoozedUntil`, `rating`. The row's `data` is `{ from, to }`, with `by: 'customer'` added when the customer made the change. The SLA fields written alongside a status change, such as `slaPausedAt`, appear in `before` but get no row.
