> ## Documentation Index
> Fetch the complete documentation index at: https://docs.yourhq.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Plugins

> Extend HQ with event-driven plugins that react to tasks, agents, knowledge, and more — locally or via webhooks.

HQ has a built-in plugin system that lets you hook into workspace events and take action. When a task completes, an agent goes over budget, or a knowledge item is created, plugins receive the event and run your code — post to Slack, sync to Linear, trigger a Zapier workflow, log an alert, or anything else.

Two plugin types, same event system:

* **Local plugins** — Python modules on the gateway. Full access to the plugin SDK (state, secrets, Supabase queries). Best for logic that needs to read HQ data or maintain state.
* **Webhook plugins** — Remote HTTP endpoints. Zero gateway code. HQ POSTs events with HMAC signatures. Best for connecting to external services, no-code tools, or teams without gateway access.

Both types are managed from **Settings → Plugins** in the UI.

***

## How it works

```
Workspace event          Plugin runner daemon           Your plugin
(task completed,    →    polls event queue,        →    local: Python handler
 agent provisioned,      matches subscribed hooks,      webhook: HTTP POST
 budget exceeded)        dispatches to plugins          with HMAC signature
```

Events originate from two sources:

1. **SQL triggers** on core tables (tasks, agents, knowledge, inbox, comments, secrets). These write to an internal event queue (`hq_plugin_event_queue`) that the plugin runner daemon polls.
2. **Python daemons** that emit events directly (command runner after provisioning, file processor after extraction, embedder after indexing, bootstrap plugin after usage recording).

The plugin runner daemon subscribes to the event queue via Supabase Realtime (with a polling fallback every 5 seconds) and dispatches matching events to all enabled plugins.

***

## Event model

Every event follows the same envelope:

```json theme={null}
{
  "event_id": "550e8400-e29b-41d4-a716-446655440000",
  "event_type": "task.completed",
  "occurred_at": "2025-01-15T10:30:00+00:00",
  "tenant_id": "00000000-0000-0000-0000-000000000000",
  "entity_type": "tasks",
  "entity_id": "d4f5e6a7-...",
  "payload": {
    "title": "Draft Q1 report",
    "assignee_agent_id": "...",
    "completed_at": "2025-01-15T10:30:00+00:00"
  }
}
```

Events are named `<entity>.<action>`. HQ ships with 16 built-in events covering tasks, agents, knowledge, inbox, routines, comments, secrets, usage, and budgets. See [Plugin events reference](/reference/plugin-events) for the full list.

***

## Plugin sources

Plugins come from four sources, tracked by the `source` field:

| Source        | Description                                                                | Can uninstall? |
| ------------- | -------------------------------------------------------------------------- | -------------- |
| `builtin`     | Ships with HQ (e.g. usage-alerts). Always available.                       | No             |
| `local`       | Python module on the gateway filesystem. For self-hosters and power users. | Yes            |
| `webhook`     | Remote HTTP endpoint. For SaaS integrations and no-code tools.             | Yes            |
| `marketplace` | Installed from the HQ marketplace (future).                                | Yes            |

***

## Local plugins

A local plugin is a Python module in `gateway/plugins/<plugin-id>/` with two files:

**`manifest.json`** — declares identity, hooks, config schema, and capabilities:

```json theme={null}
{
  "id": "slack-notify",
  "name": "Slack Notifications",
  "description": "Post to Slack when tasks complete or budgets exceed.",
  "version": "1.0.0",
  "source": "local",
  "hooks": ["task.completed", "budget.exceeded"],
  "config_schema": {
    "type": "object",
    "properties": {
      "webhook_url": {
        "type": "string",
        "title": "Slack Webhook URL"
      }
    },
    "required": ["webhook_url"]
  },
  "capabilities": ["secrets.read", "http.outbound"]
}
```

**`handler.py`** — subclasses `BasePlugin` and implements `on_event()`:

```python theme={null}
from gateway.plugins.sdk import BasePlugin, PluginEvent, PluginResponse

class Handler(BasePlugin):

    def on_event(self, event: PluginEvent) -> PluginResponse | None:
        if event.event_type == "task.completed":
            title = event.payload.get("title", "Unknown task")
            self.ctx.logger.info(f"Task completed: {title}")
            # Post to Slack, update Linear, etc.
            return PluginResponse(log_message=f"Notified about: {title}")
        return None
```

### Plugin context

Every local plugin receives a `PluginContext` with:

| Attribute  | Type             | Description                                      |
| ---------- | ---------------- | ------------------------------------------------ |
| `config`   | `dict`           | Operator-supplied config from Settings → Plugins |
| `state`    | `StateClient`    | Scoped key-value store (persists across events)  |
| `secrets`  | `SecretsClient`  | Read-only access to gateway secrets              |
| `supabase` | `SupabaseClient` | Read-only Supabase queries against HQ tables     |
| `logger`   | `Logger`         | Plugin-namespaced structured logger              |

### State management

Plugins can persist state across events using the `StateClient`. State is scoped by kind and optional entity:

```python theme={null}
# Global state
self.ctx.state.set("last_sync", "2025-01-15T10:30:00Z")
cursor = self.ctx.state.get("last_sync")

# Scoped to a specific agent
self.ctx.state.set("warned", True, scope_kind="agent", scope_id=agent_id)

# Scoped to a specific task
self.ctx.state.set("linear_issue_id", "LIN-42", scope_kind="task", scope_id=task_id)
```

State is stored in the `hq_plugin_state` table, keyed by `(plugin_id, scope_kind, scope_id, state_key)`.

***

## Webhook plugins

Webhook plugins have no gateway-side code. You register them entirely from the UI:

1. Go to **Settings → Plugins → Add plugin**
2. Enter a name, your endpoint URL, and optionally a signing secret
3. Select which events to receive
4. Save

HQ POSTs every matching event to your URL as JSON. The request includes:

| Header           | Description                                                 |
| ---------------- | ----------------------------------------------------------- |
| `Content-Type`   | `application/json`                                          |
| `X-HQ-Event`     | Event type (e.g. `task.completed`)                          |
| `X-HQ-Plugin-Id` | Your plugin's identifier                                    |
| `X-HQ-Delivery`  | Unique delivery ID for deduplication                        |
| `X-HQ-Signature` | `sha256=<hex>` HMAC-SHA256 signature (if secret configured) |

### Signature verification

If you set a signing secret, verify the `X-HQ-Signature` header before processing:

<CodeGroup>
  ```javascript Node.js theme={null}
  const crypto = require("crypto");

  function verify(secret, body, signature) {
    const expected = "sha256=" + crypto
      .createHmac("sha256", secret)
      .update(body)
      .digest("hex");
    return crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expected),
    );
  }
  ```

  ```python Python theme={null}
  import hashlib
  import hmac

  def verify(secret: str, body: bytes, signature: str) -> bool:
      expected = "sha256=" + hmac.new(
          secret.encode(), body, hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(expected, signature)
  ```
</CodeGroup>

### Webhook plugin examples

**Zapier integration** — register your Zapier catch hook URL as a webhook plugin subscribing to `task.completed`. Every completed task triggers your Zap.

**Discord notifications** — point a webhook plugin at a Discord webhook URL. HQ sends the event payload; Discord renders it as a message.

**Custom relay** — deploy a small service (Cloudflare Worker, AWS Lambda, Vercel Edge Function) that transforms HQ events into provider-specific formats (Slack Block Kit, Linear GraphQL mutations, PagerDuty alerts).

***

## Capabilities

Plugins declare what they need in `manifest.json → capabilities[]`. This documents intent and will be enforced when sandboxing is added:

| Capability      | Grants                                                 |
| --------------- | ------------------------------------------------------ |
| `secrets.read`  | Access to `ctx.secrets.resolve()`                      |
| `http.outbound` | Permission to make external HTTP requests              |
| `supabase.read` | Access to `ctx.supabase.query()` for reading HQ tables |
| `state.read`    | Access to `ctx.state.get()`                            |
| `state.write`   | Access to `ctx.state.set()` and `ctx.state.delete()`   |

***

## Execution and observability

Every plugin dispatch is logged in the `hq_plugin_events` table with:

* Plugin ID and hook name
* Status (`success`, `error`, `timeout`, `skipped`)
* Duration in milliseconds
* Error message (if any)

You can see recent activity for any plugin in **Settings → Plugins → click a plugin → Recent activity**. Events are retained for 30 days.

***

## Architecture

### Database tables

| Table                   | Purpose                                                                                               |
| ----------------------- | ----------------------------------------------------------------------------------------------------- |
| `hq_plugins`            | Plugin registry — one row per installed plugin. Realtime-enabled for live UI updates.                 |
| `hq_plugin_events`      | Execution log — every dispatch recorded for observability. 30-day retention via pg\_cron.             |
| `hq_plugin_state`       | Scoped key-value store — plugins persist state across events.                                         |
| `hq_plugin_event_queue` | Internal event queue — SQL triggers write here; plugin runner reads and dispatches. 1-hour retention. |

### Gateway daemon

The plugin runner (`gateway/daemons/plugin_runner.py`) follows the same patterns as the command runner and inbox dispatcher:

1. Connects to Supabase on startup
2. Loads enabled plugins from `hq_plugins`
3. Subscribes to `hq_plugin_event_queue` via Realtime for new events
4. Subscribes to `hq_plugins` for config changes (enable/disable/update → hot reload)
5. Dispatches matching events to plugins (in-process for local, HTTP POST for webhook)
6. Records results in `hq_plugin_events`
7. Falls back to polling every 5 seconds if Realtime disconnects

### Relationship to other systems

**hq-bootstrap** (the OpenClaw plugin) hooks into the LLM call lifecycle — model routing, usage logging, budget enforcement. It's adapter-specific. The HQ plugin system hooks into the business logic lifecycle — tasks, agents, knowledge, inbox. They coexist: hq-bootstrap forwards `usage.recorded` and `budget.exceeded` events to the plugin event queue.

**Source connectors** (`gateway/connectors/`) sync external data into HQ's knowledge system. They're purpose-built for data ingestion. Plugins react to HQ events and take actions. If you want to sync GitHub Wiki → HQ knowledge, use a connector. If you want to create Linear issues when HQ tasks are created, use a plugin.

***

## Built-in plugins

### Usage Alerts

Ships with HQ. Monitors `usage.recorded` events and logs warnings when agents approach their budget threshold (default: 80%). Uses the plugin state system to avoid duplicate warnings.

Configuration:

| Field             | Type   | Default | Description                                    |
| ----------------- | ------ | ------- | ---------------------------------------------- |
| `warn_at_percent` | number | 80      | Percentage of budget at which to log a warning |

***

## Next reads

* [Plugin events reference](/reference/plugin-events) — full list of all 16 events with payloads
* [Creating plugins guide](/guides/plugins) — step-by-step walkthrough for your first plugin
* [Architecture](/concepts/architecture) — how plugins fit into the broader system
* [Contributing](/development/contributing) — how to submit a plugin PR
