-
Notifications
You must be signed in to change notification settings - Fork 0
docs(workflows): document the webhook trigger (PRD-186) #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ef49d24
c8c1347
508884f
fcdc4ca
2d19154
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| --- | ||
| title: "Workflow triggers" | ||
| description: "Choose how a workflow starts, manually from the interface, or automatically from an external system via a webhook." | ||
| --- | ||
|
|
||
| A workflow can be started in two independent ways. Both are configured in the **Process** section of the workflow settings page, beneath the version card. Each trigger type has its own row with an on/off toggle, and the two can be enabled independently. | ||
|
|
||
| - **Manual** — users start the workflow from a matching record in the interface (List View, Summary/Details, or a Workspace). This is the default. See [Executing workflows](/product/execute/workflows). | ||
| - **Webhook** — external systems start the workflow via an authenticated HTTP POST. **Disabled by default.** | ||
|
|
||
| Webhook triggers are available on **all environments**, with no production-only restriction. | ||
|
|
||
| ## The webhook trigger | ||
|
|
||
| When enabled, the webhook lets any external system, an ETL job, a partner service, a CRON in your own infrastructure, start a workflow run on a specific record by calling a stable URL. | ||
|
|
||
| Two things are separated by design: | ||
|
|
||
| - **The URL** identifies everything *fixed* about the trigger: which workflow runs, and against which rendering. The only per-call input is the target record. | ||
| - **The token** carries authentication *and* the identity the run acts as. The run reads and writes data as the token's user, and the activity log attributes it to that user, so a webhook-triggered run can never do more than that user is allowed to. | ||
|
|
||
| For the full HTTP contract, request body, response codes, idempotency, and rate limits, see the [Trigger a workflow via webhook](/reference/api/endpoints/trigger-workflow-webhook) API reference. | ||
|
|
||
| ## Enabling the webhook | ||
|
|
||
| 1. Open the workflow settings page and go to the **Process** section. | ||
| 2. Toggle **Webhook** on. | ||
|
|
||
| Once enabled: | ||
|
|
||
| - the endpoint **URL** is displayed inline with a **copy** button; | ||
| - a hint shows the JSON body to send, with the target record's `record_id`; | ||
| - a **Generate new URL** button lets you rotate the URL (see [Regenerating the URL](#regenerating-the-url)). | ||
|
|
||
| <Frame> | ||
| <img src="/images/workflows/webhook-trigger-rows.png" alt="Process section with Manual and Webhook trigger rows beneath the version card" /> | ||
| </Frame> | ||
|
|
||
| Copy the URL and use it from your external system with a valid application token. The workflow starts on the record you pass in the request body. | ||
|
|
||
| ## Regenerating the URL | ||
|
|
||
| If a URL may have leaked, or you simply want to rotate it, generate a new one from the **Generate new URL** button below the current URL. | ||
|
|
||
| <Warning> | ||
| Generating a new URL **immediately invalidates the current one**. Any integration still calling the old URL will start failing until you update it with the new URL. | ||
| </Warning> | ||
|
|
||
| When you confirm: | ||
|
|
||
| - the URL is updated inline, and the **copy** button now copies the new one; | ||
| - a confirmation toaster briefly appears. | ||
|
|
||
| <Frame> | ||
| <img src="/images/workflows/webhook-regenerate-url.png" alt="Generate new URL button and the invalidation warning" /> | ||
| </Frame> | ||
|
|
||
| ## Revoking access | ||
|
|
||
| You have three independent levers to stop a webhook, without necessarily touching the others: | ||
|
|
||
| | Lever | Effect | Calls then return | | ||
| |---|---|---| | ||
| | **Disable the Webhook toggle** | Turns the trigger off. URL and token are unchanged. | `404` | | ||
| | **Generate new URL** | Invalidates the current URL. | `400` (old URL) | | ||
| | **Invalidate / expire the token** | Done by the token's user, from account settings. | `401` | | ||
|
|
||
| Turning the toggle back on re-enables the *same* URL and token — it is a pause, not a reset. | ||
|
|
||
| ## Auditing | ||
|
|
||
| Every webhook-triggered run is recorded in the workflow run history and in your **Activity Logs**, attributed to the token's user and marked as **webhook-triggered** so you can distinguish automated runs from manual ones. | ||
|
|
||
| ## Learn more | ||
|
|
||
| <CardGroup cols={2}> | ||
| <Card title="Trigger via webhook (API)" icon="code" href="/reference/api/endpoints/trigger-workflow-webhook"> | ||
| The HTTP contract: body, response codes, idempotency, rate limits. | ||
| </Card> | ||
| <Card title="Workflows overview" icon="diagram-project" href="/product/process/workflows/overview"> | ||
| Build and manage workflows in the no-code editor. | ||
| </Card> | ||
| <Card title="Executing workflows" icon="play" href="/product/execute/workflows"> | ||
| How operators run workflows from the interface. | ||
| </Card> | ||
| <Card title="Roles & permissions" icon="shield" href="/get-started/control/roles-permissions"> | ||
| Control what a workflow run can access. | ||
| </Card> | ||
| </CardGroup> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,160 @@ | ||
| --- | ||
| title: Trigger a workflow via webhook | ||
| description: "Start a workflow run on a specific record from an external system, over authenticated HTTP." | ||
| --- | ||
|
|
||
| ## Trigger a workflow run | ||
|
|
||
| Starts a run of a workflow on a specific record. External systems call the workflow's webhook URL to trigger it the same way an operator would from the interface, but over authenticated HTTP. | ||
|
|
||
| ``` | ||
| POST <webhook_url> | ||
| ``` | ||
|
|
||
| You do **not** build this URL yourself. Enable the webhook trigger on the workflow, then **copy the full URL** from its trigger settings — it already contains the signed identifier of the workflow and rendering. See [Workflow triggers](/product/process/workflows/triggers) for how editors enable the webhook, copy the URL, and manage the token. | ||
|
|
||
| The run uses the **latest active workflow** for the target rendering. There is no published/draft distinction — "latest active" is the contract. | ||
|
|
||
| ### Authentication | ||
|
|
||
| All requests require a Forest **application token** in the `Authorization` header, presented with the Bearer scheme. Generate one from [your account settings](https://app.forestadmin.com/user-settings). | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium The documentation contradicts itself on how authentication works. The "Authentication" section says any Forest application token whose user can reach the rendering will work (lines 26–32), but the "Revoking access" section says expiring "the" application token makes webhook calls return 🚀 Reply "fix it for me" or copy this AI Prompt for your agent: |
||
|
|
||
| ``` | ||
| Authorization: Bearer YOUR_APPLICATION_TOKEN | ||
| ``` | ||
|
|
||
| The token serves two purposes at once: | ||
|
|
||
| - **Authentication** — it identifies the caller and grants (or denies) the call. | ||
| - **Execution identity** — the run reads and writes data **as the token's user**, and the activity log attributes it to that user. Anything the user is not allowed to access, the run cannot access either. | ||
|
|
||
| <Note> | ||
| The URL alone grants no authority. It carries a signed identifier of the workflow and rendering, but starts nothing without a token whose user can reach that rendering. Authority comes entirely from the token. | ||
| </Note> | ||
|
|
||
| <Warning> | ||
| If your project uses SSO, the application token must be generated while logged in with SSO. | ||
| </Warning> | ||
|
|
||
| ### The webhook URL | ||
|
|
||
| The URL is obtained by copying it from the workflow's trigger settings — you never assemble it from IDs. It is: | ||
|
|
||
| - **Stable** — it does not change when the workflow is published again. | ||
| - **Signed and tamper-evident** — it embeds the workflow and rendering; a forged or modified URL is rejected. | ||
| - **Valid until the editor invalidates it** — regenerating the URL (or disabling the trigger) is how access is revoked. See [Revoking access](#revoking-access). | ||
|
|
||
| Treat the URL as a credential and store it alongside the token. | ||
|
|
||
| ### Request body | ||
|
|
||
| The body is JSON and is validated. Only known fields are extracted; unknown fields are ignored. | ||
|
|
||
| ```json | ||
| { | ||
| "record_id": "42" | ||
| } | ||
| ``` | ||
|
|
||
| | Field | Type | Description | | ||
| |---|---|---| | ||
| | `record_id` | string | The record the workflow runs on. Composite primary keys are supported in their packed form (e.g. `"123|456"`). | | ||
|
|
||
| <Note> | ||
| The `record_id` is **not** verified when the run is created. If the record does not exist or is inaccessible to the token's user, the run is still created and fails at its first data step during execution — observable via the run state. | ||
| </Note> | ||
|
|
||
| Execution is **asynchronous**: the endpoint creates and queues the run, then returns immediately. The run is processed by the executor afterwards. | ||
|
|
||
| ### Example request | ||
|
|
||
| Use the URL you copied from the workflow's trigger settings as-is. In the examples below, `FOREST_WEBHOOK_URL` holds that copied URL. | ||
|
|
||
| **cURL:** | ||
|
|
||
| ```bash | ||
| curl -X POST "$FOREST_WEBHOOK_URL" \ | ||
| -H "Authorization: Bearer $FOREST_APPLICATION_TOKEN" \ | ||
| -H "Content-Type: application/json" \ | ||
| -d '{ "record_id": "42" }' | ||
| ``` | ||
|
|
||
| **JavaScript (Node.js):** | ||
|
|
||
| ```javascript | ||
| const response = await fetch(process.env.FOREST_WEBHOOK_URL, { | ||
| method: 'POST', | ||
| headers: { | ||
| Authorization: `Bearer ${process.env.FOREST_APPLICATION_TOKEN}`, | ||
| 'Content-Type': 'application/json', | ||
| }, | ||
| body: JSON.stringify({ record_id: '42' }), | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error(`Webhook request failed: ${response.status}`); | ||
| } | ||
|
|
||
| const { runId } = await response.json(); | ||
|
macroscopeapp[bot] marked this conversation as resolved.
|
||
| ``` | ||
|
|
||
| ### Response | ||
|
|
||
| On success the endpoint returns `202 Accepted` with the id of the created run: | ||
|
|
||
| ```json | ||
| { | ||
| "runId": "b1e6c2a4-7f3d-4e2a-9c1b-8d5f0a2e3c4d" | ||
| } | ||
| ``` | ||
|
|
||
| Use `runId` to follow the run's progress and outcome. | ||
|
|
||
| ### Idempotency | ||
|
|
||
| Only one run of a given workflow can be active on a given record at a time. If a run is already ongoing on the target record, the endpoint returns `409 Conflict` and does **not** start or resume a run: | ||
|
|
||
| ```json | ||
| { | ||
| "error": "A run of this workflow is already ongoing on this record." | ||
| } | ||
| ``` | ||
|
|
||
| This makes retries safe **while a run is still ongoing**: a caller that retries after a network error either starts the run (`202`) or learns one is already in progress (`409`). Note the `409` only holds for the duration of the active run — once it finishes, a new request on the same record starts a fresh run (`202`). Retry to recover from transient failures on the initial call, not to re-drive a record after its run has completed. | ||
|
|
||
| ### Rate limiting | ||
|
|
||
| The endpoint is rate-limited **per webhook**, so one noisy integration cannot starve other webhooks. Exceeding the limit returns `429 Too Many Requests` with a `Retry-After` header (in seconds). | ||
|
|
||
| The starting threshold is roughly **60 requests per minute per webhook**, and is configurable. Duplicate-record bursts are additionally absorbed by the single-run-per-record `409`. | ||
|
|
||
| ### Errors | ||
|
|
||
| | Status | Meaning | | ||
| |---|---| | ||
| | `400` | Invalid JSON body, missing `record_id`, or a malformed, tampered, or superseded URL (e.g. an old URL after it was regenerated). | | ||
| | `401` | Missing, expired, or invalid Bearer token. | | ||
| | `403` | Token is valid, but its user is not authorized for the target organization, rendering, or workflow. | | ||
| | `404` | Unknown webhook, no active workflow for the rendering, or the webhook trigger is disabled. | | ||
| | `409` | A run of this workflow is already ongoing on this record (see [Idempotency](#idempotency)). | | ||
| | `429` | Rate limit exceeded. The response includes a `Retry-After` header. | | ||
|
|
||
| ### Revoking access | ||
|
|
||
| The webhook keeps working until an editor intervenes. Two independent levers, both from the workflow's trigger settings: | ||
|
|
||
| - **Disable the webhook trigger** — the toggle is the revocation switch. The URL and token are unchanged, but calls return `404` while it is off. | ||
| - **Regenerate the URL** — invalidates the current URL immediately; the old URL then returns `400`. Update your integration with the new URL. | ||
|
|
||
| Independently, the **application token** can be expired or revoked by its user at any time, which makes calls return `401`. | ||
|
|
||
| ## Learn more | ||
|
|
||
| <CardGroup cols={2}> | ||
| <Card title="Workflow triggers" icon="bolt" href="/product/process/workflows/triggers"> | ||
| Enable and configure the webhook trigger from the workflow editor. | ||
| </Card> | ||
| <Card title="Authentication" icon="key" href="/reference/api/authentication"> | ||
| Generate and manage application tokens. | ||
| </Card> | ||
| </CardGroup> | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Medium
workflows/triggers.mdx:66The docs say to invalidate or expire the application token "from account settings," but tokens are actually managed under
Project Settings > API Access, not account settings. A user following these instructions will look in the wrong place and be unable to promptly revoke a compromised webhook token. Correct the location reference so users can find the token management UI quickly.🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dismissing this one: the workflow webhook trigger authenticates with an API token managed in Account Settings, which is a different scope from the public-API tokens under Project Settings > API Access. So invalidating the token from account settings is correct here. The canonical authentication.mdx also documents token creation under Account Settings.
The other three comments were addressed in 508884f (moved the endpoint out of the public-API table into a Note, added a
response.okcheck to the Node.js example, and scoped the "safe to retry" guidance to in-flight runs).