# Webhook Triggers

> A webhook trigger starts an agent (or a team) in your project when an outside service calls a URL, instead of at a set time.

- Area: Desktop, mobile and web
- Plans: All plans
- Last checked against the product: 2026-09-30
- Web page: https://agentsroom.dev/docs/webhook-triggers

## What it does

A webhook trigger starts an agent (or a team) in your project when an outside service calls a URL, instead of at a set time. AgentsRoom gives each trigger a unique public URL and a regenerable signing secret; you paste them into GitHub, GitLab, Slack, Linear, Sentry, a CI job or anything that can POST JSON. Every call is verified against the secret before anything runs, an optional filter decides whether the event deserves a run, and an anti-burst window folds calls arriving close together into one run. The event's fields become prompt variables resolved at fire time. The agent runs on your own machine, as a real agent with its terminal and archived transcript. Nothing is consumed while nothing happens. A generic trigger also refuses replayed calls (signed timestamp and delivery id, on by default), and a **Restricted run** makes the agent CLI itself refuse the shell, the web and every AgentsRoom tool when the payload comes from someone you do not control.

## Where to find it

- Inside a project, agents panel: the webhook shortcut ("Add an agent triggered by a webhook") or **Scheduled agent** open the same **Triggers** panel; the **Webhooks** filter, carrying the webhook icon, narrows the list and says **No webhook agent yet** when there is none.
- In the editor, **Trigger** offers five cards: **Scheduled**, **Quota**, **Webhook**, **On ship**, **On demand**. The list has an **On ship** filter too.
- The list shows each webhook's **Last call**, **Replay the last call**, and "No call received yet" until the first delivery.

## How to use it

1. **New trigger**, pick the **Webhook** card. AgentsRoom registers the endpoint and shows **URL to paste into the service** (**Copy**) and the **Signing secret** (**Reveal the secret**, **Regenerate**).
2. Under **Who calls this URL**, pick **Any service (JSON)**, **GitHub**, **GitLab**, **Slack**, **Linear** or **Sentry**. A shortcut verifies that service's own signature header and pre-maps its payload fields to variables; it is not an allow list.
3. Paste the URL and secret into the service. Or use **Example call** > **Copy as curl** (macOS / Linux or Windows PowerShell variant) to send a real signed call from a terminal right away.
4. Optionally set **Only run when** (a condition on the JSON body, e.g. `action == "opened"`, `pull_request.title contains "fix"`, joined by `&&` / `||`) and **Anti-burst** (**One run per call**, or **One run per N min at most**).
5. Write the **Prompt** with **Variables available in the prompt**: click one to insert it (`{{event.title}}`, `{{event.author}}`, `{{event.url}}`, `{{event.number}}`, `{{event.branch}}`, `{{payload}}` for the raw JSON). Variables filled by the last call are highlighted.
6. Choose who runs it (one agent or a team), the **Advanced** block and **Machines** exactly as for a scheduled task, then **Save** and check **Enabled**.
7. Or, under **Who runs it**, pick the third card **Board action** > **Start next Todo ticket**: on each accepted call the trigger starts no agent of its own and needs no prompt. It does what the board's **Start next** button does: the top Todo ticket that is not waiting on a prerequisite moves to In progress and starts with its own assigned agent or team (same path as `backlog_start_next`, including its worktree). When Todo has nothing ready, the history says **Nothing to start** instead of a failure.
7. Look at **Last call received**, adjust the filter and prompt against that real payload, **Replay the last call** until the run is right.

### On ship: start a monitoring agent when a ticket ships

The **On ship** card starts the trigger's agent (or team) when a backlog ticket of the project ENTERS a column, instead of on an outside call. Use it for a post-release pass: watch logs, errors and user feedback after a change ships.

1. **New trigger**, pick **On ship**. No URL and no secret: the AgentsRoom server produces the event itself, and nothing outside AgentsRoom can send one.
2. **Starts when a ticket enters**: **Done** (default) or **In review** (a QC step before the ticket is closed).
3. Optionally **Only run when**, a condition on the ticket JSON: `ticket.type == "bug"`, `ticket.tags contains "prod"`, `fromStatus == "in_review"`.
4. Write the prompt with the ticket variables: `{{event.id}}`, `{{event.title}}`, `{{event.body}}`, `{{event.type}}`, `{{event.tags}}`, `{{event.status}}`, `{{event.from}}`, `{{event.shipped}}` (ISO time), `{{event.branch}}` (worktree branch), `{{event.version}}` (fixed-in version), `{{event.agent}}` / `{{event.team}}` (who did the work), `{{event.author}}` (reporter), `{{event.url}}` (external issue), or `{{payload}}`. Put the monitoring window and the success criteria in the prompt; **Close after inactivity** ends the run.
5. Every ticket that enters the column gets its own run, once: a double move or a retried request does not start a second one, and a ticket declined as won't fix never starts it. Only YOUR triggers of that project fire, whoever moved the ticket (kanban drag, MCP, phone).
6. History shows one row per shipped ticket. **Run now** replays the last shipped ticket (the retry); the switch disables it.

### Security: what protects a webhook trigger

A webhook URL is public and the payload is written by whoever triggers the event. Three controls, all enforced by the platform rather than by the prompt:

#### Signature and replay protection

- Every call is verified against the signing secret before anything else: `X-Hub-Signature-256` for GitHub, `X-Slack-Signature` for Slack, the `X-Gitlab-Token` shared token for GitLab, an HMAC SHA-256 of the raw body for Linear and Sentry. An unsigned call answers 401 and starts nothing.
- **Any service (JSON)** has **Replay protection** > **Require a signed timestamp**, ON for every new trigger, with an **Acceptance window** of 1, 5 (default), 15 or 60 minutes. The sender computes `HMAC-SHA256(secret, "v1:<timestamp>:<delivery id>:<raw body>")` and sends three headers: `X-AgentsRoom-Signature: v1=<hex>`, `X-AgentsRoom-Timestamp: <Unix time in seconds>`, `X-AgentsRoom-Delivery: <unique id>`. Changing the timestamp or the id breaks the signature. A call outside the window (in the past or the future) answers 401 "Delivery timestamp outside the acceptance window", and a trigger with the protection on refuses the body-only signature outright (no fallback an attacker could pick). **Example call** > **Copy as curl** already signs this way, in both shells.
- A trigger created before this protection keeps the body-only signature (`X-AgentsRoom-Signature: <hex of the body>`) until you turn it on: then update the sender, or its calls start failing. The editor warns while it is off.
- Slack signs its own timestamp: a Slack call more than 5 minutes old is refused.
- GitHub, GitLab, Linear and Sentry sign the body only. A delivery id they already sent never runs twice, but their signature cannot tell when a call was made.
- Native dedupe: every delivery id an endpoint accepts is recorded for 30 days, including the calls folded by **Anti-burst**. The same id again answers 200 `duplicate` and starts nothing, whether it is a provider retry or a replay. With replay protection on, only the SIGNED `X-AgentsRoom-Delivery` counts: an extra unsigned id header added to a captured call cannot get it through.

#### Restricted run (untrusted payloads)

For a trigger fed by input you do not control (pull request code, a public issue, a CI job running contributor code), turn on **Restricted run** under **Who runs it**. Without it the agent has your shell, your files and every tool, and only the prompt tells it what not to do, which a prompt injection in the payload can override. With it, the agent CLI itself enforces:

- no shell and no web access (unless **Allowed commands** lists some);
- project files read-only (**Allow editing project files** opens edits inside the project on Claude Code; Codex stays read-only whatever it says);
- no AgentsRoom tool at all (backlog, memory, SSH, databases, agent spawn, the embedded browser, your Chrome);
- only the trigger's own MCP servers (**Advanced** > **MCP servers**), and among them only those named in **Allowed MCP tools**, one per line: `server` for every tool of it, `server/tool` for one;
- nothing ever waits for an approval: what is not allowed is refused and the run goes on.

It replaces the trigger's permission mode and CLI options. Per CLI:

- **Claude Code**: `--restricted` (file tools confined to the project, the repository's own `.claude/settings*.json` and `.mcp.json` ignored, bypass refused), `--strict-mcp-config` with only the allowed servers, `--permission-mode dontAsk`, an explicit tool list (Read, Grep, Glob, TodoWrite, plus Edit/Write/NotebookEdit when edits are allowed, plus Bash only for **Allowed commands**, which become `Bash(<prefix>:*)` rules). Reading or editing `.env*`, `.mcp.json`, `.npmrc`, `.git-credentials`, `.agentsroom/` and the config folder of every agent CLI (`.claude/`, `.codex/`, `.cursor/`, `.gemini/`, `.agents/`, ...) is denied, editing CI workflows too, WebFetch and WebSearch as well: a run allowed to edit must not plant a hook or an MCP server that the next, unrestricted agent would run. Skills and sub-agents are not offered (a file in the repository could otherwise widen the allowlist). AgentsRoom's status hooks still run.
- **Codex**: the OS sandbox, always `--sandbox read-only` (its sandbox cannot shield the agent config files, so **Allow editing project files** does not apply), network off, `-a never`, and a per-agent profile that switches every other MCP server off and filters the allowed ones with `enabled_tools`. Needs Codex 0.134 or later. Limit: a sandboxed command can still READ files outside the project, so prefer Claude Code for a run that holds a credential. Not available on Windows (the sandbox does not verifiably cut the network there).
- **Any other CLI**, a **team** executor, or an SSH remote host: the trigger refuses to start, records a failed run with the reason, and consumes the webhook event. A restricted run never degrades into an unrestricted one.

The restriction stays on the run's agent for its whole life: a resume, a relaunch or a fork is restricted too; **Ask on the side** is not offered on it; a duplicate drops it (fresh conversation); relaunching it from the phone is refused ("relaunch it from the desktop app").

**Allowed commands** give the run a shell again: keep the list empty for an untrusted payload, and never allow a command that executes repository code (`npm`, `make`, `pytest`...). Claude Code's read-only commands also run once a shell exists; common exfiltration commands (`curl`, `wget`, `env`, `cat`, ...) are denied unless you listed them.

#### A credential for one trigger

- **Advanced** > **Environment variables** of the trigger (`KEY={{secret:NAME}}`) reach that trigger's runs only, not the rest of the project.
- **Advanced** > **MCP servers** of the trigger: a server's `env` or `headers` can hold `{{secret:NAME}}`. For a restricted run this is the recommended place for a token (a Forgejo or GitHub token for a review bot): the MCP server holds it and the agent calls the tool without ever reading the token. List only the tools the run needs in **Allowed MCP tools**, and give the token the smallest scope your forge allows.
- `{{secret:NAME}}` is resolved from the secret vault of the machine that fires the trigger, at launch: pin **Machines** to a computer that holds the secret.

## Settings

- `wakeForTriggers` (global, Settings > Terminal; project and per-trigger overrides): wake the machine for the trigger's runs.
- `autoLaunchAgentsAtStartup` (global, Settings > Terminal): lets queued deliveries be replayed at launch.
- Per trigger, `webhook.replayWindowSec` (Triggers editor > **Replay protection** > **Require a signed timestamp** / **Acceptance window**, generic source): seconds, 60 to 3600. Default 300 on a new trigger; absent on older ones (body-only signature).
- Per trigger, `webhook.source: "backlog"` + `webhook.shipStatus` (Triggers editor > **On ship** > **Starts when a ticket enters**): `done` (default) or `in_review`.
- Per trigger, `restriction` (Triggers editor > **Who runs it** > **Restricted run**): `allowedTools` (**Allowed MCP tools**), `allowedCommands` (**Allowed commands**, Claude Code only), `allowEdits` (**Allow editing project files**). Absent = not restricted, the default.

## Agent tools (MCP)

- `triggers_list`, `triggers_get`, `triggers_save`, `triggers_delete`: manage triggers, including webhook mode.
- `triggers_run`: fire the trigger now (a webhook or **On ship** trigger replays its last event).
- `triggers_save({ kind: "ship", shipStatus: "done" | "in_review" })`: an **On ship** trigger; `triggers_list({ kind: "ship" })` lists them.
- `triggers_save({ boardAction: "start-next-todo" })`: make the trigger a board action (no prompt needed); an empty string goes back to an agent or team. `triggers_get` / `triggers_list` report `boardAction`.
- `triggers_save({ replayWindowSec })` and `triggers_save({ restricted: true, allowedTools, allowedCommands, allowEdits })`: an agent may turn these controls ON or TIGHTEN them, never loosen them. Turning replay protection off, lengthening the window of an existing trigger, turning a restricted run off or widening its lists is refused over MCP: that is the user's call, in the desktop editor. A new webhook trigger starts protected (300 s) and may be created with any window from 60 to 3600. `triggers_get` returns `restriction` and `webhook.replayWindowSec`; `triggers_list` returns `restricted`.

## Providers

All providers: the spawned agent goes through the normal launch path with the role, provider and model set on the trigger.

**Restricted run**: Claude Code and Codex (not on Windows) only. Every other CLI refuses to start a restricted run instead of running it without its limits.

## Mobile

Present. The phone edits the whole trigger, including the **On ship** mode and the webhook block (URL, secret, curl example, filter, anti-burst, replay protection, variables, last payload), **Restricted run** with its allowed tools and commands, machine scope and history. Execution stays on the desktop, which is also where a restricted run's agent can be relaunched.

## Limits

- Inbound only: AgentsRoom receives events and never emits webhooks. An agent that must call a service does it with its own tools.
- The agent runs on your machine: an event arriving while AgentsRoom is closed is queued and replayed at the next launch, held for a week, then dropped.
- One event produces one run: with the project open on several machines, the first to pick the event locks it, the others skip it. Pin **Machines** to make one computer own a trigger.
- POST only. An unsigned call is rejected; knowing the URL is not enough.
- **Regenerate** invalidates the old secret at once: paste the new one into the service or the webhook breaks.
- Not a multi-step scenario builder: a trigger decides when an agent starts and hands it the event; the work is the agent's. The only built-in action it runs itself is **Start next Todo ticket**.

## Common questions

- **Is the public URL safe?** Yes: GitHub is checked with X-Hub-Signature-256, Slack with X-Slack-Signature, GitLab with its shared token, Linear and Sentry with an HMAC of the raw body, and a generic trigger with a signature that also covers a timestamp and a delivery id. A call without a valid signature does nothing.
- **Can someone replay a call they captured (a CI log, a proxy)?** Not on a generic trigger with **Replay protection** on (the default for new ones): outside the acceptance window the call is refused, inside it the same delivery id never runs twice, and the id cannot be swapped without breaking the signature. Slack calls older than 5 minutes are refused too.
- **My CI started getting 401 after I turned on replay protection.** The sender still signs the body only. Sign `v1:<timestamp>:<delivery id>:<raw body>` and send `X-AgentsRoom-Signature: v1=<hex>`, `X-AgentsRoom-Timestamp` (seconds, not milliseconds) and `X-AgentsRoom-Delivery`: **Copy as curl** shows the exact commands.
- **My webhook agent reviews pull requests from strangers. How do I stop a prompt injection from using my shell or my servers?** Turn on **Restricted run**: no shell, no web, read-only project files, no AgentsRoom tool, and only the MCP tools you list, enforced by Claude Code or Codex themselves. Put the forge token in the trigger's own MCP server so the agent never sees it.
- **Can I give one trigger its own token without exposing it to the whole project?** Yes: the trigger's **Environment variables** and **MCP servers** (under **Advanced**) take `{{secret:NAME}}` references and apply to that trigger's runs only.
- **Why not have an agent poll the API?** Every poll burns a full turn of context to answer "nothing new". A webhook costs nothing until the event happens, then reacts in seconds.
- **Which services are supported?** Anything that sends a signed POST with a JSON body: the five shortcuts, plus CI, monitoring, your own backend or a curl.
- **How do I test without pushing commits?** **Copy as curl** for a signed test call, then **Replay the last call**.
- **Can an agent start by itself when a ticket ships, to watch production?** Yes: the **On ship** mode. Pick Done or In review, write the monitoring brief with the ticket variables, and every shipped ticket gets one traceable monitoring run.
- **Can a webhook start the next backlog ticket without an agent turn?** Yes: **Board action** > **Start next Todo ticket**. Zero tokens for the trigger itself, no extra tab to close; the ticket then runs with the agent or team assigned to it. Same signature check, filter and anti-burst as the other modes.

## Related

- [Scheduled Tasks](https://agentsroom.dev/docs/scheduled-tasks.md): the same panel, fired by the clock or the quota.
- [Agent Teams](https://agentsroom.dev/docs/teams.md): point a webhook at a pipeline.
- [Backlog Task Board](https://agentsroom.dev/docs/backlog-task-board.md): scope a new issue as it is filed.
- [Keep the Machine Awake](https://agentsroom.dev/docs/keep-awake.md): keeping the machine up while the run works.
