# Hooks reference

Core exposes a deliberately small extension surface. Every public hook is a compatibility promise, so the catalog stays short on purpose.

The authoritative machine-readable catalog is `App\Support\CoreHooks::catalog()`. Reference the constants rather than typing strings — a rename is then caught by code review and tests.

```php
use App\Support\CoreHooks;
use App\Support\Hooks;

Hooks::on(CoreHooks::QUESTION_PUBLISHED, function (array $payload): void {
    // $payload['question_id'] is available here
});
```

## Catalog

| Hook | Type | Fires when |
|---|---|---|
| `user.registered` | action | A member registration was committed |
| `question.created` | action | A question was created, including pending questions |
| `question.published` | action | A question became publicly visible |
| `answer.created` | action | An answer was created, including pending answers |
| `answer.published` | action | An answer became publicly visible |
| `answer.accepted` | action | An answer was accepted for a question |
| `comment.created` | action | A question, answer, or topic comment was created |
| `topic.created` | action | A topic was created, including pending topics |
| `topic.published` | action | A topic became publicly visible |
| `topic.updated` | action | A topic changed |
| `topic.comment_created` | action | A published comment was added to a topic |
| `topic.locked` | action | A topic was locked |
| `topic.unlocked` | action | A topic was unlocked |
| `seo.meta` | filter | Filter safe public meta values before rendering |
| `seo.structured_data` | filter | Filter JSON-LD data before rendering |
| `sitemap.entries` | filter | Filter a generated sitemap entry list |
| `spam.assessment` | filter | Adjust a spam assessment after core scoring |
| `widgets.register` | filter | Extend the public widget registry |
| `menu.items` | filter | Filter visible managed menu items |
| `notification.channels` | filter | Extend the notification channel registry |

## Actions versus filters

**Actions** report that something happened. Their return value is ignored.

**Filters** receive a value and return a possibly modified value. Always return a value of the same shape; returning `null` or the wrong type is treated as "no change" where the core can detect it, and is a bug in your plugin either way.

## Guarantees

- **Post-success events fire after the state is committed.** A handler for `question.published` can safely read the published question.
- **Handler exceptions are isolated and logged.** One failing plugin does not break the request, the site, or the admin panel.
- **Security checks cannot be bypassed by a filter.** Returning `true` from a generic filter does not grant permission, skip CSRF validation, or publish content that policy holds.

## Payload stability

A payload may gain keys in a later release. It will not silently change the meaning of an existing key. Read defensively:

```php
$questionId = (int) ($payload['question_id'] ?? 0);
if ($questionId <= 0) {
    return;
}
```

## Performance

Handlers run inside the request. Do not perform slow network calls in a hook that fires on every page render. There is no queue and no cron in Quaeriva — if your work is slow, make it slow on an admin action, not on a visitor's page load.

See also [Hooks and events](../07-internals/HOOKS-EVENTS.md) and [Plugins](PLUGINS.md).
