Skip to content

docs(workflows): MCP triggering — tools, flow and trigger settings (PRD-742) - #21

Open
christophebrun-forest wants to merge 15 commits into
mainfrom
feature/prd-742-mcp-workflow-tools-docs
Open

docs(workflows): MCP triggering — tools, flow and trigger settings (PRD-742)#21
christophebrun-forest wants to merge 15 commits into
mainfrom
feature/prd-742-mcp-workflow-tools-docs

Conversation

@christophebrun-forest

@christophebrun-forest christophebrun-forest commented Aug 3, 2026

Copy link
Copy Markdown
Member

Documents the MCP trigger channel shipped by PRD-49, closing the docs half of PRD-742.

Written from the implementation in the three feature branches (front #9870, mcp-server #1792, orchestrator #8418), not from the spec — see Deviations below.

What changed

product/embed/mcp-server.mdx — the bulk of it

  • New Workflows group in Available tools
  • New section Triggering workflows from an AI assistant: the discover → trigger → poll flow (mermaid), the arg/return contract of each tool, the runState table, JSON examples for a parked and a finished run, an error table, and Identity, auditing, and limits covering the two v1 limitations PRD-742 asks for — no dedicated rate limiter and report-only human-gated runs

product/process/workflows/triggers.mdx — two triggers → three

  • Manual / Webhook / MCP table, channel-aware gating stated up front
  • MCP section: what the toggle exposes, what an assistant can and cannot do, three revocation levers
  • Fixes stale content that predates this work: the section is named Triggers (not Process), and the prerequisites for automated triggers (Forest Runtime installed + fallback inbox selected) were documented nowhere despite gating the webhook toggle too
  • Screenshot refreshed to show the MCP row

Aligned neighboursoverview.mdx, execute/workflows.mdx (automated runs land in the fallback inbox), forest-runtime.mdx (MCP triggering needs the Runtime too). Plus an unescaped pipe that broke a table in the webhook API reference.

Deviations from the spec, followed as implemented

  • listWorkflows returns no description — only workflowId, name, collectionName. The docs don't promise one.
  • getWorkflowRun is scoped to MCP-triggered runs (triggerType='mcp' in the store query): a manual or webhook run 404s, even for the same user. Not in the spec; documented as a <Warning>.
  • getWorkflowRun returns the full hydrated run (runState, triggerType, and the complete workflowHistory — each step's resolved definition and per-step context), not a normalized status. The run carries no record payload, so it is surfaced whole for LLM context (identifiers and step error strings may still hold customer data).
  • runState returned by triggerWorkflow depends on the workflow's first step, so the docs tell readers not to treat it as an outcome.

No-regression verification (2nd AC of PRD-742)

Confirmed by reading the code, not by running it: MCP gating lives in startWorkflowRunFromMcp, webhook gating in workflow-webhook-route.ts:106, and the manual /start path has neither — startWorkflowRun is only touched by an audit-label refactor. Covered by should not gate the manual /start path (no regression) on an mcp-enabled workflow (workflow-orchestrator-endpoint.test.ts:1161). No symmetric webhook test exists, but the webhook channel never reads the mcp toggle.

Checks

mint broken-links passes.

🤖 Generated with Claude Code

Note

Document MCP trigger for workflows including tools, flow, and trigger settings

  • Adds a new 'Workflows' tools section to mcp-server.mdx describing listWorkflows, triggerWorkflow, and getWorkflowRun, plus a full guide covering the discover → trigger → poll flow, run state model, constraints, and human-in-the-loop behavior.
  • Restructures triggers.mdx to introduce a three-trigger model (Manual, Webhook, MCP) with a comparison table, UI guidance, prerequisites (Forest Runtime installed, fallback inbox selected), and a dedicated MCP trigger section.
  • Updates overview.mdx, forest-runtime.mdx, and workflows.mdx to reference MCP as an opt-in trigger source alongside webhooks.
  • Fixes a pipe-character escaping bug in the composite primary key example in trigger-workflow-webhook.mdx.

Changes since #21 opened

  • Replaced getWorkflowRun response model from current-state fields to history-based model [488139f]
  • Added documentation for AI agents to start workflows via MCP and follow their progress [17b6774]
  • Documented MCP workflow triggering tools, execution semantics, error conditions, and access control [17b6774]
  • Updated MCP server tool restriction settings documentation [17b6774]
  • Changed example workflow stepDefinition configuration executionType property from "fully-automated" to "manual" [4361f16]
  • Updated documentation text to specify "a workflow editor" instead of "an editor" with corresponding link text adjustment [4361f16]
  • Clarified that MCP triggers generate two complementary audit log entries labeled via MCP: one pre-run audit entry and one attached to the workflow run [ec3b24d]
  • Updated user access revocation behavior to specify that revoking a user's access halts in-flight runs at the next data step rather than aborting them [ec3b24d]
  • Clarified fallback inbox routing occurs only when a fallback inbox is configured for the workflow [ec3b24d]
  • Updated getWorkflowRun documentation to clarify data privacy boundaries: runs carry no record payload, but identifiers like selectedRecordId and step error messages in context.error may contain customer data [ec3b24d]
  • Refined listWorkflows description to specify it returns only MCP-enabled workflows in the connected user's rendering [ec3b24d]
  • Added error documentation for when a workflow's collection is unavailable, noting the workflow may still appear in listWorkflows with a null collectionName [ec3b24d]
  • Changed logging scope description from 'is logged just like a UI action' to 'every state-changing operation is logged just like a UI action' to indicate only state-changing operations are logged [ec3b24d]
  • Clarified permission boundaries for MCP-triggered workflows: data steps are limited by user permissions, while the trigger itself is bounded only by the user's rendering and the MCP toggle, with recordId not validated at trigger time [ec3b24d]
  • Expanded MCP server documentation to include additional run field details and reclassified steps requiring human intervention [0684448]
  • Clarified workflow trigger documentation regarding where parked runs can be finished [0684448]
  • Clarified workflowHistory tool behavior to check runState first, specified that terminal end steps are never marked done: true, explained that the last entry with done: false indicates current position in live runs, and redefined human-waiting criteria based on runState and context.error presence rather than stepDefinition.executionType [72b63a2]
  • Added Forest Runtime version and connectivity requirements, warning that older runtimes reject MCP-started runs causing them to remain in pending state without visible warnings, and specifying that until runtime reports in the settings page shows unreachable screen and automated triggers cannot be enabled [72b63a2]
  • Described UI behavior for trigger prerequisites, specifying that trigger rows are hidden when Forest Runtime hasn't reported in, legacy browser environments show locked toggles with 'Ask your tech team to install the Workflow Executor' tooltip, and Webhook and MCP toggles remain locked with tooltip until fallback inbox is selected [72b63a2]
  • Clarified auditing and logging behavior to specify that three read operations (listWorkflows, getWorkflowRun, getActionForm) are not logged, while all write operations and other reads are audited, with additional details about audit-store outage behavior [19fe126]
  • Rewrote identity and permissions model to clarify that permissions bound reads and writes, while workflow run creation via MCP is gated only by workflow rendering permissions and the MCP toggle, without target record or segment checks at creation time [19fe126]
  • Documented user access revocation behavior, specifying that removing access stops new triggers immediately, in-flight runs continue until the next data access where they fail and stall with error, permission cache may take up to 15 minutes to expire, and explicit abort is required for immediate stop [19fe126]
  • Added warning and clarifications that the MCP toggle is the sole control for workflow exposure to AI assistants, and that hiding a workflow from the user interface does not hide it from MCP [19fe126]
  • Expanded explanation of why listWorkflows may return an empty array, including behavior differences with and without collectionName filtering, omission of workflows whose collection was renamed or removed, and recommendation to call without filter before concluding no workflows exist [19fe126]
  • Expanded rate limiting section to explain that duplicate run prevention applies only per workflow and record combination, with guidance on risks across many records, destructive operations, and user confirmation requirements [19fe126]
  • Reworded the Forest Runtime prerequisite tooltip to clarify that the environment still runs workflows in the browser but automated triggers require Forest Runtime [19fe126]
  • Renamed workflow trigger image asset and updated corresponding image reference in triggers documentation [21bd095]
  • Documented server-side execution failure for workflows containing MCP Task steps with OAuth2-protected connectors [21bd095]
  • Updated unaudited tool exceptions from three to four by adding requestActionFileUpload to the list of tools not logged in audit logs alongside listWorkflows, getWorkflowRun, and getActionForm, with rationale that requestActionFileUpload mints upload URLs and is enabled by default when file uploads are configured [712e294]
  • Changed triggerWorkflow response runState from 'loading' to 'pending' and expanded runState semantics documentation to explain that 'pending' is typical, 'started' or 'finished' may occur occasionally, and 'loading' cannot occur at trigger time [712e294]
  • Documented that enabling MCP trigger with outdated Forest Runtime causes runs to be picked up and immediately marked failed with a validation error on the first step, with the error visible via getWorkflowRun, replacing previous behavior description of runs stuck in 'pending' forever [712e294]
  • Added visibility filtering for listWorkflows and triggerWorkflow so they only show workflows with MCP trigger enabled, while getWorkflowRun remains readable for already-started runs even if the toggle is later turned off [712e294]
  • Added explicit 200 workflow cap per call for listWorkflows with no pagination support and guidance to use collectionName filter to stay within the cap [712e294]
  • Updated tool failures matrix to document that listWorkflows hides collection-unavailable workflows so rejection only occurs if collection changed after listing, and added explicit invalid argument cases including empty workflowId/recordId/runId rejection, recordId over 255 characters rejection, and runId out of range rejection by Forest [712e294]
  • Refined OAuth2-protected connector limitation to apply only when Forest Runtime is older than version 1.14.0 and added advice to upgrade [712e294]
  • Clarified that getWorkflowRun response is projected onto a declared contract with only specific field categories returned, and noted that examples are trimmed [712e294]
  • Added documentation clarifying that permissions bound reads and writes but not the trigger checks, and that record payloads remain in Forest Runtime while identifiers and step error messages may still contain sensitive data [712e294]
  • Updated rate limiting documentation to clarify no per-call rate limit exists and reiterated confirmation and MCP toggle guidance [712e294]
  • Clarified that permission checks for MCP-triggered workflows occur at runtime rather than trigger time [d48861a]
  • Defined 'Active' workflow runs to include runs parked on human steps and runs with step errors, both of which block new triggers until finished or aborted [d48861a]
  • Added requirement that users must be able to manage workflows to enable automated triggers, with role restrictions of Admin, Process Owner, or Developer [d48861a]
  • Documented the failure mode when MCP trigger is enabled with an outdated Forest Runtime version [d48861a]
  • Clarified scope and visibility rules for getWorkflowRun tool in MCP server [d48861a]
  • Updated access revocation behavior documentation to clarify immediate blocking of new triggers and assistant notification [d48861a]
  • Clarified identity and auditing documentation for MCP tools, listing exactly four non-audited tools and explaining requestActionFileUpload behavior [d48861a]
📊 Macroscope summarized cef6f8d. 3 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

…RD-742)

Document the MCP channel added by PRD-49, written from the implementation
in the three feature branches rather than from the spec.

MCP server page: the three workflow tools (listWorkflows, triggerWorkflow,
getWorkflowRun) with their arg/return contracts, the discover -> trigger ->
poll flow, and the v1 limitations — no dedicated rate limiter, human-gated
runs are report-only.

Triggers page: goes from two triggers to three, states that gating is
channel-aware, and documents two prerequisites that were missing for the
webhook trigger too (Forest Runtime installed, fallback inbox selected).
The section is named Triggers, not Process. Screenshot refreshed with the
MCP row.

Deviations from the spec, followed as implemented: listWorkflows returns no
description; getWorkflowRun only exposes MCP-triggered runs; result is the
terminal step, not a business payload.

Also fixes an unescaped pipe that broke a table in the webhook reference.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mintlify

mintlify Bot commented Aug 3, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
forest 🟢 Ready View Preview Aug 3, 2026, 1:57 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@linear-code

linear-code Bot commented Aug 3, 2026

Copy link
Copy Markdown

PRD-742

Comment thread product/embed/mcp-server.mdx Outdated

@PMerlet PMerlet left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial review with cross-checking against the two implementation branches (forestadmin-server 6b9c421, agent-nodejs a4d3949). A lot verified exact: tool names and gating, listWorkflows shape (no description), the runState table is exhaustive vs the server enum, the error-table semantics (404 indistinguishable on purpose, 409 leaves the in-flight run untouched), the audit label, the three revocation levers, all links/anchors/images, and the pipe fix. One blocker — the getWorkflowRun section documents a contract the system does not produce — plus four quick text fixes. Details inline.

Comment thread product/embed/mcp-server.mdx Outdated

### `getWorkflowRun`

Reads the normalized status of a run, given the `runId` returned by `triggerWorkflow`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker — this section documents a contract the system does not produce.

On forestadmin-server#8418 (head 6b9c421) the normalized status mapper was removed after review: GET /mcp-workflows/runs/:runId deliberately returns the full hydrated run (service jsdoc: "the whole run is exposed to give the MCP client maximum context"), and git grep waitingForHumanInput finds nothing server-side. agent-nodejs#1792 only casts the response type — no runtime mapping — so the assistant will actually receive { runState, triggerType, workflowId, collectionId, workflowHistory: [...] }.

The field table, both JSON examples, and every waitingForHumanInput mention (here, l.392, and triggers.mdx) describe fields that will never appear. Hold this section until the contract is settled on the code side (mapper re-landed vs hydrated run embraced), then document the real shape.

Also, even against the normalized types: currentStep is { name, type } — no taskType — and result/error are optional/absent, not null as in the examples.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 17b6774 — the page now documents the hydrated-run contract as shipped: the field table matches HydratedWorkflowRun (the runState enum is exhaustive vs the server model), waitingForHumanInput is gone everywhere, and human-gating is described via workflowHistory (a step with done: false and executionType: manual). Cross-checked against both implementation heads.

Comment thread product/embed/mcp-server.mdx Outdated
| `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": "loading" }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runId is a string in what the tool returns: the orchestrator responds with a number, but forestadmin-client normalizes it (runId: String(result.runId)) and the getWorkflowRun tool requires runId: z.string(). The example should read { "runId": "1234", "runState": "loading" }.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 17b6774 — the trigger example now returns { "runId": "1234", "runState": "loading" }, and the getWorkflowRun section explains the asymmetry (id is the numeric form of the runId string).

Comment thread product/embed/mcp-server.mdx Outdated
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 `getWorkflowRun`'s `error`, not as a trigger-time failure.
</Note>

Only **one run per record** can be active at a time. Triggering a record that already has an ongoing run fails and does **not** resume it — the run in flight is left untouched.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The real constraint is one active run per (workflow, record), not per record: the active-run guard and the partial unique index are on (workflowId, collectionId, selectedRecordId). Two different workflows can run on the same record simultaneously. Same fix needed in the error table (l.401).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 17b6774 — now "Only one run of a given workflow can be active on a given record… A different workflow can still start on that record", and the error table says "A run of the same workflow".

Comment thread product/embed/mcp-server.mdx Outdated

### `listWorkflows`

Lists workflows with the MCP trigger enabled, scoped to the connected user's rendering and permissions.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"and permissions" overstates it: listMcpEnabledWorkflows filters by renderingId + presence of the mcp trigger only — no role/user-level filtering (the only upstream check is project membership). "scoped to the connected user's rendering" is accurate on its own; triggers.mdx's "can reach in the rendering" is the right phrasing. Same nuance at l.323 ("MCP-enabled for that user" → "in that rendering") and in the error table l.402.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 17b6774 — "and permissions" dropped; the page now says "scoped to the connected user's rendering" and the error table "outside the user's rendering".

Comment thread product/embed/mcp-server.mdx Outdated
| The workflow's MCP trigger is off, is unknown, or the user cannot reach it | Not found — with a hint to call `listWorkflows` |
| A run is already ongoing on that record | Conflict; no run is started and none is resumed |
| The `runId` is unknown, belongs to another user's rendering, or was not started through MCP | Not found |
| The project has no [Forest Runtime](/product/process/workflows/forest-runtime) installed | Conflict — automated triggering needs server-side execution |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 409 is keyed on the workflow engine, not on Runtime presence — and per environment, not per project: ensureWorkflowEngineOrchestrator returns Conflict when the environment's workflowEngine === 'browser'. An orchestrator-engine environment with no executor deployed accepts the trigger (202) and the run just stays pending. Suggest: "The environment's workflows still run in the browser engine → Conflict".

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 17b6774 — now "The environment's workflows still run in the browser engine → Conflict", matching ensureWorkflowEngineOrchestrator.

Comment thread product/embed/mcp-server.mdx Outdated
Three tools let an assistant drive a [Workflow](/product/process/workflows/overview) end-to-end from its own context: pick a workflow that fits the record at hand, start it, then watch the run.

<Info>
A workflow is only reachable through MCP once an editor enables its **MCP** trigger in the workflow's [trigger settings](/product/process/workflows/triggers#the-mcp-trigger). Nothing is exposed by default.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: "an editor" risks confusion with the Editor permission level — "a workflow editor" or "anyone with access to the workflow's trigger settings" avoids implying a role requirement. Same at l.442.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 17b6774 for the intro (now "a workflow editor") and completed in 4361f16 for the Security bullet that still said "an editor".

Comment thread product/embed/mcp-server.mdx Outdated

### Runs that need a human

In this first version the assistant can *observe* a parked run but not answer it. When `waitingForHumanInput` is `true`, the run is waiting in the workflow's **fallback inbox**, and someone finishes it from the Forest UI. Relaying the step's question into the chat and submitting the answer through MCP is planned, not available yet.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: "is planned, not available yet" is a roadmap commitment in product docs; safer as "is not available in this version".

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 17b6774 — now "is not available in this version."

…ract (PRD-742)

The orchestrator endpoint now returns the whole HydratedWorkflowRun instead of
the dropped normalized status. Replace the currentStep/waitingForHumanInput/
result field table and JSON examples with runState + triggerType + the
workflowHistory shape (each step's resolved definition and per-step context),
and reword the poll step, the trigger-time error note and the human-gated
section accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@PMerlet PMerlet left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial review against the two implementation branches (orchestrator #8418, mcp-server #1792). The prose is clear, but a few technical claims and JSON examples don't match the code as shipped. One is a contradiction with the server (fallback inbox), a couple are factual errors an integrator would code against. Details inline.

Comment thread product/embed/mcp-server.mdx Outdated

### Runs that need a human

In this first version the assistant can *observe* a parked run but not answer it. When the run sits on a human step — an incomplete step (`done: false`) whose definition marks it manual — it is waiting in the workflow's **fallback inbox**, and someone finishes it from the Forest UI. Relaying the step's question into the chat and submitting the answer through MCP is planned, not available yet.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This contradicts the server — MCP runs are NOT routed to the fallback inbox.

On the orchestrator branch (#8418), tryFallbackToInbox early-returns for any triggerType !== Webhook, so a run started via MCP that parks on a human step stays in started with no inbox assignment — it never lands in the fallback inbox. The same claim appears in triggers.mdx and execute/workflows.mdx.

Either the server needs to route MCP runs to the fallback inbox too (I've raised this on #8418 as the likely fix), or these three pages need to be corrected. As written, the docs promise behavior the code doesn't implement, and a parked MCP run is effectively unactionable. Please align with the #8418 outcome before merging.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved on the server side rather than here: 945eae136 on #8418 widened the tryFallbackToInbox gate to both automated triggers (Webhook and Mcp), with a mirrored test — so this claim now matches the shipped behavior, in all three pages. (Remaining nuance, deliberately left open: the sentence is unconditional while the assignment only happens when a fallback inbox is configured — candidate wording "when a fallback inbox is configured".)

Comment thread product/embed/mcp-server.mdx Outdated
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.
</Note>

Only **one run per record** can be active at a time. Triggering a record that already has an ongoing run fails and does **not** resume it — the run in flight is left untouched.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inaccurate, and contradicts another page. Uniqueness is per (workflow, record), not per record — findActiveRunForWorkflowAndRecord filters on workflowId + collectionId + selectedRecordId. Two different workflows can each have an active run on the same record. The API reference page states it correctly: "Only one run of a given workflow can be active on a given record" (trigger-workflow-webhook.mdx). Suggest matching that wording.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 17b6774 — aligned with the webhook reference wording: "Only one run of a given workflow can be active on a given record… A different workflow can still start on that record".

Comment thread product/embed/mcp-server.mdx Outdated
| `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": "loading" }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runId is a string in the actual response, not a number. The forestadmin-client HTTP layer normalizes it explicitly: return { ...result, runId: String(result.runId) } (forest-http-api.ts), and triggerWorkflow serializes that. So the assistant sees { "runId": "1234", "runState": "loading" }. (Note the real asymmetry: getWorkflowRun returns "id": 1234 as a number — so the example at L362 is right, but this one should be "1234".)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 17b6774"runId": "1234" in the trigger example; the numeric "id": 1234 in the getWorkflowRun examples stays as-is per your note.


```json Parked on a human step
{
"id": 1234,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These JSON examples drop required fields with no ellipsis, so an integrator coding a strict parser against them will break. getWorkflowRun returns the whole hydrated run with no serializer (response: run); the HydratedWorkflowRun type carries userId, renderingId, collectionId, bpmnVersion, engine, lockedAt, createdAt, updatedAt (all omitted here), and every step always has isCardStep (set unconditionally by hydrateStep) plus automaticCompletion, none of which appear. The prose at L356 even references collectionId and the timestamps the examples don't show. Either add an ellipsis/"trimmed for brevity" note or include the required fields.

Separately, at L404 the Finished example shows a guideline task step with "outgoing": [] — a task step always has exactly one transition (WorkflowTaskBase.outgoing: [WorkflowTransition]); only end steps have an empty outgoing.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 17b6774 — the run-level fields are now enumerated with a "trimmed to the load-bearing fields" note, and both task-step examples show exactly one outgoing entry (the empty array remains only on the End step, matching WorkflowEnd.outgoing: []). The end step's executionType was additionally corrected to manual in 4361f16.

…tract (PRD-742)

- runId is a string; one active run per (workflow, record); Conflict is
  keyed on the browser engine, not Runtime presence
- getWorkflowRun: document executionType/stepName/stepIndex, the trimmed
  examples note, the id/runId asymmetry, and no dedicated failed state
- fix the finished example (static stepDefinition keeps prompt/outgoing)
- enabledTools levers rewritten with allowlist semantics + third
  revocation lever; rate-limiting rationale corrected (no anonymous
  surface claim)
- new from the impl fixes: polling cadence, segments not enforced at
  trigger time, 4xx row for out-of-bounds ids
- onboarding page: workflows bullet in the MCP capability list

Fallback-inbox claims stay as written: forestadmin-server 945eae136 now
routes parked MCP runs to the fallback inbox.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@christophebrun-forest

Copy link
Copy Markdown
Member Author

Two one-line corrections landed in 4361f16 after a final cross-check against the implementation heads:

  • The End step in the Finished getWorkflowRun example now shows "executionType": "manual" — End steps are pinned to manual server-side (WorkflowEnd.executionType), so fully-automated was an impossible value there.
  • The Security bullet now reads "a workflow editor", matching the fix already applied earlier on the page.

Deliberately left open (non-blocking wording-softening candidates): the unconditional fallback-inbox phrasing (mcp-server.mdx:432, execute/workflows.mdx:34 — could be qualified with "when a fallback inbox is configured"), "stops runs already in flight" (mcp-server.mdx:451, triggers.mdx:122 — revocation doesn't abort in-flight runs; their next data steps fail on permissions), and the webhook reference's "single-run-per-record" shorthand (trigger-workflow-webhook.mdx:123/129).

@PMerlet PMerlet left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial review — docs (PRD-742)

Checked every claim in this PR against the two implementation branches (ForestAdmin/forestadmin-server#8418, ForestAdmin/agent-nodejs#1792).

This is the strongest of the three PRs. The Deviations section, the <Note> on the unvalidated record, the "no dedicated rate limiter" statement, the runState table (matches the server enum exactly, and correctly says there is no failed state), the one-run-per-workflow-per-record rule, the 400/404/409 mapping — all verified accurate against the code. It reads like it was written from the implementation, as claimed.

Four claims are stronger than what the code enforces, and one error case is missing. All inline. The permissions one (triggers.mdx) is the one I would not ship as-is: it promises a guarantee the server does not implement.

Note this PR is already approved, so these are post-approval findings.

- 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This bullet lands directly above "Every operation runs with the permissions of the authenticated Forest user and is logged just like a UI action" — and for the two read-only workflow tools, the second half is not true.

listWorkflows and getWorkflowRun write no activity log (unlike list, listRelated, describeCollection, which all do). Only triggerWorkflow is audited. So of the three tools this bullet introduces, one is logged and two are not.

Either the tools gain audit logging (raised on ForestAdmin/agent-nodejs#1792) or this page needs a qualifier — "every state-changing operation" would already be accurate.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ec3b24d with your qualifier — the sentence now reads "every state-changing operation is logged just like a UI action", which is exactly what the code enforces. Auditing the two read tools needs server-side support (the MCP activity-log route silently drops resource-less logs), tracked in PRD-967; when that lands this sentence can be strengthened back.

Comment thread product/embed/mcp-server.mdx Outdated
G -. "not finished yet" .-> G
```

1. **Discover** — `listWorkflows` returns the MCP-enabled workflows the connected user can reach, with the collection each one operates on. Pass `collectionName` to narrow it to the collection of the record in context.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"the MCP-enabled workflows the connected user can reach" — the endpoint does not filter by anything the user can or cannot reach beyond the rendering itself.

listMcpEnabledWorkflows returns every workflow in the rendering whose mcp trigger is on. There is no collection-permission filter, and isVisible is deliberately ignored (so a workflow hidden from the UI still appears here). A user with no read access to customers sees the customers workflow — and can trigger it.

Suggest: "the MCP-enabled workflows in the connected user's rendering". That is exactly what the code does, and it keeps the sentence honest without weakening the page.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ec3b24d with your wording — "the MCP-enabled workflows in the connected user's rendering", here and in triggers.mdx's Enabling section ("only the MCP-enabled workflows in the connected user's rendering"). No reach/permission claim left on the listing.

Comment thread product/embed/mcp-server.mdx Outdated

### `getWorkflowRun`

Reads the full run, given the `runId` returned by `triggerWorkflow`. The orchestrator holds no customer record data — that lives in the executor — so the whole run is returned, giving the assistant maximum context about where it is and what each step does.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"The orchestrator holds no customer record data" is a slightly stronger guarantee than the payload gives, and it is doing load-bearing work here — it is the stated reason the whole run is returned to a third-party LLM.

Two counter-examples in the very examples below: selectedRecordId is a customer record identifier, and context.error is free-form text reported by the executor — a failing get-data/update-data step can embed values from the customer's database in it. The Runs that need a human section and the runState table both tell readers to read context.error precisely because it carries execution detail.

Suggest: "holds no record payload — only identifiers and the executor's step outcomes", so a reader assessing what leaves their perimeter is not misled.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in ec3b24d — now "The run carries no record payload — records live in the executor — … Identifiers (selectedRecordId) and step error messages (context.error) may still contain customer data." The same hedge landed on the server jsdoc (forestadmin-server 50f2b89c2) and the client docstring (agent-nodejs 1ce50dce), so the three copies agree.

Comment thread product/embed/mcp-server.mdx Outdated

| 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` |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One error case is missing from this table: the tool also rejects a workflow whose collection is unavailable (renamed or deleted, so collectionName resolves to null) — before any run and before any audit log, with a distinct message pointing at the workflow's configuration.

Worth a row, because the symptom is confusing: the workflow appears in listWorkflows (with collectionName: null) yet cannot be triggered.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in ec3b24d — new row right after the unknown/disabled one: "The workflow's collection is unavailable (renamed or deleted — it may still appear in listWorkflows with a null collectionName) → Rejected up front, pointing at the workflow's configuration; nothing starts." The listing symptom you called out is in the row so the confusing pairing is explained where readers will hit it.

Comment thread product/embed/mcp-server.mdx Outdated
### Identity, auditing, and limits

- **Identity** — the run executes as the Forest user of the MCP session, established by the OAuth login. Its permissions bound everything the run can read, write, or trigger.
- **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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"labelled via MCP" — only one of the two rows written per trigger carries that label.

A single MCP trigger produces two Activity Log entries: the MCP server writes one before calling the orchestrator (fail-closed: the audit lands before any side effect), labelled triggered the workflow "X" with no channel; the orchestrator then writes its own, labelled triggered the workflow "X" via MCP and attached to the run. Webhook has no such duplicate.

So a reader auditing Activity Logs sees each MCP trigger twice, and the first row looks like a manual start. I have asked both code PRs to align the labels; if that lands, this sentence becomes true and the duplication is still worth a line here (the two rows are complementary — the early one has no runId).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The label fix landed (agent-nodejs ecb3125f: the MCP-server row now also says via MCP), so the sentence is true as written — and ec3b24d adds the line you suggested: "An MCP trigger writes two complementary log entries: one before the run starts (the audit that blocks the trigger if it cannot be written), and one attached to the run." Mirrored in triggers.mdx's Auditing section.

Comment thread product/process/workflows/triggers.mdx Outdated
### 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 — data and actions are gated by [Roles & permissions](/get-started/control/roles-permissions) exactly as in the interface.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the claim I would not ship as written. "It cannot exceed the connected user's permissions — data and actions are gated by Roles & permissions exactly as in the interface."

The trigger path enforces two things and no more: the caller has access to the rendering, and the workflow's mcp toggle is on. There is no collection-permission check and no scoping of the recordId — the server's own test suite pins this (should start a segment-restricted workflow on an out-of-segment record (segments not enforced)).

"Exactly as in the interface" is what makes it misleading: in the interface a user can only start a workflow from a record they can already see, so the visible surface is the permission boundary. Over MCP the assistant supplies the id, so that boundary disappears — and the assistant may be choosing that id from record content it just read (prompt injection).

The <Note> on mcp-server.mdx is much better calibrated about the record not being checked; this bullet contradicts it. Suggest aligning the two: what an assistant cannot do is exceed the user's permissions on the data the run reads and writes — while the trigger itself is bounded only by the rendering and the toggle.

Related: "permission scopes still bound everything the run reads and writes" on mcp-server.mdx rests on the executor minting a per-user token and the agent applying scopes on a by-primary-key read. Worth confirming that end of the chain before publishing it as a guarantee — a get-by-id that skips scope filtering would make this a scope bypass, and it is the same class of bug as PRD-898/916/922.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reworded in ec3b24d along the line you proposed — the bullet now scopes the guarantee to execution and states the trigger boundary explicitly: "It cannot exceed the connected user's permissions on the data the run reads and writes — every data step is gated by 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)." No "exactly as in the interface" left, and it now agrees with the <Note> on mcp-server.mdx instead of contradicting it.

On the related ask — the scope-enforcement chain was traced end-to-end during the #8418 segments thread: the executor calls the agent per data step with a JWT minted from the triggering user, and the agent applies getScope() on the by-primary-key get/update/action routes too, so "permission scopes still bound everything the run reads and writes" holds. The trigger-time gap itself (collection permission + unchecked record id, incl. the prompt-injection angle) is now consolidated with segments in PRD-919 for one decision.

Comment thread product/process/workflows/triggers.mdx Outdated
## 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same point as the Auditing bullet on mcp-server.mdx: "labelled by channel — via webhook or via MCP" holds for the webhook (one row, labelled) but an MCP trigger writes two rows and only the orchestrator's carries the channel. Worth waiting for the label fix on ForestAdmin/agent-nodejs#1792 before this sentence is accurate.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aligned in ec3b24d now that the label fix landed on agent-nodejs (ecb3125f): the channel claim is accurate, and the Auditing paragraph gains the same two-complementary-entries sentence as mcp-server.mdx so an auditor expects two via MCP rows per trigger.

Comment thread product/process/workflows/triggers.mdx Outdated
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 two prerequisites. Until both are met, their toggles stay locked, with a tooltip explaining which one is missing.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flagging as unverifiable rather than wrong: this whole section (locked toggles, the tooltip wording "Ask your tech team to install the Workflow Executor", the Automated group layout, and the "stays actionable once enabled" behavior below) describes front-end behavior that comes from ForestAdmin/forestadmin#9870, not from either back-end branch — so I could not confirm any of it from the code this PR cites.

Since the PR is already approved, worth a second pass against the front branch before merge — tooltip strings in particular tend to drift late, and the "re-locks once disabled" rule is precise enough to be wrong.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second pass done against the front branch (ForestAdmin/forestadmin#9870, current head) — all four claims are confirmed in code:

  • Layoutupdate/template.hbs: "Manual" row with its own toggle, then <h3>Automated</h3> holding FallbackInbox, WebhookTrigger, McpTrigger in that order; labels match the page word-for-word.
  • Lock conditions — both webhook-trigger and mcp-trigger components compute isLocked from the same two prerequisites and bind @disabled; the tooltip names the missing one.
  • Tooltip wording'Ask your tech team to install the Workflow Executor.' is byte-identical (hardcoded, no i18n) in both components, trailing period included.
  • Stays actionable / re-locks — the inbox branch is guarded by && !hasMcpTrigger (webhook: && !hasWebhookTrigger), so an enabled trigger stays toggleable with a cleared inbox and re-locks once off; pinned by component-test.ts ("no fallback inbox is set and the mcp trigger is enabled" → toggle enabled, no tooltip).

One nuance for the record: the executor prerequisite is gated on the environment's engine (workflowEngine === 'orchestrator'), not on a reported executor version — functionally what the page describes, so no text change needed.

…nforces (PRD-742)

Post-approval review findings: the listing is rendering-scoped (no
permission filter), the trigger is bounded by rendering + toggle only,
runs carry no record payload but ids/error strings may hold customer
data, an MCP trigger writes two via-MCP audit entries, revocation does
not abort in-flight runs, fallback-inbox routing needs one configured,
and the unavailable-collection rejection joins the error table.
Read-tool auditing is deferred to PRD-967 ("every state-changing
operation is logged").

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…claim (PRD-742)

- A parked run covers automated-with-confirmation steps too, not only manual ones
- List the always-present isCardStep / automaticCompletion fields the examples omit
- Make the triggers-page fallback-inbox claim conditional, matching the other pages

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread product/embed/mcp-server.mdx Outdated

@PMerlet PMerlet left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial pass, read against the two implementation branches rather than against the spec. The pages are accurate on the hard parts (segments not enforced, the record not validated at trigger time, the two audit rows) — these five are where the text promises more than the code does, or less.

Comment thread get-started/expose-to-ai-agents.mdx Outdated
- 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 runs with the permissions of the authenticated Forest user, and every state-changing operation is logged just like a UI action.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important — this sentence was weakened globally to fit a local gap.

"Every operation […] is logged""every state-changing operation is logged".

The change is needed only because the two new read tools (listWorkflows, getWorkflowRun) skip withActivityLog, unlike every other read tool on the server — list, describeCollection and listRelatedData all write a type: 'read' entry. So the new wording now under-describes the pre-existing behaviour: it tells readers their reads aren't logged, when most of them are.

It also quietly drops the audit guarantee on the one tool whose payload we document as potentially carrying customer data (getWorkflowRunselectedRecordId, context.error).

I've raised the code side on agent-nodejs#1792. If the tools get wrapped, this line should go back to the original sentence. If they deliberately stay unaudited, the honest form isn't to weaken the global claim — it's to keep it and carve out the exception where it applies, in mcp-server.mdx under Identity, auditing, and limits.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed on the conclusion, and fixed in 19fe126 — but the premise needs a correction that changes the fix.

These aren't the only unaudited reads. getActionForm doesn't import withActivityLog either, and it predates this epic. So "Every operation […] is logged" was already inaccurate before PRD-49 — the weakening wasn't introducing a falsehood, it was replacing one imprecision with another, and under-describing three-quarters of the reads in the process, exactly as you said.

And my own justification on the code side was wrong, which I've corrected on agent-nodejs#1792: the audit route doesn't silently drop a resource-less log — collectionModelName is Joi.string().required() → 400, and an unresolvable modelName → 404. The conclusion holds (no persisted row, an extra round trip per poll) but the mechanism is a rejection — which was precisely the uncovered path in with-activity-log.ts you flagged in the other thread. Fixed there in e97b498.

So I took your preferred shape: the global sentence is restored here, and the exception is carved out where it applies, naming all three tools plus the fail policy in one line under Identity, auditing, and limits:

Three reads are not audited — listWorkflows, getWorkflowRun and getActionForm leave no Activity Logs entry, because the log route needs a resource these tools have no way to name. Every other tool, read or write, writes one. A write whose log cannot be created is blocked; a read proceeds with a warning, so an audit-store outage never takes the read surface down.

Two other global claims on mcp-server.mdx said the same thing and would have contradicted the carve-out — the intro (:19, "logging every activity") and the security bullet list ("Logs all operations") — both now point at the same section. PRD-967 is updated to cover the third tool and to record that restoring the unqualified sentence is part of closing it.

Comment thread product/process/workflows/triggers.mdx Outdated

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 visible to assistants at all:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important — this reads as parity with UI visibility, and there is none.

"Enabling MCP triggering is what makes the workflow visible to assistants at all" — true as a necessary condition, misleading as a sufficient description. The store query deliberately does not filter isVisible (layout-workflows-store.ts, the wf CTE carries a comment saying so), so a workflow hidden in the interface stays fully listable and triggerable through MCP.

That matters because hiding a workflow is the gesture people use to retire one. Flip isVisible off, it disappears from every list view, and assistants keep starting runs on it — the retirement that works everywhere else silently doesn't here.

The page already does the honest thing for segments ("Workflow segments are not enforced either" in mcp-server.mdx); this deserves the same treatment. A line under What an assistant can and cannot do would cover it: hiding a workflow does not withdraw it from MCP — disable the MCP toggle.

Flagged the code side on forestadmin-server#8418 too, since the behaviour was left pending an explicit product sign-off that I can't find recorded anywhere in the three PRs.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 19fe126, with your wording as the basis.

The sentence now says enabling MCP is what makes the workflow reachable and is the only thing that does, followed by an explicit warning where a reader will hit it:

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.

I verified the code claim rather than taking it from the CTE comment, and there's a nuance worth carrying: the server filters isVisible nowhere — not in the listing, not in the by-id lookup, not on the manual /start. So a hidden workflow was already triggerable by API before this epic; what MCP adds is a discovery surface that hands it to an assistant. That's what I've put to product on the server PR, since it's a narrower and more answerable question than the one the thread implies.

Your point about the segments sentence being the right precedent is what shaped the fix — I checked that one too and it's accurate: segmentIds is read nowhere in the orchestrator service.

The sign-off is still outstanding and I'm not treating the docs change as a substitute for it. If product decides hidden = unreachable, the CTE filter is one line and this warning comes back out.

Comment thread product/process/workflows/triggers.mdx Outdated
|---|---|
| **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 or the workflow halts its runs — runs in flight are not aborted, but their next data step fails once the permissions are gone. |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This row makes a behavioural guarantee that nothing in the three branches demonstrates.

"runs in flight are not aborted, but their next data step fails once the permissions are gone"

It's presented as one of three revocation levers, in a security table — so a reader will act on it as the emergency stop for a run already moving. But it's an assertion about how the executor re-evaluates permissions mid-run, and no test in the server, MCP or executor branches covers it.

The other two rows are verifiable from this epic's code (the toggle gates the store query; enabledTools gates registration). This one isn't. Either confirm it against the executor and say so, or soften it to what we know — revoking access stops new triggers, and a run in flight is not aborted — so nobody plans an incident response around the second half of the sentence.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that nothing demonstrated it, and softened in 19fe126 — but there is a mechanism, just not the one the row described, so the new wording says what the code actually does.

What I found tracing it:

  • The user profile is re-resolved on every hand-off to the executor (workflow-history-hydrater.ts:134-174). If the user is no longer in the project, the run doesn't "fail its next data step" — it gets WorkflowUserNotFoundInternalError, the error is written onto the current step, and the run goes back to started. It stalls, which is a different observable outcome from the one an incident responder would plan around.
  • If the user keeps project access but loses collection permissions, the check does fall on the agent side — with a 15-minute permission cache by default (build-application-services.ts:48). So "once the permissions are gone" was missing the delay that matters most in an incident.
  • getPendingRuns performs no access check at all, and hasRunAccess is only wired to an executor HTTP route, never to step execution.
  • And grep -rn "permissions are gone\|revok" packages/private-api/test/jest/domain/workflow-orchestrator/ → nothing. Confirmed untested, in all three repos.

The row now reads:

Removing that user's access to the rendering stops new triggers immediately. 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 — allow up to 15 minutes for the agent's permission cache to expire. Abort a run explicitly if you need it stopped now.

I also dropped "or the workflow" from the same row: there is no per-workflow user access to revoke — permissions-by-action.ts:88 only defines manageWorkflow, which is the editing right, and the trigger path checks getRendering only. Same fix applied to the mirrored sentence in mcp-server.mdx.

Comment thread product/embed/mcp-server.mdx Outdated

- **Identity** — the run executes as the Forest user of the MCP session, established by the OAuth login. Its permissions bound everything the run can read, write, or trigger.
- **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 log entries: one before the run starts (the audit that blocks the trigger if it cannot be written), and one attached to the run.
- **Rate limiting** — the workflow tools have no dedicated limiter. They inherit the MCP server's authentication and limits; 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 also absorbs repeated triggers on the same record.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important — this bullet reassures about the wrong risk.

"The one-run-per-workflow-per-record rule also absorbs repeated triggers on the same record."

True, and beside the point. The rule bounds repetition on one record; it bounds nothing across records. An assistant looping over a list view can open one run per record, and each run consumes executor capacity — there's no limiter anywhere on the path (the bullet says so itself, one sentence earlier).

So the paragraph names the absence of a limiter and then offers a mitigation that doesn't cover the actual fan-out vector. triggerWorkflow is the only write tool we ship with destructiveHint: true and no volume guard; a reader deciding whether to enable it deserves that stated plainly rather than softened.

Suggest keeping the first two sentences, and replacing the third with what's actually true: the one-run-per-record rule prevents duplicate runs on the same record, but nothing bounds the number of records an assistant can trigger on — size the exposure with the mcp toggle, per workflow.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 19fe126, essentially your suggested replacement.

You were right that the paragraph named the absence of a limiter and then offered a mitigation for a different risk. Verified before rewriting: the unique index is on (workflowId, collectionId, selectedRecordId) — per record, bounding nothing across records; the only rate limiter in the orchestrator routes is rateLimitPendingRun, mounted on the executor's /pending-run and not on the MCP start; and triggerWorkflow is indeed the only tool shipping destructiveHint: true.

The bullet now ends:

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 as destructive so MCP clients ask the user to confirm each call; keep that confirmation on, and size the exposure with the mcp toggle, per workflow.

I kept the destructive-annotation sentence because it's the only actual mitigation on that path, and it's worth a reader knowing it exists — but it's framed as a client-side confirmation prompt, not as a volume guard, so it can't be mistaken for one.

Adjacent and now bounded on the server side: the workflow listing had no LIMIT at all (fixed on #8418, capped at 200). Different axis from the fan-out you're describing, but the same "nothing bounds this" family.

Comment thread product/embed/mcp-server.mdx Outdated
]
```

An empty array means no workflow is MCP-enabled in that rendering — most often because nobody has turned the toggle on yet.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor — "An empty array means no workflow is MCP-enabled in that rendering" is only true when no filter was passed. With collectionName set, an empty array much more often means "none for this collection", and the suggested cause ("nobody has turned the toggle on yet") sends the reader debugging the wrong thing.

Worth one clause: with collectionName set, an empty array means no MCP-enabled workflow operates on that collection.

Same section, one more: the error table below ("An empty or out-of-bounds recordId […] rejected up front, nothing starts") is accurate for the empty case (zod catches it) but not for the >255-char one — that only fails at the server, after the pending audit row has been written. Adding .max(255) to the tool's zod schema would make the sentence true rather than needing a caveat; raised it on agent-nodejs#1792.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both fixed in 19fe126, and the second one is now true rather than caveated.

Empty array — reworded as you suggested, with the debugging misdirection removed:

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.

I added a second line there, because the listing changed under this review: workflows whose collection was renamed or removed are now filtered out. They were being listed while triggerWorkflow rejects them up front, so an assistant could list, pick, get rejected, and list again — the same loop shape as the rollback case, but reachable today. Fixed on agent-nodejs#1792 (9ec10b78).

The recordId row — rather than caveat it, .max(255) was added to the tool's zod schema (2be4504), so "rejected up front, nothing starts" is now accurate for the over-long case too. Worth noting what it buys: I checked the server, and Joi already capped recordId at 255, so a long id was a clean 400 before any INSERT — never a 500. The gain is avoiding the round trip and the pending audit row marked failed, which is exactly the gap between the sentence and reality that you pointed at.

While in that table: Macroscope found a related precision bug on the server, now fixed (6205c9e0f) — a numeric recordId past MAX_SAFE_INTEGER was rounded by JSON.parse before being persisted, starting the run on a different record. The MCP tool always sends strings so this path was never exposed, but it's the same row's territory if we ever document the numeric form.

christophebrun-forest and others added 2 commits August 18, 2026 16:01
… (PRD-742)

The finished example marked the terminal end step done: true, which the
code never does - startStepExecution creates every entry with done: false
and the End branch only sets runState. Combined with the parked rule
(done: false plus a manual executionType) and end steps being pinned to
manual server-side, an assistant following this page reported a finished
run as awaiting a human.

The rule now matches buildHumanInterventionFallback: runState started
with no context.error on the last entry, covering both an unanswered step
and one awaiting confirmation, and saying not to read executionType.
Also drops a selectedOption from a task step - it is only written for
condition steps, and carries the branch answer, not the button text.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… (PRD-742)

The lock on the automated toggles depends on the environment's workflow
engine, not on whether a runtime is installed: a missing runtime replaces
the whole settings page with a "Forest Runtime can't be reached" screen,
and the install-executor tooltip only appears on the legacy browser
engine. Both pages claimed the tooltip covered the missing runtime.

Adds the upgrade-before-enabling warning: a runtime older than the MCP
trigger release rejects those runs at validation, so the run is created,
never picked up, and nothing warns the operator.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s (PRD-742)

Reworked after verifying each claim against the implementation heads.

- The global "every operation is logged" sentence is restored rather than
  weakened: it had been narrowed to state-changing operations only
  because two new read tools skip the audit wrapper, which
  under-described the reads that do write one. The exception is now named
  where it applies, and it covers three tools - getActionForm was already
  unaudited before this epic.
- The Identity bullet no longer implies permissions bound triggering.
  They bound what the run reads and writes; the trigger is gated by the
  rendering and the MCP toggle alone.
- Hiding a workflow does not withdraw it from MCP. The listing is driven
  solely by the toggle, and hiding is how people retire a workflow.
- The one-run-per-record rule bounds repetition on one record, not the
  number of records an assistant can fan out over - nothing limits that.
- The revocation lever no longer promises that in-flight runs fail on
  their next data step: nothing in these branches demonstrates it, and
  the agent's permission cache holds for up to 15 minutes.
- An empty listing means something different once collectionName is set,
  and the locked-toggle tooltip is quoted as the frontend now ships it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…742)

The image had been replaced to show the whole Triggers section but kept
its webhook-only filename, so the next person editing the page would go
looking for a webhook screenshot.

Also adds the one trigger-time failure missing from the errors table: a
workflow whose MCP Task step targets an OAuth2-protected connector is
refused at start, and a reader had no way to know that from this page.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
christophebrun-forest and others added 2 commits August 19, 2026 15:06
…RD-742)

Checked against the three implementation heads.

The error table promised a split that never landed, and was wrong twice over:
"rejected up front" holds for an empty argument and an over-long recordId, but
an out-of-range runId is only a string on this side, so it round-trips and comes
back a server 400. The OAuth2 row said the step type is "not yet supported"
when the gate is version-based at 1.14.0 — which forest-runtime.mdx, in this
same PR, documents as a supported feature.

The unaudited-tool list counted three; there are four. The fourth,
requestActionFileUpload, is not a read: it mints a pre-authorized upload, and it
is on by default whenever file uploads are configured. An enumerated compliance
claim that omits the one write-ish tool is worse than a general statement. The
stated rationale also did not hold for getActionForm, which does receive a
collection.

Three more: the two Activity Log entries were presented as equally reliable
when the run-attached one is best-effort; the opt-in Note implied the toggle
governs reading a run already in flight; and the Security section still carried
flat guarantees the page contradicts elsewhere. Adds the 200-workflow cap and
its invisible truncation, and replaces a runState example that cannot occur at
trigger time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An executor predating MCP triggering does not leave the run pending forever. It
picks it up, classifies it as malformed and reports an error outcome, so the run
fails immediately with a validation error on its first step and getWorkflowRun
shows it — that reporting path predates the executor's MCP support, so every
affected runtime has it.

The prerequisite is unchanged, but the operator symptom is "every MCP trigger
fails loudly with a confusing error", not "runs pile up unnoticed", and the
rollback runbook should read that way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread product/embed/mcp-server.mdx Outdated
Resolved the Security bullet list in product/embed/mcp-server.mdx: kept this
branch's three corrected claims (four unaudited tools, permissions scoped to
what a run reads and writes rather than to the trigger, and the hedge that
identifiers and step error messages can still carry customer data) and kept
main's new bullet on restricting which AI clients may connect, which is new
information rather than a claim this branch corrected.

Both anchors referenced from the list exist on the merged page.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
christophebrun-forest and others added 2 commits August 19, 2026 19:06
…e code does (PRD-742)

Four claims the implementation contradicts:

- The Security summary still read "logs every write, and every read except four
  tools", contradicting the enumerated bullet on the same page. Ten tools are
  audited and four are not — getActionForm, requestActionFileUpload,
  listWorkflows and getWorkflowRun — so it is three reads out of six plus one
  non-read. Naming the four removes the read/write split that produced two wrong
  versions in a row.
- The entry-point page promised "every operation runs with the permissions of
  the authenticated Forest user" one line after adding workflow triggering to
  what that sentence covers. Triggering checks rendering membership and the MCP
  toggle only — no collection permission, no record, no segment. The diagram alt
  text repeated the audit half and is aligned too.
- getWorkflowRun was described as user-scoped ("another user's rendering"). The
  store filters on renderingId with no userId, so any MCP session on the
  rendering can read any MCP-started run in it — including its selectedRecordId
  and step error strings, which the same page flags as possibly sensitive.
- requestActionFileUpload was called historical: it landed on main two days ago.
  The "the log route needs a resource" rationale applies to it, not to
  getActionForm, which does receive a collection.

Also documents the non-UUID workflowId case now that it is reported as terminal
rather than retryable, notes that a parked or errored run keeps blocking new
triggers on its record, and drops the internal tracker id, the only one in the
repo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ew symptom (PRD-742)

The permission gate that landed on the front branch added a third prerequisite,
and the page still said two. Worse, it named "a workflow editor" as the person
who enables MCP triggering — Editor is precisely the level the interface now
excludes. The levels are named, and the fact that the gate is client-side only
is stated rather than implied, since the layout permission behind the PATCH is
broader today.

The runtime-skew warning said the run is "marked failed", while the sibling page
states there is no failed state. The run stays in `started` with an error on its
first step. Two consequences were missing and are the actual recovery cost: it
is routed to the fallback inbox, which fills with runs that never began, and
`started` counts as active, so each affected record is blocked from being
triggered again on that workflow until its run is aborted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants