diff --git a/get-started/expose-to-ai-agents.mdx b/get-started/expose-to-ai-agents.mdx index 1efa396..0c8d2d5 100644 --- a/get-started/expose-to-ai-agents.mdx +++ b/get-started/expose-to-ai-agents.mdx @@ -5,7 +5,7 @@ description: "Turn on the MCP server so AI agents can act on your data under the The data, actions, and workflows you've built aren't just for the Forest UI. Forest ships an **MCP server** that exposes the same capabilities to AI agents like Claude, enforcing the exact same permissions and writing the same audit trail, regardless of whether a human or an AI agent is acting. -An AI agent request hits the /mcp endpoint, passes the same permissions and role check as a human (allowed or blocked), reaches the Forest back-end, and every action is written to the audit log +An AI agent request hits the /mcp endpoint, passes the same permissions and role check as a human (allowed or blocked), reaches the Forest back-end, and is written to the audit log ## What the MCP server exposes @@ -14,8 +14,9 @@ Through the Model Context Protocol, connected AI agents can: - Browse your collections and their schemas - Query and filter records across collections - Execute actions on records, including approval workflows +- Start [workflows](/product/process/workflows/triggers#the-mcp-trigger) on a record and follow their progress -Every operation runs with the permissions of the authenticated Forest user and is logged just like a UI action. +Every operation on data runs with the permissions of the authenticated Forest user, and is logged just like a UI action — with four exceptions, listed under [Identity, auditing, and limits](/product/embed/mcp-server#identity-auditing-and-limits). The three workflow tools are the exception to the permission half: discovering, triggering and reading runs are gated by the user's rendering and the workflow's MCP toggle only — no collection permission, no segment, and no check on the target record. What the run then reads and writes is bound by that user's permissions as usual. ## Turn on the MCP server diff --git a/images/workflows/trigger-rows.png b/images/workflows/trigger-rows.png new file mode 100644 index 0000000..999de3c Binary files /dev/null and b/images/workflows/trigger-rows.png differ diff --git a/images/workflows/webhook-trigger-rows.png b/images/workflows/webhook-trigger-rows.png deleted file mode 100644 index 03b6323..0000000 Binary files a/images/workflows/webhook-trigger-rows.png and /dev/null differ diff --git a/product/embed/mcp-server.mdx b/product/embed/mcp-server.mdx index de9cc95..79b3bc5 100644 --- a/product/embed/mcp-server.mdx +++ b/product/embed/mcp-server.mdx @@ -14,8 +14,9 @@ The Forest MCP server lets AI tools like Claude, Dust, and others to: - Access collection schemas - Securely query and browse your data - Execute actions on records +- Start [Workflows](/product/process/workflows/overview) on a record and follow their progress -All of this while respecting the Roles & Permissions of your Forest project, and logging every activity, just like if they were performed through the UI. +All of this while respecting the Roles & Permissions of your Forest project, and logging activity just like if it were performed through the UI — see [Identity, auditing, and limits](#identity-auditing-and-limits) for the four tools that are not logged. The Forest MCP Server also enables other third party apps to embed and access Forest data and actions, for example in [Zendesk](/product/embed/zendesk), or [n8n](/product/embed/n8n). @@ -154,6 +155,18 @@ The Forest MCP server exposes the following capabilities: | `executeAction` | Execute a smart action | | `requestActionFileUpload` | Get an upload destination for an action's `File` or `FileList` field | +### Workflows + +| Tool | Description | +| ----------------- | ------------------------------------------------------------ | +| `listWorkflows` | Discover the workflows enabled for MCP triggering | +| `triggerWorkflow` | Start a workflow run on a record | +| `getWorkflowRun` | Poll the status of a run started through MCP | + + + Workflows are **opt-in per workflow**: `listWorkflows` and `triggerWorkflow` only see those whose **MCP** trigger is enabled. Turning the toggle off stops discovery and new triggers, but a run already started through MCP stays readable with `getWorkflowRun` — including after it has finished, since the lookup is scoped to the run's rendering and trigger type, not to its state. See [Triggering workflows from an AI assistant](#triggering-workflows-from-an-ai-assistant). + + ## Restrict tools You can restrict which tools the MCP server exposes using `enabledTools`. Only the tools you list will be available, and **new tools added in future releases will NOT be automatically enabled**, so your configuration stays safe over time. @@ -414,6 +427,201 @@ url = "" Use the MCP transport type `"http"` (not `"sse"` or `"url"`): the Forest MCP server uses Streamable HTTP. Your URL should still use `https://`. Clients that rely on `mcp-remote` (Claude Desktop, Windsurf, JetBrains) require Node.js 18\+ (some versions need 20\+). +## Triggering workflows from an AI assistant + +Three tools let an assistant start and follow a [Workflow](/product/process/workflows/overview) from its own context: pick a workflow that fits the record at hand, start it, then watch the run. + + + A workflow is only reachable through MCP once someone who can manage workflows enables its **MCP** trigger in the workflow's [trigger settings](/product/process/workflows/triggers#the-mcp-trigger). Nothing is exposed by default. + + +### Discover → trigger → poll + +The three tools are meant to be chained, and the split is deliberate: MCP has no push channel, and a run is asynchronous — it can be long, or parked waiting for a person. So `triggerWorkflow` returns immediately with a `runId`, and the assistant polls `getWorkflowRun` for as long as it cares about the outcome — at a reasonable interval: the tool's own description tells the assistant to wait between calls and not to busy-loop on a long-running or human-gated run. + +```mermaid +flowchart LR + L["listWorkflows
(pick a workflow)"] --> T["triggerWorkflow
(→ runId)"] --> G["getWorkflowRun
(poll runState)"] + G -. "not finished yet" .-> G +``` + +1. **Discover** — `listWorkflows` returns the MCP-enabled workflows in the connected user's rendering, with the collection each one operates on. Pass `collectionName` to narrow it to the collection of the record in context. +2. **Trigger** — `triggerWorkflow` starts a run on one record and returns its `runId`. The run continues server-side; nothing blocks. +3. **Poll** — `getWorkflowRun` returns the full run: its state plus the complete step-by-step history, each step with its definition and outcome, so the assistant can see exactly where the run is and how it got there. + +### `listWorkflows` + +Lists workflows with the MCP trigger enabled, scoped to the connected user's rendering. + +| Argument | Description | +|---|---| +| `collectionName` | Optional. Narrows the results to workflows operating on that collection — typically the collection of the record in context. | + +```json Returns +[ + { + "workflowId": "9f1b0c4e-2f7a-4d5b-9e31-6c0a8b7d1234", + "name": "KYC review", + "collectionName": "customers" + } +] +``` + +An empty array means nothing matched. Without `collectionName`, that means no workflow is MCP-enabled in that rendering — most often because nobody has turned the toggle on yet. With `collectionName` set, it usually just means no MCP-enabled workflow operates on that collection: call `listWorkflows` again without the filter before concluding anything. + +Workflows whose collection was renamed or removed are left out, since they cannot be triggered. + +The listing is capped at **200 workflows** per call, and there is no pagination: the response is a bare array, so neither you nor the assistant can tell a full list from a truncated one. If a rendering can realistically pass 200 MCP-enabled workflows, use the `collectionName` filter to keep each call well inside the cap. + +### `triggerWorkflow` + +Starts a run of an MCP-enabled workflow on a specific record. + +| Argument | Description | +|---|---| +| `workflowId` | As returned by `listWorkflows`. Its MCP trigger must be enabled. | +| `recordId` | The record to run on. Composite primary keys use the packed form, values joined by `\|` (e.g. `"123\|456"`). | + +```json Returns +{ "runId": "1234", "runState": "pending" } +``` + +`runId` is what every subsequent `getWorkflowRun` call needs. `runState` is only the state at that instant — it depends on the workflow's first step, and it moves on without further calls, so treat it as a starting point, not an outcome. In practice it is `pending` (the run is queued for Forest Runtime), and occasionally `started` or `finished` when the first step needs no execution. `loading` means a runtime has claimed the run, which cannot have happened yet at this point. + + + The record is **not** checked when the run is created — the orchestrator has no data access at that point. An id that does not exist, or that the user cannot read, produces a run that fails at its first data step; the assistant sees it through the failing step's `context.error` in `getWorkflowRun`'s history, not as a trigger-time failure. Workflow segments are not enforced either: they control where the manual trigger appears in the interface, so an out-of-segment record is accepted — permission scopes still bound everything the run reads and writes. + + +Only **one run of a given workflow** can be active on a given record at a time. Triggering a record that already has an ongoing run of the same workflow fails and does **not** resume it — the run in flight is left untouched. A different workflow can still start on that record. + +"Active" is wider than "progressing": a run parked on a human step and a run whose step errored both sit in `started`, so they keep blocking new triggers on that record until they are finished from the Forest UI or aborted. + +### `getWorkflowRun` + +Reads the full run, given the `runId` returned by `triggerWorkflow`. The run carries no record payload — records live in the executor — so the whole run is returned, giving the assistant maximum context about where it is and what each step does. Identifiers (`selectedRecordId`) and step error messages (`context.error`) may still contain customer data. + +| Field | Description | +|---|---| +| `runState` | `pending` or `loading` while a step is queued or executing, `started` when the run is parked, `finished` on completion, `aborted` when stopped. There is no dedicated *failed* state: a step failure surfaces in that step's `context.error`. | +| `triggerType` | Always `mcp` here — only MCP-started runs are readable through this tool. | +| `workflowHistory` | The ordered steps the run has reached. Each entry pairs the step's resolved **definition** (`stepDefinition`: `type`, `title`, an `executionType` — `manual`, `automated-with-confirmation` or `fully-automated` — plus an optional `taskType` and `prompt`, and the `outgoing` branches) with its **outcome** (`stepName`, `stepIndex`, `done`, plus an optional `context` carrying the selected option, a manual completion, an escalation state, an awaiting-input reason, or an `error`). Read `runState` first to know whether the run is still live — the terminal `end` entry is never marked done, so a `finished` run still ends on a `done: false` entry. While the run is live, the last entry with `done: false` is where it sits. | + +The run comes back with every field the contract declares, and only those — Forest projects the response onto that list, so a field added server-side never reaches the assistant without a corresponding release. **One exception:** `stepDefinition` is forwarded whole, deliberately, so the assistant can reason about what each step does. It is your own workflow configuration — titles, prompts, configured argument values — and it is the one part of the payload that is not filtered, so anything you put in a step's configuration is readable by the assistant. What you get: identifying fields (`id` — the numeric form of the `runId` string, `workflowId`, `collectionId`, `selectedRecordId`, the `createdAt`/`updatedAt` timestamps) and internal ones (`userId`, `renderingId`, `bpmnVersion`, `engine`, `lockedAt`) come with it. History entries likewise carry an `isCardStep` flag, and every `stepDefinition` an `automaticCompletion` one. The examples below are trimmed to the load-bearing fields. + +One naming trap: `stepName` and `outgoing[].stepId` are **BPMN element ids, not labels** — they come from the diagram, so they look like `Activity_ReviewKyb`, not like a sentence. They share one identifier space, so a step's `outgoing[].stepId` is the `stepName` of the entry that follows it. The human-readable name is `stepDefinition.title`. + + + +```json Parked on a human step +{ + "id": 1234, + "workflowId": "9f1b0c4e-2f7a-4d5b-9e31-6c0a8b7d1234", + "selectedRecordId": "42", + "runState": "started", + "triggerType": "mcp", + "workflowHistory": [ + { + "stepName": "Activity_ReviewKyb", + "stepIndex": 0, + "done": false, + "context": {}, + "stepDefinition": { + "type": "task", + "taskType": "guideline", + "title": "Review the KYB documents", + "prompt": "Check the uploaded documents, then approve or reject", + "executionType": "manual", + "outgoing": [{ "stepId": "Event_CustomerApproved", "buttonText": "Approve" }] + } + } + ] +} +``` + +```json Finished +{ + "id": 1234, + "workflowId": "9f1b0c4e-2f7a-4d5b-9e31-6c0a8b7d1234", + "selectedRecordId": "42", + "runState": "finished", + "triggerType": "mcp", + "workflowHistory": [ + { + "stepName": "Activity_ReviewKyb", + "stepIndex": 0, + "done": true, + "context": { "manuallyCompleted": true }, + "stepDefinition": { + "type": "task", + "taskType": "guideline", + "title": "Review the KYB documents", + "prompt": "Check the uploaded documents, then approve or reject", + "executionType": "manual", + "outgoing": [{ "stepId": "Event_CustomerApproved", "buttonText": "Approve" }] + } + }, + { + "stepName": "Event_CustomerApproved", + "stepIndex": 1, + "done": false, + "context": {}, + "stepDefinition": { + "type": "end", + "title": "Customer approved", + "executionType": "manual", + "outgoing": [] + } + } + ] +} +``` + + + + + `getWorkflowRun` only exposes runs that were **started through MCP**. A run triggered manually or by webhook is not observable here, even by the same user — asking for its id returns a not-found error. + + Conversely, the scope is the **rendering**, not the user: any MCP session on the same rendering can read any MCP-started run in it, including one another user started. Since a run carries `selectedRecordId` and step error messages, treat run history as readable by everyone who can reach that rendering through MCP. + + +### Runs that need a human + +In this first version the assistant can *observe* a parked run but not answer it. + +A run is waiting on a person when `runState` is `started` and its last history entry carries no `context.error`. That covers two shapes: the step is still `done: false` and awaiting an answer, or it is already `done: true` and someone has to confirm before the run advances. Don't read `stepDefinition.executionType` as the signal — a terminal `end` step is `manual` too, and a `finished` run is not parked. + +A `started` run whose last entry *does* carry a `context.error` is a failed step rather than a question. Either way the run is routed to the workflow's **fallback inbox** (when one is configured), and someone finishes it from the Forest UI. Relaying the step's question into the chat and submitting the answer through MCP is not available in this version. + +### Errors + +Tool failures come back as tool errors with an explanatory message, so the assistant can react rather than crash: + +| Situation | What the assistant gets | +|---|---| +| The workflow is unknown, its MCP trigger is off, or it is outside the user's rendering | Not found — with a hint to call `listWorkflows`. The three cases are deliberately indistinguishable, so the tool cannot be used to probe which workflows exist | +| The `workflowId` is not a UUID — for instance a workflow *name* rather than its id | Rejected, quoting Forest's reason, and told that retrying will not help | +| The workflow's collection is unavailable (renamed or deleted — `listWorkflows` hides these, so this only happens if the collection changed after the listing) | Rejected up front, pointing at the workflow's configuration; nothing starts | +| A run of the same workflow is already ongoing on that record | Conflict; no run is started and none is resumed | +| The `runId` is unknown, belongs to a different rendering, or was not started through MCP | Not found | +| An empty `workflowId`, `recordId` or `runId` | Invalid argument — rejected before any call, nothing starts | +| A `recordId` longer than 255 characters (the column bound) | Invalid argument — rejected before any call, nothing starts | +| A `runId` that is not a positive integer within range | Invalid argument — the call reaches Forest and is refused there; nothing is read | +| The environment's workflows still run in the browser engine | Conflict — automated triggering needs server-side execution ([Forest Runtime](/product/process/workflows/forest-runtime)) | +| The workflow contains an MCP Task step targeting an OAuth2-protected connector, and the environment's [Forest Runtime](/product/process/workflows/forest-runtime) is older than 1.14.0 | Unprocessable — upgrade Forest Runtime to run these steps server-side; nothing starts | + +### Identity, auditing, and limits + +- **Identity** — the run executes as the Forest user of the MCP session, established by the OAuth login. That user's permissions bound everything the run **reads and writes**. The trigger itself is gated only by the user's rendering and the workflow's MCP toggle: neither the target record nor the workflow's segments are checked when the run is created. +- **Auditing** — each trigger is recorded in the run history and in your **Activity Logs**, attributed to that user and labelled *via MCP*, so MCP-started runs are distinguishable from manual and webhook ones. An MCP trigger writes two complementary entries, and they are worded differently on purpose: + - *requested the workflow "…" via MCP* — written **before** the run starts, and the trigger is refused if it cannot be written. It has no run attached, because the run does not exist yet. + - *triggered the workflow "…" via MCP* — written once the run is committed, carrying its run id. + + Only the first is guaranteed: the run-attached entry is best-effort, so a successful trigger can leave just the request entry. Count *triggered* rows to count runs actually started, and *requested* rows to count what assistants asked for. +- **Four tools are not audited** — `listWorkflows`, `getWorkflowRun`, `getActionForm` and `requestActionFileUpload` leave no Activity Logs entry. Three of them have nothing to attach an entry to, since the Activity Logs route needs a collection: the two workflow reads operate on the orchestrator rather than a collection, and `requestActionFileUpload` is identified only by a filename and a checksum. `getActionForm` is the exception — it does receive a collection and record ids, so nothing stands in the way of auditing it; it simply predates this work and was never wired to the audit path. `requestActionFileUpload` is worth calling out separately: it is not a read — it mints a pre-authorized upload URL — and it is enabled by default, including when no storage backend is configured. Every other tool, read or write, writes an entry. Closing these gaps is on the roadmap. +- **When the audit log itself fails** — a **write** whose log cannot be created is blocked, so no side effect happens unaudited. A **read** proceeds with a warning, so an audit-store outage never takes the read surface down. An **authorization refusal** (the caller's identity was rejected) propagates either way — it is not an outage. +- **Rate limiting** — the workflow tools have no dedicated limiter. They inherit the MCP server's authentication, and no per-call rate limit applies; unlike the [webhook trigger](/reference/api/endpoints/trigger-workflow-webhook#rate-limiting), there is no separately exposed HTTP endpoint to protect — every call happens inside an authenticated MCP session. The one-run-per-workflow-per-record rule prevents duplicate runs on the *same* record, but nothing bounds how many records an assistant can trigger on — walking a list view opens one run per record, each consuming Forest Runtime capacity. `triggerWorkflow` is annotated `destructiveHint: true`, but MCP annotations are advisory: whether a call is confirmed, auto-approved, or allowlisted for the rest of the session is entirely up to the client. Check and configure that behaviour in the MCP client you connect, and treat the per-workflow `mcp` toggle as the only guard Forest itself enforces. +- **Turning it off** — disable a single workflow's MCP toggle, or set an [`enabledTools`](#restrict-tools) allowlist that leaves `triggerWorkflow` out to remove MCP triggering across all workflows (when `enabledTools` is unset, every tool is enabled — restricting means listing the tools you keep). Either way, manual and webhook starts of that workflow keep working. Revoking the connected user's access stops new triggers immediately, and the assistant is told not to retry; a run already in flight is not aborted, and keeps going until it next touches data. By default the agent refreshes its permissions from Forest's event stream, so a revocation lands within seconds; an agent started with `instantCacheRefresh: false` instead waits out its permission cache — 15 minutes by default, or whatever `permissionsCacheDurationInSeconds` is set to. Neither delay is a guarantee: abort a run explicitly if you need it stopped now. See [Revoking MCP access](/product/process/workflows/triggers#revoking-mcp-access). + ## Use cases ### AI-assisted operations @@ -433,16 +641,20 @@ Use Claude or other AI assistants to: > "Execute the 'Send Invoice' action on order #12345" +> "Start the KYC review workflow on customer #482 and tell me where it gets to" + ## Security The Forest MCP server: - Respects all Forest permissions and roles - Uses your environment's authentication -- Logs all operations for audit purposes -- Never exposes sensitive data without proper access +- Logs every operation except four tools — `getActionForm`, `requestActionFileUpload`, `listWorkflows` and `getWorkflowRun` — for audit purposes (see [Identity, auditing, and limits](#identity-auditing-and-limits)) +- Bounds what a run reads and writes by the connected user's permissions. Note this does not bound the *trigger*: neither the target record nor the workflow's segments are checked when a run is created +- Carries no record payload out of your infrastructure — records stay in Forest Runtime. Identifiers (`selectedRecordId`) and step error messages (`context.error`) can still contain customer data, so treat a run's history as sensitive - Lets you restrict which AI client applications can connect (see [Restrict which AI clients can connect](#restrict-which-ai-clients-can-connect)) - Lets you shorten the OAuth token lifetimes (see [Token lifetimes](#token-lifetimes)) +- Exposes no workflow until someone who can manage workflows opts that workflow in (see [Triggering workflows](#triggering-workflows-from-an-ai-assistant)) Only provide MCP server access to trusted AI tools and users. The server can perform any operation that the authenticated user can perform. diff --git a/product/execute/workflows.mdx b/product/execute/workflows.mdx index 692487f..af4494f 100644 --- a/product/execute/workflows.mdx +++ b/product/execute/workflows.mdx @@ -31,7 +31,7 @@ Workflows can be executed from: The workflow opens in a guided panel. The user works through each step; the workflow context accumulates as they go. -Workflows can also be started automatically by an external system through a [webhook trigger](/product/process/workflows/triggers), without a user in the interface. +Workflows can also start without a user in the interface — from an external system through a [webhook](/product/process/workflows/triggers), or from an AI assistant through the [Forest MCP server](/product/embed/mcp-server). When such a run reaches a step that needs a human, it is routed to the workflow's **fallback inbox** (when one is configured), where an operator picks it up and finishes it as usual. ## AI-powered steps diff --git a/product/process/workflows/forest-runtime.mdx b/product/process/workflows/forest-runtime.mdx index 871eb46..60b02ec 100644 --- a/product/process/workflows/forest-runtime.mdx +++ b/product/process/workflows/forest-runtime.mdx @@ -15,13 +15,17 @@ Because it talks to your data through your own Forest agent, **the Forest orches ## Do I need Forest Runtime? -Yes — Forest Runtime is what executes your workflow steps. **[Webhook-triggered workflows](/product/process/workflows/triggers) especially**: a webhook can fire at any time, with no guarantee anyone has Forest open in a browser, so they can only run server-side. +Yes — Forest Runtime is what executes your workflow steps. **[Automatically-triggered workflows](/product/process/workflows/triggers) especially**: a webhook call or an AI assistant can fire at any time, with no guarantee anyone has Forest open in a browser, so those runs can only execute server-side. Until a runtime has reported in, the workflow settings page shows a *"Your Forest Runtime can't be reached"* screen and neither automated trigger can be enabled. Running on infrastructure you control also keeps the records handled by data steps out of Forest's infrastructure — decisive for compliance or data-residency requirements, or when a step needs access to systems reachable only from within your network. + + **Upgrade Forest Runtime before you enable the MCP trigger.** A runtime older than the release that added MCP triggering rejects MCP-started runs when it validates them: the run is created, picked up, and immediately errored — its first step carries a validation message, and `getWorkflowRun` reports that error rather than a run in progress. There is no dedicated failed state: the run stays in `started`, and it is routed to the fallback inbox, so that inbox fills up with runs that never began. Because `started` counts as active, **each affected record is then blocked from being triggered again on the same workflow until you abort its run**. Nothing warns you up front, and there is no version check: the older runtime simply rejects `triggerType: "mcp"` as an unknown value when it validates the step, so the message recorded there is the generic *"Internal validation error occurred while preparing the step. Please contact support."* — it names neither the version nor the trigger type. Upgrade first, then turn the toggle on. + + ## How it works -1. A workflow is triggered — by a user or a [webhook](/product/process/workflows/triggers). The Forest orchestrator queues the pending steps. +1. A workflow is [triggered](/product/process/workflows/triggers) — by a user, a webhook, or an AI assistant. The Forest orchestrator queues the pending steps. 2. Forest Runtime polls the orchestrator and pulls the steps assigned to it. 3. Each step runs locally, reaching your data and actions through your Forest agent. 4. It reports the step outcome back to the orchestrator, which advances the workflow. diff --git a/product/process/workflows/overview.mdx b/product/process/workflows/overview.mdx index 6049e0f..3583a63 100644 --- a/product/process/workflows/overview.mdx +++ b/product/process/workflows/overview.mdx @@ -88,10 +88,10 @@ Workflows can also be made available in Summary Views and Workspaces. ### Triggering workflows -By default, workflows are started manually by users from the interface. A workflow can also be triggered automatically by an external system through a **webhook**, configured in the **Process** section of the workflow settings. +By default, workflows are started manually by users from the interface. A workflow can also be started without anyone in the interface: by an external system through a **webhook**, or by an AI assistant through the **[Forest MCP server](/product/embed/mcp-server)**. Both are opt-in per workflow, from the **Triggers** section of the workflow settings. - Enable manual and webhook triggers, and manage the webhook URL and token + Enable the manual, webhook, and MCP triggers, and manage access to each --- diff --git a/product/process/workflows/triggers.mdx b/product/process/workflows/triggers.mdx index 0adf761..03c0c2f 100644 --- a/product/process/workflows/triggers.mdx +++ b/product/process/workflows/triggers.mdx @@ -1,14 +1,39 @@ --- title: "Workflow triggers" -description: "Choose how a workflow starts, manually from the interface, or automatically from an external system via a webhook." +description: "Choose how a workflow starts — manually from the interface, from an external system via a webhook, or from an AI assistant through the Forest MCP server." --- -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. +A workflow can be started in three independent ways: -- **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.** +| Trigger | Who starts the run | Default | +|---|---|---| +| **Manual** | Users, from a matching record in the interface (List View, Summary/Details, or a Workspace). See [Executing workflows](/product/execute/workflows). | Enabled | +| **Webhook** | Any external system, via an authenticated HTTP POST to a stable URL. | Disabled | +| **MCP** | An AI assistant (Claude, Cursor, …) connected to the [Forest MCP server](/product/embed/mcp-server). | Disabled | + +Each has its own on/off toggle and they can be enabled in any combination. Toggling one **never** affects the others: turning MCP off blocks MCP triggering only, and the same workflow keeps starting from the interface and from its webhook. + +All three are available on **all environments**, with no production-only restriction. + +## Where triggers are configured + +Open the workflow settings page and go to the **Triggers** section. **Manual** sits at the top with its own toggle, followed by an **Automated** group holding the **Fallback Inbox** selector and the **Webhook** and **MCP** rows. + + + Triggers section: the Manual row, then the Automated group with the fallback inbox, the webhook, and the MCP toggle + -Webhook triggers are available on **all environments**, with no production-only restriction. +### Before you can enable an automated trigger + +Both automated triggers (Webhook and MCP) share three prerequisites. + +- **You must be able to manage workflows.** All three trigger toggles — Manual included — are disabled below the **Admin**, **Process Owner** or **Developer** level, since turning an automated trigger on is what makes a workflow startable with nobody watching. This is enforced in the interface; the underlying layout permission is broader today, so the API still accepts the change from an Editor. +- **[Forest Runtime](/product/process/workflows/forest-runtime) must be running.** An automated run can fire at any time, with nobody holding Forest open in a browser, so its steps can only run server-side. Until a runtime has reported in, the workflow settings page shows *"Your Forest Runtime can't be reached"* instead of the trigger rows. On an environment still configured for the legacy browser engine, the rows do show but both toggles are locked, with the tooltip *"This environment still runs workflows in the browser. Automated triggers need Forest Runtime."* +- **A fallback inbox must be selected.** Until one is picked, the Webhook and MCP toggles stay locked, with a tooltip saying so. When an automated run reaches a step that needs a human, there is no operator on the other end to hand it to — so the run is routed to that inbox for someone to pick up. See [Inboxes & escalations](/product/manage/inbox). + + + A trigger that is already enabled stays actionable even if its fallback inbox is later cleared, so you can always turn it **off**. It re-locks once disabled. + ## The webhook trigger @@ -21,9 +46,9 @@ Two things are separated by design: 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 +### Enabling the webhook -1. Open the workflow settings page and go to the **Process** section. +1. Open the workflow settings page and go to the **Triggers** section. 2. Toggle **Webhook** on. Once enabled: @@ -32,13 +57,9 @@ Once enabled: - 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)). - - Process section with Manual and Webhook trigger rows beneath the version card - - 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 +### 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. @@ -55,7 +76,7 @@ When you confirm: Generate new URL button and the invalidation warning -## Revoking access +### Revoking webhook access You have three independent levers to stop a webhook, without necessarily touching the others: @@ -67,9 +88,47 @@ You have three independent levers to stop a webhook, without necessarily touchin Turning the toggle back on re-enables the *same* URL and token — it is a pause, not a reset. +## The MCP trigger + +When enabled, an AI assistant connected to the [Forest MCP server](/product/embed/mcp-server) can discover this workflow, start it on a record, and follow the run's progress — using three tools: `listWorkflows`, `triggerWorkflow`, and `getWorkflowRun`. + +There is nothing to copy or rotate here: **the toggle is the whole configuration**. The assistant is already authenticated against the MCP server through OAuth, and that session's Forest user is the identity the run executes under — the same model as a manual start, not the webhook's separately-provisioned URL and token. + +Enabling MCP triggering is what makes the workflow reachable by assistants — and it is the *only* thing that does: + +- `listWorkflows` returns only the MCP-enabled workflows in the connected user's rendering; +- `triggerWorkflow` on a workflow whose MCP toggle is off fails — even if the assistant already knows its id. + + + **Hiding a workflow from the interface does not hide it from MCP.** Assistant exposure is driven solely by the MCP toggle, not by the workflow's visibility — so a workflow you retired by hiding it stays listable and triggerable until you also turn its MCP toggle off. + + +### Enabling MCP triggering + +1. Open the workflow settings page and go to the **Triggers** section. +2. Toggle **MCP** on. + +That's it. Assistants connected to your Forest MCP server pick the workflow up on their next `listWorkflows` call. + +### What an assistant can and cannot do + +- It **can** start the workflow on a record and poll the run's state, current step, and outcome. +- It **cannot** exceed the connected user's permissions on the data the run reads and writes — every data step is gated by [Roles & permissions](/get-started/control/roles-permissions). The trigger itself is bounded by the user's rendering and the MCP toggle only: the record id the assistant supplies is not checked at trigger time (see [the note on unvalidated records](/product/embed/mcp-server#triggerworkflow)). +- It **cannot** answer a step that needs a human. A run parked on such a step is *reported* to the assistant, but finishing it happens in the Forest UI, from the fallback inbox (when one is configured). + +For the tool contracts and the discover → trigger → poll flow, see [Triggering workflows from an AI assistant](/product/embed/mcp-server#triggering-workflows-from-an-ai-assistant). + +### Revoking MCP access + +| Lever | Effect | +|---|---| +| **Disable the MCP toggle** | The workflow disappears from `listWorkflows` and can no longer be triggered through MCP. Manual and webhook starts are unaffected. | +| **Restrict the MCP server's tools** | Set an `enabledTools` allowlist that leaves `triggerWorkflow` out to remove MCP triggering across *all* workflows at once — when `enabledTools` is unset, every tool is enabled. See [Restrict tools](/product/embed/mcp-server#restrict-tools). | +| **Revoke the user's access** | The assistant acts as its connected Forest user. Removing that user's access to the rendering stops new triggers immediately — the assistant is told the workflow could not be resolved and that retrying will not help. A run already in flight is not aborted: it keeps going until it next touches data, then fails on the permission check and stalls with the error recorded on the current step. By default the agent refreshes its permissions from Forest's event stream, so the revocation lands within seconds; an agent started with `instantCacheRefresh: false` instead waits out its permission cache — 15 minutes by default, or whatever `permissionsCacheDurationInSeconds` is set to. Neither delay is a guarantee: abort a run explicitly if you need it stopped now. | + ## 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. +Every automated run is recorded in the workflow run history and in your **Activity Logs**, attributed to the user the run acted as, and labelled by channel — *via webhook* or *via MCP* — so you can tell automated runs from manual ones, and from each other. An MCP trigger writes two complementary entries: *requested the workflow "…" via MCP* before the run starts (the audit that blocks the trigger if it cannot be written, with no run attached yet), and *triggered the workflow "…" via MCP* once the run exists. Only the first is guaranteed — the second is best-effort — so count *triggered* rows for runs actually started. ## Learn more @@ -77,6 +136,12 @@ Every webhook-triggered run is recorded in the workflow run history and in your The HTTP contract: body, response codes, idempotency, rate limits. + + The workflow tools, and the discover → trigger → poll flow. + + + Required for automated triggers: run your workflow steps server-side. + Build and manage workflows in the no-code editor. diff --git a/reference/agent-api/nodejs.mdx b/reference/agent-api/nodejs.mdx index 17c3b7f..0664c9d 100644 --- a/reference/agent-api/nodejs.mdx +++ b/reference/agent-api/nodejs.mdx @@ -238,7 +238,7 @@ agent.mountAiMcpServer(options?: { | `tokenTtl` | `{ accessTokenSeconds?: number; refreshTokenSeconds?: number }` | Shorten the OAuth token lifetimes issued by the MCP server (defaults: 1 hour for access tokens, unbounded session for refresh). Upper bounds only — they can never extend what Forest grants; minimum `60` seconds each. See [Token lifetimes](/product/embed/mcp-server#token-lifetimes). Since `@forestadmin/agent` 1.91.0. | | `fileUploads` | `false \| { storage?, maxBytes?, ... }` | Configure action file uploads, which are **on by default** with the files held in memory — pass `false` to turn the feature off, or an object to set a `storage` backend, size limits and TTLs. Experimental. See [Action file uploads](/product/embed/mcp-server#action-file-uploads). Since `@forestadmin/agent` 1.95.0. | -**Available tool names:** `'describeCollection'`, `'list'`, `'listRelated'`, `'create'`, `'update'`, `'delete'`, `'associate'`, `'dissociate'`, `'getActionForm'`, `'executeAction'`, `'requestActionFileUpload'` +**Available tool names:** `'describeCollection'`, `'list'`, `'listRelated'`, `'create'`, `'update'`, `'delete'`, `'associate'`, `'dissociate'`, `'getActionForm'`, `'executeAction'`, `'requestActionFileUpload'`, `'listWorkflows'`, `'triggerWorkflow'`, `'getWorkflowRun'` **Example:** diff --git a/reference/api/endpoints/trigger-workflow-webhook.mdx b/reference/api/endpoints/trigger-workflow-webhook.mdx index 6675eb6..6e312f3 100644 --- a/reference/api/endpoints/trigger-workflow-webhook.mdx +++ b/reference/api/endpoints/trigger-workflow-webhook.mdx @@ -58,7 +58,7 @@ The body is JSON and is validated. Only known fields are extracted; unknown fiel | 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"`). | +| `record_id` | string | The record the workflow runs on. Composite primary keys are supported in their packed form (e.g. `"123\|456"`). | 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.