# Triggers

A trigger subscribes to a provider kind. Exactly one of `prompt` or `handler` must be set. Eval throws otherwise. A trigger whose `provider` or `kind` is missing from the collected providers fails eval. Two providers on one agent may not share a route path.

```
triggerProvider = {
  id = "hooks";
  auth = {
    env = "WEBHOOK_SECRET";
    header = "x-webhook-token";
  };
  routes = [{ path = "webhooks/hooks/:trigger"; }];
  kinds.ping = {
    match.headers."x-event" = "ping";
  };
};

agent.triggers.hello = {
  provider = "hooks";
  kind = "ping";
  prompt = ./prompts/hello.md;
};
```

## triggerProvider

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| id | string | required | Must match `[a-z0-9][a-z0-9-]*`. Triggers set `provider` to this id. |
| auth.env | string | required | SecretSpec key name for the token or HMAC secret. The value is not stored in the image. |
| auth.header | null or string | `null` | Shared-token header name. At least one of `header` or `hmac` is required. |
| auth.hmac.header | string | required when hmac is set | Signature header name. |
| auth.hmac.algorithm | enum | `sha256` | Only `sha256`. |
| auth.hmac.prefix | string | empty | Prefix stripped from the header before compare. |
| auth.hmac.message | string | `{{body}}` | Mustache over `headers`, `query`, `payload`, and `body`. |
| routes[].path | string | required | Suffix under `/v1/agents/:agent/`. |
| kinds.<id>.match.headers | attrs | `{}` | Header name to string or list of strings. Names are case-insensitive. |
| kinds.<id>.match.payload | attrs | `{}` | JSON path to string or list of strings. |
| kinds.<id>.dedup.header | null or string | `null` | Dedup key from a request header. |
| kinds.<id>.dedup.payload | null or string | `null` | Dedup key from a JSON path when the header is absent. |
| kinds.<id>.respond.contentType | string | `text/plain` | Immediate response Content-Type. Stops trigger dispatch. |
| kinds.<id>.respond.body | string | required when respond is set | Mustache response body. |

## trigger

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| provider | string | required | Provider id from a `triggerProvider` on the compose list. |
| kind | string | required | Kind id on that provider. |
| prompt | null or path | `null` | Mustache prompt file. Exactly one of `prompt` or `handler`. |
| handler | null or path | `null` | TypeScript file. The runtime runs `bun <file>` for 10 seconds. Stdin is JSON `{ headers, query, payload, body }`. |
| session | null or string | `null` | Mustache session name over payload, headers, query, and body. Empty or null uses the image name. |

Mustache does not HTML-escape. Missing paths render empty. Triggers that share a provider and kind must agree on a non-empty `session` template.

## HTTP

`POST /v1/agents/:agent/:route`

Not behind the API bearer token. The provider secret is the credential. `:agent` is the pulled image name or `name:tag`. `:route` is a provider `routes[].path`, which may include `:trigger`.

```
POST /v1/agents/agency-tester/webhooks/hooks/hello
```

The body is the vendor payload. JSON is parsed when Content-Type is empty or includes `json`. HMAC already ran on the raw body.

### Query

Session and workdir ride on the query string so HMAC of the body stays valid.

| Name | Meaning |
| --- | --- |
| session | Session name to reuse or create. A pinned stopped or failed session stays 409. An unpinned dead name may take a live NATO sibling of the image name. |
| workdir | Persistent workdir name under `$RIG_DATA_DIR/workdirs/`. |
| workdirHost | Host bind path. The path must exist. |
| workdirClone | Git URL. Same scheme rules as `--workdir-clone`. |
| workdirRef | Clone ref. Used with `workdirClone`. |

```
POST /v1/agents/agency-tester/webhooks/hooks/hello?session=notes&workdirHost=/home/me/proj
```

### Status

| Code | Meaning |
| --- | --- |
| 200 | A matched kind has `respond`. Body is the rendered template. Content-Type is `respond.contentType`, or `application/json` if that field is unset in the HTTP writer. |
| 202 | Accepted. JSON `{ ok: true }`, plus `session` when a session was resolved. Also returned when the provider declares no kinds. |
| 204 | No kind matched, or the request is a duplicate dedup key. |
| 401 | Token or HMAC failed. JSON `{ error: "unauthorized" }`. |
| 404 | Agent home missing, no trigger routes, or no matching route. Errors are `agent not found`, `no trigger routes for agent`, or `no matching route`. |
| 409 | Prompt target session is stopped or failed. JSON `{ error: "session is not running" }`. |
| 503 | The provider `auth.env` key is missing. JSON `{ error: "missing secret: <key>" }`. |

A handler that exits non-zero is logged and does not change a 202.

### Dispatch order

1. Resolve the agent home.
2. Match a provider route.
3. Read the provider secret. Missing is 503.
4. Verify token or HMAC. Fail is 401.
5. Parse JSON when the content type allows it.
6. Match kinds. None is 204. No kinds declared is 202.
7. Render a `respond` kind and return 200.
8. Drop duplicates. Duplicate is 204.
9. Resolve the prompt session when any matching trigger has `prompt` or `session`.
10. Run matching handlers as `bun` children, 10 second cap, stdin JSON `{ headers, query, payload, body }`.
11. Queue rendered prompts on that session, one at a time, in order. Return 202 once queued, or 409.

The image must already be on that runtime. Rig does not pull on a webhook.

## Invariants

- Exactly one of `prompt` or `handler`.
- At least one of `auth.header` or `hmac`.
- `triggerProvider.id` matches `[a-z0-9][a-z0-9-]*`.
- Two providers on one agent may not share a route path.

## Failure modes

- Both `prompt` and `handler` set, or neither, throws at eval.
- Missing provider or kind on the collected providers fails eval.
- Missing `auth.env` at request time is 503.
- HMAC of the body stays valid only if session and workdir stay on the query string.

## How to check

Nix fields: https://rig.slopkit.dev/docs/#triggers
HTTP: https://rig.slopkit.dev/docs/triggers/
