# Service levels

> First response, next response and resolution clocks on any record, from a policy per record type and priority, in business hours, paused while the record waits, with warnings and breaches a job records.

Source: https://bettersupabase.com/docs/blocks/sla

`sla` puts service-level clocks on any record in [`config.subjects`](/docs/blocks/sql#subject-registry): inbox conversations, tickets, incidents, [work items](/docs/blocks/work-items). A policy per record type and priority says how many minutes a first response, a next response and the resolution may take, and whether only business hours count. A record's clocks start when it is created, pause while it waits on the customer, close when it is answered or resolved, and a job records the ones that warn or breach.

Reading policies and clocks needs `sla.read`, and changing policies needs `sla.manage`. Admins get both, and members get `sla.read`.

```bash
better-supabase sql add sla
```

The module needs `tenant`, `access` and `subjects`.

```ts title="better-supabase.config.ts"
sql: {
  modules: {
    sla: {
      options: {
        businessHours: { timeZone: "Europe/Amsterdam", start: "09:00", end: "17:00", days: [1, 2, 3, 4, 5] },
        subjects: {
          ticket: { status: "status", priority: "priority", pause: ["waiting"], resolve: ["solved", "closed"] },
        },
      },
    },
  },
},
```

## Policies [#policies]

```ts
import { createSla } from "better-supabase/blocks/sla";

const sla = createSla({ transport });
await sla
  .savePolicy(organizationId, {
    subjectType: "ticket",
    name: "Normal",
    firstResponseMinutes: 240,
    nextResponseMinutes: 120,
    resolutionMinutes: 2880,
    businessHours: true,
  })
  .orThrow();
await sla
  .savePolicy(organizationId, {
    subjectType: "ticket",
    name: "Urgent",
    priority: "urgent",
    firstResponseMinutes: 30,
    resolutionMinutes: 240,
    warnAtPercent: 50,
  })
  .orThrow();
```

A record uses the policy for its type and priority, or the policy without a priority. One active policy covers each type and priority (`SLA_POLICY_TAKEN`). `warnAtPercent` (default 80) is when a clock warns. With `businessHours`, only the hours of `options.businessHours` count, in its time zone (UTC, 09:00 to 17:00, Monday to Friday by default); `dueAt(start, minutes, businessHours)` shows the result.

## Clocks [#clocks]

Bind a record type in `options.subjects` and the module drives its clocks from the record's own columns:

| Change                               | Clocks                                                         |
| ------------------------------------ | -------------------------------------------------------------- |
| a record is created                  | first response and resolution start                            |
| its priority changes                 | the open clocks take the new policy's minutes from their start |
| its status enters a `pause` status   | every open clock pauses                                        |
| its status leaves a `pause` status   | they resume, and the due times move by the pause               |
| its status enters a `resolve` status | every open clock is met                                        |
| its status leaves a `resolve` status | a new resolution clock starts                                  |

A reply is an event only the app knows about: call `record(subject, "responded")` when an agent answers. It meets the first response, and with a next-response time and the record still open, it starts a next-response clock. Without a binding, call `start(subject, priority)` and `record(subject, event)` with `resolved`, `reopened`, `paused` and `resumed` yourself.

`clocks(subject)` returns a record's clocks with their state (`running`, `paused`, `met`, `breached`) and the seconds left, for a ticket's timer.

## Warnings and breaches [#warnings-and-breaches]

Run `check()` every minute from a [job](/docs/blocks/jobs) with the service role. It records `sla.warned` once a running clock passes its warning share and `sla.breached` once it passes its due time, and, with the [notifications](/docs/blocks/notifications) module installed, notifies the record's watchers at high priority. Escalate from those events with a [workflow](/docs/blocks/workflows), such as reassigning a breached ticket. A clock met after its due time records `sla.completed` with `late: true`.

## Events [#events]

| Event                                         | Data                                                                               |
| --------------------------------------------- | ---------------------------------------------------------------------------------- |
| `sla.warned`, `sla.breached`, `sla.completed` | `organizationId`, `clockId`, `subjectType`, `subjectId`, `metric`, `dueAt`, `late` |

## Wire it to ReUI [#wire-it-to-reui]

The ReUI Support Escalations board shows each ticket's SLA timer and turns it to "Paused" while it waits on the customer: read `clocks(ticket)` and show the open clock's `secondsLeft`, or "Paused" when its state is `paused`.

## Errors [#errors]

| Hint                   | Cause                                                     |
| ---------------------- | --------------------------------------------------------- |
| `SLA_FORBIDDEN`        | the caller lacks `sla.manage`, or can't read the record   |
| `SLA_POLICY_TAKEN`     | another active policy covers the type and priority        |
| `SLA_POLICY_INVALID`   | a policy needs a name, a record type and positive minutes |
| `SLA_POLICY_NOT_FOUND` | no such policy                                            |
| `SLA_SUBJECT_INVALID`  | the record isn't in `config.subjects`                     |
| `SLA_EVENT_INVALID`    | not one of the events                                     |
| `SLA_HOURS_INVALID`    | the business hours never open                             |