# Events and hooks

Reference: [Events index](events.md) (every event, payload and emit site), [Cancel and modify](cancellable.md) (everything you can stop or change) and [Hook points index](hook-points.md).

PeltyCMS emits a **domain event** after something has happened, for example `invoice.paid`. Before a few sensitive actions it also runs a **pre-event**, for example `order.before_place`, which a hook can cancel or modify. Every name, payload field and flag is listed in the [event registry](events.md). That registry is checked by a test that scans the source for every emitted event.

## Who can receive events

- **WASM addons** list domain events in `capabilities.events`. The host calls `on-event(name, payload)` after the action has been committed.
- **WASM hooks** list event names in `capabilities.hooks`. These are pre-events, lifecycle phases such as `before:PUT /api/v1/client/profile`, or custom events. The host calls op `hook`.
- **Webhook plugins** declare `hooks` in their plugin manifest. The host sends each event as a signed HTTPS POST to `webhook_url`.

## Listening

_extensions/examples/addon-guide/src/lib.rs · snippet listen_

```rust
/// Domain events listed in `capabilities.events` arrive here after the fact.
/// They cannot be cancelled; use them to sync, notify or count.
fn on_event(name: &str, payload: &Value) {
    match name {
        "invoice.paid" => bump("paid"),
        "user.registered" => {
            bump("registered");
            let _ = host::kv_set("last_user", payload["user_id"].as_str().unwrap_or(""));
        }
        _ => {}
    }
}
```

## Cancelling and modifying

_extensions/examples/addon-guide/src/lib.rs · snippet hooks_

```rust
/// Pre-events and lifecycle phases listed in `capabilities.hooks` call op
/// `hook`. Return `proceed`, `modify(payload)` or `cancel(reason)`.
fn hook(input: &Value) -> Value {
    let event = input["event"].as_str().unwrap_or("");
    if input["phase"].as_str().unwrap_or("before") != "before" {
        return reply::proceed(); // during/after/finished only observe
    }
    let mut p = input["payload"].clone();
    bump(&format!("hook:{event}"));
    match event {
        // Cancel: refuse sign-in for one domain.
        "login.before" if p["email"].as_str().is_some_and(|e| e.ends_with("@denied.test")) => reply::cancel("domain not allowed"),
        // Modify: tidy up the display name before the account is created.
        "client.before_register" => {
            p["display_name"] = json!(p["display_name"].as_str().unwrap_or("").trim().to_string());
            reply::modify(p)
        }
        // Own custom event (declared as `before:integration-guide:greeted`,
        // delivered with `event` = the bare name and `phase` = "before").
        "integration-guide:greeted" if p["name"] == "nobody" => reply::cancel("no greeting for nobody"),
        "order.before_place" => order_hook(input, p),
        _ => reply::proceed(),
    }
}
```

A hook replies with one of the following:

- `{"action":"continue"}`: let it through.
- `{"action":"modify","payload":{…}}`: replace the payload. Money fields can only change for addons holding `money.modify`.
- `{"action":"cancel","reason":"…"}`: stop the action.
- `{"action":"uncancel"}`: undo an earlier cancellation. This only works with `ignore_cancelled: true`.

## Lifecycle phases

Phases in order: `before`, `during`, `after`, `finished`. Source: `crates/pelty-api/src/addon_events.rs`.

- `before`: synchronous, in priority order; listeners may `modify` the payload or `cancel` it (`uncancel` too when `ignore_cancelled`). `monitor` listeners only observe. Money and secret rules apply.
- `during`: delivered while the action runs (concurrently, observe only).
- `after`: the action succeeded; the result is in the payload.
- `finished`: always last, with `status` = `ok` | `failed` | `cancelled`.

## Custom events

An addon can emit `<its-own-id>:<event>` if the name is listed in `capabilities.emits`. The `before` phase runs synchronously through every listener, in priority order. Other addons subscribe with `before:<id>:<event>` (and `after:` or `finished:`) in their `hooks`. An addon cannot emit an event in another addon's namespace.

_extensions/examples/addon-guide/src/lib.rs · snippet emit_

```rust
/// Emit a custom event (`capabilities.emits`, name `<addon-id>:<event>`).
/// The `before` phase runs synchronously, so listeners can cancel or modify it.
fn greet(name: &str) -> Result<Value, String> {
    let e = pelty_addon_sdk::emit("integration-guide:greeted", &json!({ "name": name }))?;
    if e.cancelled {
        return Ok(json!({ "status": 409, "body": { "error": e.reason } }));
    }
    bump("greeted");
    Ok(json!({ "status": 200, "body": { "greeted": e.payload["name"] } }))
}
```

