---
title: Hooks and events
description: Change a record before it is written, say what happened, and act on it elsewhere.
section: Server
order: 4
---

# Hooks and events

<p class="lead">A hook runs before a write and may change or refuse it. When something worth knowing happens, it says so with an event, and agents elsewhere act on it: send the email, write the audit row, say the next thing. Each piece stays small, and none knows about the others.</p>

## Hooks

A file in `hooks/` named after a collection runs before each write to it. `beforeCreate`, `beforeUpdate` and `beforeDelete` each get the record and return it, changed if need be; `reject` refuses the write with a message.

```ts title="hooks/orders.ts"
export function beforeCreate({ record, reject, alert }) {
  if (String(record.note ?? "").includes("<script")) {
    alert("A script in an order's note", { email: record.email });
    reject("That note is not allowed.");
  }
  // Card numbers never reach the database.
  return { ...record, note: String(record.note ?? "").replace(/\d{16}/g, "[card]") };
}

export function beforeUpdate({ record, previous, emit }) {
  // `record` is what changes; `previous` is the row as it was.
  const order = { ...previous, ...record };
  if (order.paid && !previous.paid) emit("order.paid", { id: order.id, email: order.email });
  return record;
}
```

`hooks/_all.ts` runs before every collection's own hook, for what applies everywhere.

## Events

`emit(name, data)` says something happened. From a hook, it is said only once the write has gone through: a refused write says nothing, except its alerts. Server functions and agents can emit too.

An agent that watches an event hears each one, in order:

```ts title="agents/receipts.ts"
export const watch = { event: "order.paid" };

export async function act({ event, mail, emit }) {
  mail({ to: event.data.email, subject: "Paid, thank you", text: `Order ${event.data.id} is paid.` });
  emit("receipt.sent", { order: event.data.id });
}
```

And another may listen to that one:

```ts title="agents/audit.ts"
export const watch = { event: "receipt.sent" };

export async function act({ event, server }) {
  server.collection("audit").create({ what: `receipt for ${event.data.order}` });
}
```

The hook does not send email; it says the order is paid. What follows from that is each agent's business, and adding one changes nothing that was there. A chain of events more than eight deep is refused, so two agents answering each other cannot run forever.

## Alerts

`alert(message, details)` is for what someone should look at. It is written to the log, counted in the development overlay, and said as an `alert` event, so an agent can pass it on:

```ts title="agents/alerts.ts"
export const watch = { event: "alert" };

export async function act({ event, mail }) {
  mail({ to: "ops@example.com", subject: "Alert", text: event.data.message });
}
```

## Events from outside

Another program can say an event too. Over HTTP, as a superuser:

```sh title="Terminal"
curl -X POST http://localhost:8090/api/events/door.opened \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"door": 3}'
```

JSON, MessagePack (`application/msgpack`) and CBOR (`application/cbor`) all arrive as JSON. Other bytes arrive as `{ "bytes": "<base64>" }`. `?app=reports` says the event only to the app mounted as `reports`.

On the same machine, without a token, the server can listen over UDP:

```sh title="Terminal"
sluurp serve --public ./app --events
# from anything else on this machine:
sluurp emit door.opened '{"door": 3}'
```

`--events` listens on `127.0.0.1:9900` unless given another address. Each datagram is a map of `name`, `data` and, optionally, `app`, written in JSON, MessagePack or CBOR. A sensor, a script or a cron job can each send one in a line of code.
