---
title: Hooks and events
description: Change a record before it is written, say what happened, and act on it elsewhere.
section: Server
order: 3
---

# Hooks and events

<p class="lead">A hook runs before a write and can modify or reject it. When something notable happens it emits an event, and agents react to it: send an email, write an audit row, emit the next event. Each piece stays small and independent.</p>

## Hooks

A file in `hooks/` named after a collection runs before each write to it. `beforeCreate`, `beforeUpdate` and `beforeDelete` receive the record and return it, modified if needed; `reject` aborts 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` holds the changes; `previous` is the current row.
  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 hook, for cross-cutting logic.

## Events

`emit(name, data)` publishes an event. From a hook, events are only published once the write commits; a rejected write emits nothing except alerts. Server functions and agents can emit too.

An agent watching an event receives 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 });
}
```

Another agent can react to that:

```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 doesn't send email; it just announces the order is paid. What happens next is up to the agents, and adding one doesn't touch existing code. Event chains deeper than eight are rejected, so two agents can't loop forever.

## Alerts

`alert(message, details)` flags something for a human. It's logged, counted in the dev overlay, and emitted as an `alert` event so an agent can forward it:

```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

External programs can emit events 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`) are all delivered as JSON; other payloads as `{ "bytes": "<base64>" }`. `?app=reports` targets only the app mounted as `reports`.

Locally, without a token, the server can also listen on 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` by default. Each datagram is a map with `name`, `data` and optionally `app`, encoded as JSON, MessagePack or CBOR. A sensor, script or cron job can send one in a single line.
