---
title: "Triggers and webhooks"
description: "Start a workflow manually, on a schedule, from a workspace event, or from an authenticated webhook, and keep webhook credentials under control."
---

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

# Triggers and webhooks

A trigger decides when a workflow starts. Every trigger enters the same run model, so run history, approvals, and credits behave the same way whichever trigger you choose.

## Trigger kinds

| Trigger   | Starts when                                                                                                                                                                                                                            | Typical use                                                 |
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| Manual    | You choose **Run** in the builder                                                                                                                                                                                                      | Testing a new process                                       |
| Schedule  | A cron expression matches in the workflow's timezone                                                                                                                                                                                   | Weekly reviews, daily digests                               |
| Event     | A workspace event fires: a ticket is decided, brand enrichment completes, an inbox or chat channel receives a message, a monitored competitor page changes, or a table row is created, updated, or deleted                             | Reacting to work inside AI Agent                            |
| App event | A connected app fires (Stripe, Slack, Linear, Google Sheets, …). Pick the connection, the event, fill the event's settings; the workflow receives the provider object under `input.payload`.                                           | Reacting to work in another product                         |
| Poll      | A connected app is checked every N minutes (5 minimum); one run starts per new item (or one run per batch). Turning the trigger on remembers what already exists — only items that arrive afterwards start runs. Daily limit 500 runs. | Google Sheets: new rows, and similar "watch a list" sources |
| Webhook   | An external system sends an HTTP `POST` to the workflow's URL                                                                                                                                                                          | Form submissions, CRM automations, custom code              |

Set the trigger from the **Start** block in the builder, or from the Autopilot form. A schedule, event, or webhook trigger only fires while the workflow is **active**.

**Only run when.** An event trigger can carry a filter such as `input.source == 'human'`. The workflow runs only when the filter is true for the event. Table-row triggers can also be limited to one table. The event payload carries ids and metadata, not the row's values; to test a row's field, add a Query table step and a Condition after the trigger.

**Google Sheets: new rows.** Pick a Poll trigger, connect the sheet, and set the interval (once a week is fine for a digest). The sheet needs a column that uniquely identifies each row, such as an id or a timestamp — the trigger uses it to tell new rows from ones it already saw. Each new row starts its own run with that row under `input.item`, or turn on batch mode to get every new row at once under `input.items`.

## Webhook requests

Each webhook-triggered workflow has its own URL. Copy it from the Start block.

- Method: `POST`.
- Body: any text up to 1 MB. When the `Content-Type` is JSON, the body is parsed and exposed to the workflow as its input; otherwise the raw text is exposed.
- Delivery id: send an `Idempotency-Key`, `Webhook-Id`, or `X-Request-Id` header to make retries safe. Without one, the SHA-256 of the body is used, so an identical body is treated as a duplicate.
- Limit: 60 requests per minute per webhook and sender address. Above that you receive `429` with a `Retry-After` header.

### Responses

| Status | Body                                                       | Meaning                                                                                           |
| ------ | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `200`  | `{ "ok": true, "status": "queued", "deliveryId": "…" }`    | Accepted; a run starts shortly.                                                                   |
| `200`  | `{ "ok": true, "status": "duplicate", "deliveryId": "…" }` | Same delivery id already accepted; no new run.                                                    |
| `202`  | `{ ok, status: "suspended", deliveryId }`                  | Sync mode: the run is waiting for a person.                                                       |
| `401`  | `Unauthorized`                                             | No valid token or signature.                                                                      |
| `404`  | `Workflow webhook not found`                               | Unknown, archived, or non-webhook workflow.                                                       |
| `408`  | `{ ok, status: "timeout", deliveryId }`                    | Sync mode: the run did not answer in time (or did not start); if it is running, it keeps running. |
| `413`  | `Payload too large`                                        | Body over 1 MB.                                                                                   |
| `429`  | `Too Many Requests`                                        | Rate limit reached.                                                                               |
| `500`  | `{ ok: false, status: "failed", deliveryId }`              | Sync mode: the run failed.                                                                        |

The response never includes a run id, except in sync mode below. Open the workflow's run history to follow the run.

The delivery-id dedupe above only catches an identical retry. If the same real-world event can arrive under a different delivery id — no stable `Idempotency-Key` from the sender, or a re-send with a new one — add a **Deduplicate** step after the trigger and key it on a field from the body, such as an order id. It remembers that key for the window you set and skips or flags the repeat.

### Wait for the result

Add `?mode=sync` to the webhook URL to wait for the workflow's answer in the same request. The workflow replies through a **Respond to webhook** block (JSON, raw text, or a redirect, with a status code and headers of your choice). Without that block, a finished run returns `200` with `status: "completed"` and the workflow's final output when it defines one. The wait is limited to 25 seconds by default; a slower run returns `408` with `status: "timeout"` and the same `deliveryId`, and keeps running — find it in run history. A run waiting for approval returns `202` with `status: "suspended"`. A failed run returns `500` with `status: "failed"`. Every reply carries the `X-Webhook-Delivery-Id` header.

Choose **Respond and stop** to end the run after replying, or **Respond and continue** to reply and keep working. Only requests sent with `?mode=sync` receive the reply; others get the usual `queued` receipt.

## Authenticate a webhook

A request is accepted when it carries **either** a valid token **or** a valid signature. Requests with neither are rejected before the body is read.

### Token (default)

Every webhook trigger has a token that starts with `whsec_`. Send it in one of two headers:

- `Authorization: Bearer <token>`
- `X-Webhook-Token: <token>`

Use the token with tools that let you add a fixed header, such as Zapier or Make.
### Body signature

Turn on **Require a body signature** in the Start block to get a signing secret that starts with `whsig_`. The sender computes an HMAC-SHA256 of the exact request body with that secret and sends it in the header you choose. The default header is `X-Signature-256` and the default encoding is hex. Choose base64 for services that send base64 signatures.

The header value may start with `sha256=`. Both forms are accepted:

- `X-Signature-256: sha256=f7bc83f4…`
- `X-Signature-256: f7bc83f4…`

> **Sign the bytes you send**
>
> Compute the signature over the raw body bytes, not over a re-serialized
> object. Any change to whitespace or key order after signing makes the
> signature invalid.

Node.js example:

```js
import { createHmac } from "node:crypto"

const body = JSON.stringify({ email: "lead@example.com" })
const signature = createHmac("sha256", process.env.WEBHOOK_SIGNING_SECRET)
	.update(body)
	.digest("hex")

await fetch(WEBHOOK_URL, {
	method: "POST",
	headers: {
		"content-type": "application/json",
		"x-signature-256": `sha256=${signature}`,
	},
	body,
})
```

Shell example:

```bash
BODY='{"email":"lead@example.com"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SIGNING_SECRET" | awk '{print $NF}')
curl -X POST "$WEBHOOK_URL" \
  -H "content-type: application/json" \
  -H "x-signature-256: sha256=$SIG" \
  --data "$BODY"
```

## Sample data

AI Agent keeps the last five payloads a webhook, app-event or polling trigger received. Open the Start block to see them, pin one, and click **Use fields** to add its fields to the input contract so you can pick them in any step. **Catch a test event** waits two minutes for the next payload; for a polling trigger it fetches the current items right away. A step test and **Run with sample** use the pinned sample as the workflow input. Long values are shortened to keep a sample under 16 KB.

## Resume a waiting run

A step that waits for a URL can suspend a run until an external caller sends `POST` to the run's own resume URL. Present the resume token one of three ways, checked in this order: `Authorization: Bearer <token>`, an `X-Resume-Token` header, or `?token=` on the URL. Prefer a header — a query-string token is more likely to end up logged somewhere.

The route responds `202` on a successful resume, `401` on a missing or invalid token, and `409` when the run is not currently waiting there (already resumed, canceled, or never suspended).

## Rotate a credential

1. **Rotate in the Start block**

   Choose **Rotate token** or **Rotate signing secret**. A new value is
   generated immediately.
2. **Update the sender**

   Paste the new value into the external system. The previous value keeps
   working for 24 hours so the change does not drop deliveries.
3. **Revoke immediately when needed**

   If a credential leaked, tick **Revoke the current token immediately** in the
   rotation dialog. The previous value stops working at once.
4. **Verify**

   Send one test request with the new value and confirm a `queued` response and
   a new run in run history.

Every rotation is recorded in the workspace audit log without the secret values.

## Fix a rejected request

### 401 with a token

Check that the header is `Authorization: Bearer <token>` or `X-Webhook-Token`, that the token starts with `whsec_`, and that it was not rotated more than 24 hours ago.
### 401 with a signature

Confirm signing is enabled on the trigger, the header name matches the trigger setting, the encoding (hex or base64) matches, and the signature was computed over the exact bytes sent.
### 200 duplicate but no run

The delivery id was seen before. Send a new `Idempotency-Key` for each distinct event.
### 404

The workflow is archived, the URL is wrong, or the trigger is no longer a webhook. Copy the URL again from the Start block.

## Continue learning

		Choose between on-demand and continuously operating automation.

		Diagnose a run that queued, failed, or is waiting.

Source: https://docs.aiagent.app/core-concepts/triggers-and-webhooks/index.mdx
