---
title: 'Automations & webhooks'
group: 'data-manager'
---

## Automations

Automations let nuzur call your systems when your data changes — for example, when a record's `status` becomes `PUBLISHED`, nuzur can POST a signed webhook to your API so it can react (send a notification, kick off a job, sync another service).

The key property: **automations only fire on approved changes.** Every data modification in nuzur goes through a change request, and an automation runs only when a change request is approved and applied. Nothing — not a teammate, not an AI agent — can trigger your webhook without that change having gone through review. Your receiving system only ever sees changes a human signed off on.

## Create an automation

Open the **Data Manager** for your project and switch to the **Automations** tab in the left panel (next to **Entities**), then click **New automation**.

An automation has:

- **Name** — a human label, e.g. `Episode published → scoring`.
- **Entity** — the table to watch.
- **Operation** — fire on `create`, `update`, `delete`, or `any`.
- **Conditions (optional)** — one or more "when field X changes to Y" rules, combined with **AND** (all must hold in the same change; default) or **OR** (any one is enough). The single-field transition is the most common pattern for lifecycle-driven automations: a record is created as a draft and later *updated* to `PUBLISHED`. For enum fields the "changes to" input offers the enum's options directly. A condition only fires on a real transition — an update that leaves the field at a value it already had doesn't count. If you need anything fancier than AND/OR field transitions, leave the conditions empty and filter on the receiving side — your endpoint may receive events it doesn't care about; check the payload.
- **Webhook URL** — where nuzur POSTs the event. Must be `https` and publicly reachable (private and local addresses are rejected).

When you create the automation, nuzur shows you the **signing secret exactly once**. Copy it and store it in your receiving service's configuration — it is used to verify that deliveries really come from nuzur, and it cannot be retrieved later.

If the secret is lost or may have leaked, open the automation and click **Rotate secret** (or use the `rotateAutomationSecret` MCP tool). A new secret is generated and shown once; the old one stops signing new deliveries within about a minute. There is no dual-secret overlap, so update your receiver's configuration immediately after rotating — deliveries that fail during the switch retry automatically with the new signature.

You can also manage automations from your AI tools through the [nuzur MCP server](/docs/mcp-connect) — the `createAutomation`, `testAutomation`, and `listAutomationEvents` tools mirror everything described here.

## What your endpoint receives

nuzur POSTs a JSON body shaped like this:

```json
{
  "event_uuid": "5f0c2f6e-6a41-4e6e-9b1a-2f8f6f2f7c11",
  "sent_at": "2026-07-13T18:04:05Z",
  "automation": { "uuid": "…", "name": "Episode published → scoring" },
  "project_uuid": "…",
  "entity": { "uuid": "…", "identifier": "episode" },
  "operation": "update",
  "record": {
    "keys": { "uuid": "d3b1…" },
    "changes": {
      "status": { "old": "DRAFT", "new": "PUBLISHED" }
    }
  },
  "change_request": {
    "uuid": "…",
    "title": "Publish episode 12",
    "approved_by_uuid": "…",
    "applied_at": "2026-07-13T18:04:01Z"
  }
}
```

- `record.keys` identifies the row; `record.changes` carries old/new values for updates; creates carry `record.values` instead; deletes carry only `keys`.
- `change_request` tells you *who approved it and when* — provenance no database-level CDC tool can give you.
- Test deliveries (from the **Send test delivery** button) additionally carry `"test": true`.

Delivery rules:

- **Any 2xx response counts as success.** Respond quickly and do your work asynchronously — the request times out after 10 seconds.
- **Delivery is at-least-once and unordered.** Duplicates are possible; deduplicate on `event_uuid`.
- Failed deliveries retry with backoff (1m, 5m, 30m, 2h, 12h) and are then marked dead. You can inspect and retry them from the **Deliveries** log on each automation.
- Redirects are not followed.

## Verify the signature

Every delivery includes two headers:

```
X-Nuzur-Event-Id: 5f0c2f6e-6a41-4e6e-9b1a-2f8f6f2f7c11
X-Nuzur-Signature: sha256=8f2c1a…
```

The signature is the **hex HMAC-SHA256 of the raw request body**, computed with your signing secret. Verify it before trusting the payload:

1. Read the **raw bytes** of the request body — before any JSON parsing or middleware touches it. Re-serialized JSON will not match.
2. Compute `HMAC-SHA256(raw_body, secret)` and hex-encode it.
3. Compare against the header value (after the `sha256=` prefix) using a **constant-time comparison**.
4. Reject anything that doesn't match, then deduplicate on `event_uuid`. Optionally reject events whose `sent_at` is older than a few minutes to narrow the replay window.

### Go

```go
func verifyNuzurSignature(rawBody []byte, header, secret string) bool {
	sig, ok := strings.CutPrefix(header, "sha256=")
	if !ok {
		return false
	}
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write(rawBody)
	expected := hex.EncodeToString(mac.Sum(nil))
	return hmac.Equal([]byte(expected), []byte(sig))
}

func handler(w http.ResponseWriter, r *http.Request) {
	rawBody, _ := io.ReadAll(http.MaxBytesReader(w, r.Body, 1<<20))
	if !verifyNuzurSignature(rawBody, r.Header.Get("X-Nuzur-Signature"), os.Getenv("NUZUR_SIGNING_SECRET")) {
		http.Error(w, "invalid signature", http.StatusUnauthorized)
		return
	}
	// parse rawBody, dedupe on event_uuid, then do your work async
	w.WriteHeader(http.StatusOK)
}
```

### Node

```js
import crypto from "node:crypto";
import express from "express";

const app = express();

// capture the RAW body — express.json() alone re-parses it and the
// signature will not match
app.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf; } }));

function verifyNuzurSignature(rawBody, header, secret) {
  if (!header?.startsWith("sha256=")) return false;
  const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  const received = header.slice("sha256=".length);
  return (
    expected.length === received.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))
  );
}

app.post("/hooks/nuzur", (req, res) => {
  if (!verifyNuzurSignature(req.rawBody, req.get("X-Nuzur-Signature"), process.env.NUZUR_SIGNING_SECRET)) {
    return res.status(401).send("invalid signature");
  }
  // dedupe on req.body.event_uuid, then do your work async
  res.sendStatus(200);
});
```

## Test before you ship

Use the **Send test delivery** button (or the `testAutomation` MCP tool) to POST a synthetic, fully signed event to your endpoint — the payload has the exact shape of a real delivery with sample values and `"test": true`. You can build and verify your receiver end to end before any real change request exists.

## Monitoring deliveries

Each automation has a **Deliveries** log: every event with its status (`pending`, `delivering`, `delivered`, `failed`, `dead`), attempt count, last error, and the frozen payload. Failed and dead events can be retried manually.

Two things to know:

- If an endpoint keeps failing, the automation is flagged **needs attention** after several consecutive dead events and stops firing — the configuration is kept, and a banner appears in the Automations tab. Fix the endpoint and save the automation to reactivate it.
- If a schema change removes the entity or the condition field an automation watches, it is also flagged **needs attention** at publish time instead of firing incorrectly.

## Next steps

- [Managing data](/docs/data-manager-manage-data)
- [Change request reviews](/docs/collaboration-change-requests)
- [Connecting to the nuzur MCP Server](/docs/mcp-connect)
