> Provider events such as GitHub labels that dispatch durable agent runs

# Event Triggers

Event triggers let an external system start agent work: when a provider event arrives and a rule matches it, Dewee records a durable dispatch, runs the rule’s agent with its task, and can report the result back to the provider. GitHub issue and pull request label events are the first supported source.

Event triggers are not the [Webhook API](/api/webhooks/). `/v1/webhooks` is for your own systems calling an agent directly. Event triggers receive provider events, check them against rules and policy, and turn each match into a dispatch that you can track, cancel and retry.

## Concepts

\| Concept | Meaning |
\|---|---|
\| **Source** | A tenant’s connection to a provider, for example one GitHub webhook. Holds an encrypted signing secret, an optional encrypted outbound token, and the provider config (such as the allowed repositories). |
\| **Rule** | Belongs to a source. Event type, optional subject type and match expression, the agent that runs, the task template, session mode, policy and on-complete action. |
\| **Event** | One authenticated provider delivery, normalized and stored in the inbox. The raw provider payload is not stored, only its SHA-256. |
\| **Dispatch** | The decision for one event and rule: `queued`, `running`, then `succeeded`, `failed`, `interrupted`, `cancelled` or `dead`; or `skipped` with a reason. |

The gateway answers the provider only after the event and its dispatches are stored. Agents never run inside the webhook request. A redelivery of the same delivery is recognized and creates nothing new. Only one run of an agent on one resource (for example one issue) happens at a time; different resources run in parallel.

A run that is cut off by a crash or a restart becomes `interrupted` and is not re-run automatically, because its tools may already have had side effects. Retry it explicitly.

## GitHub setup walkthrough

Event triggers need `GOCLAW_ENCRYPTION_KEY` on the gateway; without it the routes are not mounted. Every management step needs a tenant admin.

### 1. Create a source

Create a GitHub source with the repositories it may accept events from:

```bash
dewee triggers sources create --provider github --name acme --repo acme/api
```

The response shows the **webhook secret once** and the webhook path `/v1/integrations/github/webhook/<source-id>`. Copy the secret now. If you lose it, rotate it (step 6).

To let Dewee comment the outcome on the issue or pull request, add a fine-grained GitHub token with **Issues: Read and write** and **Pull requests: Read and write** on the allowed repositories. Pass it on stdin so it stays out of shell history:

```bash
printf '%s' "$GITHUB_TOKEN" | dewee triggers sources create --provider github --name acme --repo acme/api --credential-stdin
```

The token is encrypted at rest and never returned. It is also what Dewee uses to check the sender’s repository permission (see [Security](#security)).

### 2. Configure the webhook in GitHub

In the GitHub repository, open **Settings → Webhooks → Add webhook** and set:

* **Payload URL:** your gateway’s public origin followed by the webhook path from step 1.
* **Content type:** `application/json`.
* **Secret:** the webhook secret from step 1.
* **Events:** choose “Let me select individual events” and tick **Issues** and **Pull requests**.

GitHub sends a `ping` first; the gateway answers `200`.

### 3. Create a rule

```bash
dewee triggers rules create --source <source-id> --name "AI handle" --subject-type issue \
  --match "data.label == 'ai-handle'" --agent coding-agent \
  --task "Implement the issue" --on-complete source.comment
```

`--agent` takes an agent id or `agent_key`. `--match` is a CEL expression; leave it out to match every event of the type. Policy flags such as `--max-duration`, `--max-per-hour` and `--allowed-actor` are listed in the [CLI reference](/products/cli/#dewee-triggers).

### 4. Test the rule without dispatching

Write a canonical event to `event.json`:

```json
{
  "event_type": "resource.label.added",
  "subject": { "type": "issue", "id": "I_test", "scope": "acme/api", "ref": "#42" },
  "actor": { "type": "external_user", "id": "1", "name": "octocat" },
  "data": { "label": "ai-handle" }
}
```

Then dry-run it:

```bash
dewee triggers rules test <rule-id> --event-file event.json
```

The result shows whether the rule matched and the decision (`queued`, `skipped` with a reason, or `no_match`). A dry run stores and dispatches nothing, and it does not check the hourly rate limit, coalescing or the sender’s GitHub permission.

### 5. Trigger it for real

Add the `ai-handle` label to an issue in `acme/api`, then follow the dispatch:

```bash
dewee triggers events list --source <source-id>
dewee triggers dispatches list --status running
dewee triggers dispatches get <dispatch-id>
```

The dispatch shows its run id, trace id and outcome. Open the trace with `dewee traces get <trace-id>` or in Traces in RGD or CCP.

### 6. Rotate or revoke

`dewee triggers sources rotate-secret <source-id>` issues a new secret; update it in GitHub straight away. `dewee triggers sources disable <source-id>` or `delete` revokes the source.

## Supported GitHub events

\| GitHub event | Result |
\|---|---|
\| `issues` or `pull_request` with action `labeled` | Event type `resource.label.added`, subject type `issue` or `pull_request` |
\| `ping` | `200`, nothing stored |
\| Anything else | `202` with status `ignored`, nothing stored |

Match expressions can use `event`, `source`, `subject`, `actor`, `data` (including `data.label`, `data.title` and `data.body`) and `meta`. Example: `data.label == "ai-handle" && subject.scope == "acme/api"`.

## Policy defaults

Each rule has a policy. Omitted fields use these defaults:

\| Field | Default | Bounds | Meaning |
\|---|---|---|---|
\| `max_duration_seconds` | 1800 | 60–7200 | Wall-clock budget of the agent run. |
\| `max_iterations` | 0 | 0–200 | Tool-loop iteration cap; 0 uses the agent’s own limit. |
\| `max_attempts` | 3 | 1–10 | Attempts to start the run before the dispatch becomes `dead`. |
\| `on_active` | `coalesce` | `coalesce` or `queue` | When several dispatches for one rule and resource wait behind a running one: keep only the newest, or run all in order. |
\| `max_dispatches_per_hour` | 10 | 1–100 | Per rule and resource. Extra dispatches are skipped as `rate_limited`, which stops trigger loops. |
\| `allowed_actors` | empty | up to 50 | If set, only these actor names or ids may trigger the rule. |
\| `allow_bot_actors` | `false` | | Bot senders are refused unless enabled. |

Session modes: `per_resource` (default; follow-up events on the same issue continue the same conversation) or `isolated` (a fresh session per dispatch).

When you update a rule’s policy, send only the keys you want to change; `null` resets a key to its default.

## Security

* **Signed deliveries only.** The `X-Hub-Signature-256` signature is verified with the source secret. A bad signature gets `401` and creates nothing. Unknown or disabled sources get `404`.
* **Repository allowlist.** Events from repositories outside the source’s list, or from senders in the source’s ignore list, are stored as `rejected` and dispatch nothing.
* **Sender permission is checked with GitHub.** Issue forms can add labels on behalf of any issue author, so a label alone proves nothing. Before each run Dewee asks GitHub for the sender’s permission and requires `triage`, `write`, `maintain` or `admin`. It also refuses events sent by the token’s own account. Without a token this check cannot run, so dispatches are skipped unless the rule lists `allowed_actors`.
* **Issue text is untrusted.** Issue and pull request text reaches the agent only inside a fenced, length-capped untrusted-content block. It cannot change the agent’s tools, permissions, tenant or repository scope; the run uses the agent’s own tool policy and approval gates.
* **Rate limit.** The receiver accepts 1,200 deliveries per minute from one sender address across all sources, and 600 per minute per source from one address. Over either limit it answers `429` with `Retry-After: 60`. An IPv6 sender is counted per `/64`.

## API routes

Management routes need a tenant admin and act in the caller’s tenant. Request and response schemas are in the [Interactive API Console](https://dewee.sh/api-docs) under the **Event Triggers** tag.

\| Method and path | Purpose |
\|---|---|
\| `GET /v1/event-triggers/providers` | Providers with their event kinds and outcome actions |
\| `GET`, `POST /v1/event-triggers/sources` | List or create sources (create returns the secret once) |
\| `GET`, `PATCH`, `DELETE /v1/event-triggers/sources/{id}` | Show, update (name, status, config, token) or delete a source |
\| `POST /v1/event-triggers/sources/{id}/rotate-secret` | Issue a new webhook secret |
\| `GET`, `POST /v1/event-triggers/rules` | List (filter by `source_id`) or create rules |
\| `GET`, `PATCH`, `DELETE /v1/event-triggers/rules/{id}` | Show, update (including `enabled`) or delete a rule |
\| `POST /v1/event-triggers/rules/{id}/test` | Dry-run an event against a rule |
\| `GET /v1/event-triggers/events` | Event inbox |
\| `GET /v1/event-triggers/events/{id}` | One event with its dispatches |
\| `GET /v1/event-triggers/dispatches` | Dispatches |
\| `GET /v1/event-triggers/dispatches/{id}` | One dispatch with run id, trace id and outcome |
\| `POST /v1/event-triggers/dispatches/{id}/cancel` | Cancel a queued dispatch or stop a running one |
\| `POST /v1/event-triggers/dispatches/{id}/retry` | Re-queue a `failed`, `interrupted`, `cancelled` or `dead` dispatch |
\| `POST /v1/integrations/{provider}/webhook/{source_id}` | Public receiver, authenticated by the provider signature |

List routes page with `limit` (default 50, maximum 200) and `offset`. There is no total count, so keep paging until a page comes back short.

## CLI

`dewee triggers` covers every management route. All commands accept `--json` or `-o json`, and destructive commands ask for confirmation or take `--force`.

```bash
dewee triggers providers
dewee triggers sources list
dewee triggers sources enable <id>
dewee triggers rules list --source <source-id>
dewee triggers rules disable <rule-id>
dewee triggers events list --status rejected
dewee triggers events get <event-id>
dewee triggers dispatches list --status failed
dewee triggers dispatches retry <dispatch-id>
dewee triggers dispatches cancel <dispatch-id>
```

Every flag is in the [CLI reference](/products/cli/#dewee-triggers).

## In the web apps

* **RGD (Runtime Gateway Dashboard):** sidebar **System → Event Triggers**, with **Sources**, **Rules** and **Activity** tabs. Visible to admins.
* **CCP (Customer Control Plane):** **Connect → Event triggers** in your workspace, with the same three tabs.

Both show the webhook secret once after you create a source or rotate its secret, let you test a rule, and show each event’s dispatches with their reasons.

## Troubleshooting

### Delivery responses

\| What you see | Meaning |
\|---|---|
\| `401` in GitHub’s delivery log | The signature did not verify. The secret in GitHub does not match; paste the current one or rotate it. |
\| `404` | The source id is unknown, or the source is disabled. |
\| `429` | More than 600 deliveries per minute for this source, or 1,200 for all sources, from one address. |
\| `500` | The gateway could not store the delivery. GitHub does not resend it on its own; see below. |
\| `202` with status `ignored` | Not an `issues` or `pull_request` event, or an action other than `labeled`. Nothing is stored. |
\| `400` | The payload is malformed, for example not JSON or missing the repository, sender or label. |
\| `200` with status `duplicate` | GitHub redelivered a delivery that was already stored. |
\| Event accepted but no dispatch | No enabled rule matched the event type, subject type and match expression. Non-matching rules are not recorded; use `rules test` to check. |

GitHub does not redeliver a failed delivery automatically. After a gateway outage, open the repository’s webhook settings, go to **Recent Deliveries**, and redeliver the failed ones. Redelivery is safe: a delivery that was already stored answers `duplicate` and creates nothing new.

### Reason codes

Find the reason with `dewee triggers events get <event-id>` or `dewee triggers dispatches get <dispatch-id>`, or on the Activity tab.

\| Reason | Where | What to do |
\|---|---|---|
\| `repository_not_allowed` | Event `rejected` | Add the repository to the source’s allowed list. |
\| `actor_ignored` | Event `rejected` | The sender is on the source’s ignore list. |
\| `actor_unverified` | Dispatch `skipped` | The source has no GitHub token, so the sender’s permission cannot be checked. Add a token or set `allowed_actors` on the rule. |
\| `actor_not_authorized` | Dispatch `skipped` | The sender lacks `triage` or higher on the repository. |
\| `self_actor` | Dispatch `skipped` | The event was sent by the token’s own account. |
\| `actor_not_allowed` | Dispatch `skipped` | The sender is not in the rule’s `allowed_actors`. |
\| `bot_actor` | Dispatch `skipped` | A bot sent the event. Set `allow_bot_actors` if that is intended. |
\| `actor_check_failed` | Dispatch re-queued | GitHub could not be reached for the permission check; retried within `max_attempts`. |
\| `rate_limited` | Dispatch `skipped` | The rule hit `max_dispatches_per_hour` for this resource. |
\| `superseded` | Dispatch `skipped` | A newer dispatch for the same rule and resource replaced it (`on_active: coalesce`). |
\| `match_error` | Dispatch `skipped` | The match expression failed at runtime. Fix the expression and test the rule. |
\| `hop_limit` | Dispatch `skipped` | The event was caused by too long a chain of earlier triggers. |
\| `target_unavailable` | Dispatch `skipped` or `failed` | The rule’s agent no longer exists or cannot be resolved. |
\| `policy_invalid` | Dispatch `skipped` or `failed` | The stored policy could not be read. Update the rule’s policy. |
\| `agent_unavailable` | Dispatch `dead` | The agent could not be started within `max_attempts`. Retry once it is available. |
\| `timeout` | Dispatch `failed` | The run exceeded `max_duration_seconds`. |
\| `run_error` | Dispatch `failed` | The agent run ended with an error. Open its trace. |
\| `cancelled_by_operator` | Dispatch `cancelled` | Someone cancelled it. |
\| `no_credential` | Outcome failed | The rule posts a comment but the source has no token. |
\| `outcome_error` | Outcome failed | The comment could not be posted; the detail follows the code. |

Each dispatched run is a normal agent trace named `Event trigger [<rule>] - <agent>` and tagged `event-trigger`, so the full run is in Traces in RGD and CCP and in `dewee traces get <trace-id>`.
