# MCP tools for flows and flow nodes

Exact flow and node tool arguments, tree schemas, mutation safeguards, analytics responses, and draft-only behavior.

Available on the Grow, Scale, and Pro plans.

Use these tools to build the sequence of questions, conditions, forms, offers, and actions a visitor follows. Read `get-flow-summary` before editing branches. Use `get-flow` for metadata and node IDs, not to reconstruct the connections between nodes.

## MCP flow access and draft changes

All tools accept optional `project_id` (string). Omission uses the connection's active project or an accessible project the server can resolve. Pass the ID explicitly when working across projects.

The tools follow the [shared MCP authorization and project-selection rules](https://rightmessage.com/help/mcp-authorization-reference). Grow includes MCP access. There is no additional general flow-plan requirement. New or changed app-embed nodes also require a connection to the embed app in your project. A draft save does not bypass feature requirements applied at publication or execution.

Configuration reads prefer the draft, otherwise the published configuration. Mutations save a validated draft and a configuration snapshot. None publishes the draft or runs the flow. Normal website visitors use the published flow. Previewing a draft can still execute your Code nodes, so the debugger is not a sandbox for arbitrary JavaScript. `create-flow-from-description` also uses AI generation while drafting.

Shared failures include inaccessible projects, missing entitlement, malformed configuration, and invalid resource references. Errors are MCP errors, not successful resource objects. A `modify-flow` error can describe a **partially saved** batch. Its section explains recovery.

Examples are illustrative and were not executed. Request objects show tool-call names and arguments; results omit the MCP transport envelope. Short results are marked as excerpts.

## Flow and node schema

A saved flow has `id` (`ofn_…`), `name`, `funnelDefinition`, and optionally `isSystemFlow`. A write uses `definition` for the tree and `is_system_flow` for the system-flow flag. Do not pass the returned field names as top-level write arguments.

A definition is `{type: "root", child: [...]}`. `child` is an ordered array of nodes. Each node has `type` and an optional `_uuid` string; whole-definition creation/update assigns missing UUIDs. Node creation assigns the new node's UUID. Keep existing UUIDs when preserving a tree.

Node objects reject properties outside their type's schema. Branch child arrays recursively contain the same node types.

| Node `type` | Properties and defaults                                                                                                                                                                                                                                                     |
| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sg`        | `segmentGroupId`: dimension ID, question ID, or null in the stored schema. `ask` boolean defaults to true during creation. `split` defaults to false, or is an array of `{segmentId, child}`; both branch properties are required, and `segmentId` is a segment ID or null. |
| `yesno`     | `condition`: [logic object](https://rightmessage.com/help/mcp-tools-conditions), default `{}` during creation. `yes` and `no`: node arrays, default `[]`. Optional `note` string.                                                                                           |
| `offer`     | `offerId`: offer/form ID or null. Optional `desiredType`: `form` or `offer`. A placeholder can be stored without a selected item, but cannot show an offer.                                                                                                                 |
| `redirect`  | Required `url` string. `open` boolean defaults false; `sendSubscriberData` boolean defaults false.                                                                                                                                                                          |
| `data`      | `actions` array, default `[]`; action fields below.                                                                                                                                                                                                                         |
| `message`   | Required `content` string.                                                                                                                                                                                                                                                  |
| `code`      | Required `name` and `code` strings; optional `selector` string.                                                                                                                                                                                                             |
| `webhook`   | Required HTTPS `url`; `method`: `POST`, `PUT`, or `PATCH`, default `POST`. Optional `name` string. `customPayload` string defaults empty; `fireOnce` boolean defaults true; `timeoutMs` number defaults 10000. Optional `includeVisitorContext` boolean.                    |
| `embed`     | `app`, `content`, and `target`, described below. The stored schema also requires `_uuid`.                                                                                                                                                                                   |
| `highscore` | Required `split` array of `{score, child}`. `score` is a string or null; `child` is a node array. Optional `mode`: `scores` or `variables`; optional `inverse` boolean.                                                                                                     |
| `random`    | Required `split` array of `{child: [...]}` branch objects.                                                                                                                                                                                                                  |
| `exit`      | No additional properties.                                                                                                                                                                                                                                                   |

For a choice-question node, create the [segment group](https://rightmessage.com/help/mcp-tools-segments) and its linked [question](https://rightmessage.com/help/mcp-tools-questions) first. An `sg` with `ask: true` and no linked question can return `_warnings` on whole-flow saves. Do not treat that warning as a working question.

The stored schema permits input-question IDs (`raq_…`) in `segmentGroupId`, but `create-flow-node` currently prevalidates that property against dimension IDs only. Use a complete `definition` with `create-flow` or `update-flow` when inserting a standalone input-question reference; preserve the rest of the tree. Do not invent a dimension for an input question.

### Data actions

Each action has required `type` and `id` in the saved schema. Mutation normalization generates missing `act_…` IDs. Valid types are `score`, `field`, `variable`, `add_tag`, `remove_tag`, and `set_segment`. Actions allow additional properties; the useful runtime fields are:

- `score`: `score` name and numeric `adjust` amount.
- `field`: `field` identifier and `value` to render into the connected platform's custom field.
- `variable`: string `variableName` and `variableValue`. Values support Liquid; an empty rendered value removes the variable. For timed expiration, set `expiresType: "duration"`, numeric `expiresDuration`, and `expiresUnit`: `minutes`, `hours`, `days`, or `weeks`. Otherwise the runtime stores no expiration timestamp. The open action schema does not declare numeric bounds for the duration.
- `add_tag` and `remove_tag`: string `tagId`.
- `set_segment`: string `segmentGroupId` and `segmentId`. Whole-definition normalization can infer the group from an existing segment; supply both for individual node creation.

These are stored instructions, not immediate contact writes. Once published, field/tag actions depend on the connected provider and visitor identity; assigning a segment updates the answer bank and can trigger configured synchronization.

The runtime currently deduplicates score adjustments by node ID. Multiple score actions in one Set Data node can therefore retain only the first adjustment. See the [data-action limits and Liquid reference](https://rightmessage.com/help/flow-data-liquid-reference).

### Embed nodes

The production schema supports `app: "senja"`. Content is null for an incomplete draft, or one of:

- Widget snapshot: `{kind: "widget", id: string, name: string, design: string}`. `id` must contain a nonwhitespace character.
- Wall-of-love snapshot: `{kind: "wall_of_love", slug: string, permalink: string, name: string}`. `slug` and `permalink` must contain nonwhitespace characters.

Targets are either `{kind: "element", selector: string, position: "inside"|"before"|"after", pages: logic}` or `{kind: "page", pages: logic}`. All listed target properties are required. Use `pages: {"and":[]}` for all pages. Extra snapshot/target properties are rejected. Do not put credentials in content.

Creation defaults an omitted embed app to `senja`, content to null, and target to `{kind: "element", selector: "", position: "after", pages: {and: []}}`. These defaults make an incomplete draft, not a ready-to-display embed. New or changed embeds check the registered app and the project's local connection without fetching upstream resources. Unknown or disconnected apps return an error. Unchanged saved embeds are not revalidated solely because a connection was removed.

### Structural and privacy constraints

A form is an `offer` node whose selected item's action is not `link`. Only `message` and `redirect` nodes may follow that form in the same branch. Branch before the form when visitors need different terminal content. Whole-flow create/update and individual node insertion reject this structural violation.

System flows run on page views without a widget. Use background nodes: `data`, `code`, `webhook`, `embed`, `yesno`, `highscore`, `redirect`, `random`, and `exit`. Questions, offers, and messages require a widget.

Set `sendSubscriberData: true` only for an absolute redirect URL on another domain running this same RightMessage project. Leave it false for same-site paths and ordinary links. A webhook URL must start with `https://`; other URLs return an error instead of creating an action that cannot fire.

## Node position object

`create-flow-node` and `move-flow-node` accept a position object:

| Key             | Type and behavior                                                                                                   |
| --------------- | ------------------------------------------------------------------------------------------------------------------- |
| `parent`        | Parent node UUID string. Omit for root placement, unless a sibling locator resolves a nested branch.                |
| `after`         | Sibling UUID string. Inserts after that sibling; without `parent`, its containing branch is found automatically.    |
| `before`        | Sibling UUID string supported by the positioning service. Inserts before that sibling.                              |
| `branch`        | For `yesno`, `yes` or `no`; for `sg`, a segment-ID string; for `random` and `highscore`, a zero-based branch index. |
| `insertAtStart` | Boolean. Inserts at the beginning of the resolved array.                                                            |

Within the target array, priority is `insertAtStart`, then `before`, then `after`, then append. Omitted position on node creation appends at root. An unknown parent or sibling, a parent without a child branch, or an invalid branch returns a position error. Use IDs from the current flow, not display names.

## `list-flows`

List compact flow summaries. The only argument is optional `project_id`; the tool is read-only.

Returns an array with `id`, `name`, `isSystemFlow`, `type` (`system` or `widget`), and `widgetCount`. When a tree exists, `stats` contains `questionCount`, `offerCount` (unique offers), and `conditionCount`. An empty configuration returns `[]`. Shared access errors apply.

Illustrative request and result:

```json
{"name":"list-flows","arguments":{}}
```

```json
[{"id":"ofn_workforge","name":"WorkForge intake","isSystemFlow":false,"type":"widget","stats":{"questionCount":1,"offerCount":1,"conditionCount":0},"widgetCount":1}]
```

## `get-flow`

Read flow metadata and a flat list of node IDs. Requires `flow_id` (string); accepts optional `project_id`. No configuration changes occur.

Returns saved flow metadata, removes `funnelDefinition`, and adds:

- `stats`: `questionCount`, `offerCount`, `uniqueOfferCount`, `conditionCount`, `messageCount`, `redirectCount`, `hasSegmentRouting`, and `conditionTypes`.
- `_nodeUuids`: array of `{_uuid, type, name}` for non-root nodes. It is not a connected tree.
- `linkedWidgets`: array of `{id, name, type, isEnabled}`.
- `_systemFlowNote` for a system flow, plus resolved names alongside known resource IDs.

Missing ID returns `flow_id is required.` An unknown flow returns a not-found error. Use `get-flow-summary` to understand routing.

Illustrative request and result excerpt:

```json
{"name":"get-flow","arguments":{"flow_id":"ofn_workforge"}}
```

```json
{"id":"ofn_workforge","name":"WorkForge intake","_nodeUuids":[{"_uuid":"11111111-1111-4111-8111-111111111111","type":"message","name":"Thanks for visiting WorkForge."}],"linkedWidgets":[]}
```

## `get-flow-summary`

Read a human-readable view of the flow's connections. Requires `flow_id` (string). Optional `days` is an integer with default 30; optional `project_id` uses the shared contract.

Returns **text containing YAML**, not JSON and not a lossless write definition. It contains a reading guide, nodes keyed by UUID, node-specific text/conditions/branches, and catalogs of available dimensions, offers, questions, and other flows. Question nodes use `type: question`; non-asking segment routers use `router`; yes/no nodes use `condition` in this presentation.

Each question/router's `trunk_next` shows where it continues when an answer branch does not override it. A branch's `next` can point to a UUID, return to the trunk, or be `(flow-end)`. Only `(flow-end)` means termination.

The hosted tool returns a compact summary. Despite the tool's general description mentioning metrics, this response omits analytics, email-platform sync fields, field values, and visitor counts. Changing `days` does not add those metrics. Use `get-flow-performance` for analytics. The summary uses the latest stored configuration, not necessarily the live version.

Missing ID returns `flow_id is required.` Missing configuration/flow or generation failures return `Failed to generate flow summary: …`. No mutation occurs.

Illustrative request and YAML result excerpt:

```json
{"name":"get-flow-summary","arguments":{"flow_id":"ofn_workforge","days":30}}
```

```yaml
# Flow: WorkForge intake
nodes:
  11111111-1111-4111-8111-111111111111:
    type: message
    text: "Thanks for visiting WorkForge."
```

## `create-flow`

Create a draft flow. Requires `name` (string). Optional `definition` is the complete tree above; omission creates an empty root with `child: []`. Optional `is_system_flow` is boolean, default false; optional `project_id` uses the shared contract.

Returns the saved flow with its generated ID, normalized tree, and `_warnings` array when a choice-question node lacks a linked question. It does not create widgets or the resources referenced by nodes. Missing name returns `name is required.` Invalid definitions, embed access, and post-form structure return errors.

Illustrative request and result:

```json
{"name":"create-flow","arguments":{"name":"WorkForge intake"}}
```

```json
{"id":"ofn_workforge","name":"WorkForge intake","funnelDefinition":{"type":"root","child":[],"_uuid":"00000000-0000-4000-8000-000000000000"}}
```

## `update-flow`

Change draft metadata or replace a whole tree. Requires `flow_id` (string) and at least one of `name` (string), `definition` (object), or `is_system_flow` (boolean). Optional `project_id` uses the shared contract.

Omitted fields remain unchanged. Supplying `definition` replaces the entire tree, not just the mentioned nodes. Returns the saved flow with normalized nodes and any `_warnings`. For a small edit, use the node tools instead.

Unknown flow returns `Flow not found: …`. An empty update returns `At least one of name or definition must be provided.` even though a system-flow flag alone is also accepted. Invalid configuration and post-form structural errors reject the update.

Illustrative request and result excerpt:

```json
{"name":"update-flow","arguments":{"flow_id":"ofn_workforge","name":"WorkForge team intake"}}
```

```json
{"id":"ofn_workforge","name":"WorkForge team intake","funnelDefinition":{"type":"root","child":[],"_uuid":"00000000-0000-4000-8000-000000000000"}}
```

## `create-flow-from-description`

Generate a flow and its supporting resources from plain text. Requires `description` (string). Optional `name` defaults to `New Flow`; optional `target_flow_id` identifies an existing flow; optional `project_id` uses the shared contract.

The server invokes AI generation, then creates or reuses segment groups and questions, creates offers and personalization phrases as needed, and saves the assembled draft. Existing dimensions can be reused by ID or matching name. This is not a deterministic JSON-tree builder.

**A supplied `target_flow_id` replaces that flow's definition, even if it already contains nodes.** The tool supplies the default name `New Flow` when `name` is omitted, which can rename the target. Read the target first and provide the intended name.

Returns text summarizing the flow ID/name, dimensions, questions, offers, personalization tokens when created, top-level flow-node count, and generated explanation when available. It is not the full saved object; read the flow afterward for its current structure.

Missing description returns an instruction to describe the flow. Unknown target, no configuration, generation failure, and save validation failures return errors. No publishing or visitor action occurs.

Illustrative request and text result:

```json
{"name":"create-flow-from-description","arguments":{"name":"WorkForge intake","description":"Ask which team the visitor works in: Operations or Customer Success. Show a link to https://example.com/demo after either answer."}}
```

```text
Created flow "WorkForge intake" (ofn_workforge) with:
- 1 dimensions: Team
- 1 questions
- 1 offers: WorkForge demo
- 2 flow nodes

Flow logic: Ask the visitor's team, then show the demo link.
```

## `duplicate-flow`

Copy a draft/current flow. Requires `flow_id` (string). Optional `new_name` defaults to the original name plus ` (Copy)`; optional `project_id` uses the shared contract.

Returns the copied flow with a new `ofn_` ID and fresh UUIDs for existing nodes throughout its child and branch arrays. It keeps references to the original questions, offers, and segment groups. It does not copy or relink widgets. Missing/unknown flow returns an error; copied configuration must still validate.

Illustrative request and result excerpt:

```json
{"name":"duplicate-flow","arguments":{"flow_id":"ofn_workforge","new_name":"WorkForge alternate intake"}}
```

```json
{"id":"ofn_alternate","name":"WorkForge alternate intake","funnelDefinition":{"type":"root","child":[],"_uuid":"22222222-2222-4222-8222-222222222222"}}
```

## `delete-flow`

Remove a flow from the draft. Requires `flow_id` (string); accepts optional `project_id`.

Returns `{deleted: true, flow_id}`. It also clears `contentId` on widgets that referenced the flow. Questions, forms, and offers remain separate resources. Published behavior changes only after publication.

A flow in an active A/B test cannot be deleted; end the test first. Missing ID or unknown flow returns an error. The test protection is explicit for deletion; do not assume every other edit has the same guard.

Illustrative request and result:

```json
{"name":"delete-flow","arguments":{"flow_id":"ofn_alternate"}}
```

```json
{"deleted":true,"flow_id":"ofn_alternate"}
```

## `create-flow-node`

Add one node to an existing draft flow. Requires `flow_id` (string) and `type` (one of the node types above). Optional `data` holds type-specific node properties; optional `position` uses the position contract; optional `project_id` uses the shared contract.

Returns the created node with `_uuid`, `type`, and normalized type-specific properties. A linear asking `sg` can include an informational `_note`. For branching through the hosted tools, use `modify-flow` with `enable_split`; internal underscore-style tool names in diagnostic text are not public tool names.

The tool checks referenced dimensions, offers, and segment conditions before saving. It also simulates insertion to reject forbidden nodes after a form. Missing required IDs/types, unknown type, bad position, missing references, or schema validation produces an error. It saves configuration only; it does not run the new node.

Illustrative request and result:

```json
{"name":"create-flow-node","arguments":{"flow_id":"ofn_workforge","type":"message","data":{"content":"Thanks for visiting WorkForge."}}}
```

```json
{"_uuid":"11111111-1111-4111-8111-111111111111","type":"message","content":"Thanks for visiting WorkForge."}
```

## `update-flow-node`

Edit one node's properties. Requires `flow_id` (string), `node_uuid` (string), and nonempty `data` (object); accepts optional `project_id`.

Use the type-specific fields from the schema above. Only supplied properties change. Each supplied object or array replaces that property; for example, replace an embed's complete `target`, a yes/no node's `condition`, or a data node's `actions` array.

`_uuid`, `type`, `child`, `split`, `yes`, and `no` are protected and ignored by this update path. It cannot change a node type or restructure branches. Use create/delete/move, `modify-flow` for enabling a split, or a deliberate whole-definition replacement.

Returns the saved node. Missing arguments return explicit required-field errors; unknown node/flow returns `Node not found: … in flow …`. Invalid type-specific properties, non-HTTPS webhooks, or disconnected embeds return errors.

Illustrative request and result:

```json
{"name":"update-flow-node","arguments":{"flow_id":"ofn_workforge","node_uuid":"11111111-1111-4111-8111-111111111111","data":{"content":"Your WorkForge team is ready."}}}
```

```json
{"_uuid":"11111111-1111-4111-8111-111111111111","type":"message","content":"Your WorkForge team is ready."}
```

## `move-flow-node`

Move a node, with its nested branches, within the same flow. Requires `flow_id` (string), `node_uuid` (string), and `position` (object). Optional `project_id` uses the shared contract.

Returns `{moved: true, node_uuid, flow_id}`. The node keeps its UUID; it is removed from its old array and inserted at the requested location. The handler rejects an empty position object, so specify a locator or `insertAtStart` explicitly rather than sending `{}`.

Unknown nodes/flows and invalid positions return errors. This saves a draft movement, not a visitor event.

Illustrative request and result:

```json
{"name":"move-flow-node","arguments":{"flow_id":"ofn_workforge","node_uuid":"11111111-1111-4111-8111-111111111111","position":{"insertAtStart":true}}}
```

```json
{"moved":true,"node_uuid":"11111111-1111-4111-8111-111111111111","flow_id":"ofn_workforge"}
```

## `delete-flow-node`

Delete a node and every nested child below it. Requires `flow_id` and `node_uuid` (strings); accepts optional `project_id`.

Returns `{deleted: true, node_uuid, flow_id}`. It does not delete separately stored questions, forms, or offers referenced by the removed nodes. The change is draft-only. Missing arguments or unknown flow/node returns an error.

Illustrative request and result:

```json
{"name":"delete-flow-node","arguments":{"flow_id":"ofn_workforge","node_uuid":"11111111-1111-4111-8111-111111111111"}}
```

```json
{"deleted":true,"node_uuid":"11111111-1111-4111-8111-111111111111","flow_id":"ofn_workforge"}
```

## `modify-flow`

Apply an ordered batch of simplified flow operations. Requires `flow_id` (string) and nonempty `operations` (array). A legacy JSON-encoded string is also accepted for `operations`; use the typed array for new clients. Optional `project_id` uses the shared contract.

Each operation requires `action`, one of:

| `action`       | Required and optional properties                                                                                                                                                                                                                                   |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `insert_node`  | Requires `type`. Use `after` UUID or `parent` UUID plus `branch` to position it; omission appends at root. `after_dimension` inserts after every question/router for a dimension when neither `after` nor `parent` is supplied. Node-specific fields follow below. |
| `delete_node`  | Requires `node_uuid`; removes descendants too.                                                                                                                                                                                                                     |
| `enable_split` | Requires `node_uuid` for a linear `sg`. Optional `segment_ids` string array selects branches; omission uses all segments in its dimension.                                                                                                                         |
| `update_node`  | Requires `node_uuid`; other properties become a node-property update. Use native stored keys for updates, such as `segmentGroupId` and `offerId`, rather than insertion aliases. Protected structure keys remain ignored.                                          |

Insertion fields are the node schema with these differences:

- `sg` uses `dimension` for the group ID, optional `ask` (default true), and starts with `split: false`. Set `enable_split: true` to add branches; optional `children` is an array of child insertion objects, each with its target segment ID in `branch`.
- `offer` uses `offer_id`, which must identify an existing form or offer.
- `yesno` accepts a full logic object or simplified `{dimension, operator, segments}`. `dimension` is a group-ID string; `segments` is an array of segment IDs; `operator` defaults to `include_any`, with underscores translated to spaces. Omission creates an empty placeholder condition. Optional `yes_path` and `no_path` arrays contain child insertion objects, without positioning fields.
- Other node types use their native properties: `url`, `open`, `sendSubscriberData`, `actions`, `content`, `name`, `code`, `selector`, `method`, `customPayload`, `fireOnce`, `timeoutMs`, `includeVisitorContext`, `app`, `target`, `split`, `mode`, and `inverse`, as applicable.

The declared operation schema types `after`, `parent`, `after_dimension`, `node_uuid`, `dimension`, `offer_id`, `type`, `url`, `name`, `code`, `selector`, `method`, `customPayload`, `app`, and `mode` as strings. `branch` is string or integer. `ask`, `enable_split`, `open`, `sendSubscriberData`, `fireOnce`, `includeVisitorContext`, and `inverse` are booleans. `condition` and `target` are objects; `content` is string, object, or null according to node type. `split` is boolean or an array of objects; `segment_ids` is a string array; `yes_path`, `no_path`, `children`, and `actions` are object arrays; `timeoutMs` is numeric. Node validation still restricts each field to its applicable node type.

Returns **text**, with a results heading and zero-based operation entries containing saved node UUIDs. Its current wording says `Created` even for the `updated`, `deleted`, and `enable_split` result types; read the operation type, not that verb alone.

Insertion validation can reject the batch before any operation runs. Later failures do not roll back earlier successful operations. A failed insertion with inline children or `after_dimension` can also have saved some nodes. The error lists failures and saved operations. Read `get-flow-summary` before retrying, then resend only work that did not save.

Other errors include invalid JSON, empty operations, unknown flow, missing per-operation fields, invalid references, and `enable_split` on an unsuitable node or an already enabled split.

Illustrative request and text result:

```json
{"name":"modify-flow","arguments":{"flow_id":"ofn_workforge","operations":[{"action":"insert_node","type":"message","content":"Welcome to WorkForge."}]}}
```

```text
## modify_flow results for ofn_workforge

- Operation #0: Created message node [33333333-3333-4333-8333-333333333333]
```

## `get-flow-performance`

Read recorded flow executions and node metrics. Requires `date_from` and `date_to` (strings: `YYYY-MM-DD` or ISO 8601 timestamps). Optional `flow_id` limits the result to one flow; omission returns project-wide metrics. Optional `timezone` defaults to `UTC`; optional `project_id` uses the shared contract.

Date-only boundaries use local midnight through the end of the selected final day in `timezone`. Explicit timestamp offsets are preserved. The metrics result is cached for five minutes. No configuration changes occur.

Returns:

| Field                             | Contract                                                                                                                                                                      |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `flowMetrics`                     | `totalExecutions`, `completedRuns`, `unfinishedRuns`, `uniqueVisitors`, `identifiedVisitors`, `avgDurationMs`, `completionBreakdown`, and `byFlow`.                           |
| `flowMetrics.completionBreakdown` | Map of recorded completion-status strings to run counts.                                                                                                                      |
| `flowMetrics.byFlow`              | Array of `{flowId, executions, uniqueVisitors, completionRate}`. Rate is a percentage, rounded to two decimal places.                                                         |
| `nodeMetrics`                     | Map keyed by node ID. Each entry has `nodeId`, `nodeType`, `visitorCount`, and `avgDurationMs`, with applicable `conversionCount`, `conversionRate`, and distributions below. |
| `exposurePages`                   | Array of `{url, pathname, hostname, uniqueVisitors, executions}`, ordered by unique visitors. These are observed pages, not configured targeting rules.                       |
| `pathFlows`                       | Present only for a single `flow_id`: `paths`, `dropOffs`, `runsAnalyzed`, and `analyzedFrom`.                                                                                 |
| `estimates`                       | Optional `{questions: true}` when question figures are estimated; an explanatory `note` accompanies it.                                                                       |

Node distributions are maps of identifiers to unique-visitor counts: `splitDistribution`, `answerDistribution`, `branchDistribution`, `offerDistribution`, and `bucketDistribution`. Question entries can contain `estimated` and `coverage: {status, reason, from}`. Unavailable coverage makes question counts and rates null rather than zero. High-volume question estimates are scoped to exposing widgets without requiring exposure-before-answer ordering; do not present them as exact ordered conversion counts.

`pathFlows.paths` contains `{from, to, count}` edges; `dropOffs` contains `{nodeId, count}`. Path analysis uses the latest 10,000 runs and the final seven days of the requested range, whichever is narrower. `analyzedFrom` and `runsAnalyzed` describe that smaller window. Project-wide requests omit paths. Recognized IDs can have resolved name fields added.

A completed run reached the end of its flow; it does not necessarily mean a form submission or a goal conversion. Analytics failures return `Flow metrics are unavailable. Please try again later.` Required-argument and date parsing errors can also reject a request.

Illustrative request and result excerpt:

```json
{"name":"get-flow-performance","arguments":{"flow_id":"ofn_workforge","date_from":"2026-10-01","date_to":"2026-10-07","timezone":"UTC"}}
```

```json
{"flowMetrics":{"totalExecutions":100,"completedRuns":60,"unfinishedRuns":40,"uniqueVisitors":90,"identifiedVisitors":25,"avgDurationMs":12000,"completionBreakdown":{"completed":60,"dismissed":40},"byFlow":[{"flowId":"ofn_workforge","executions":100,"uniqueVisitors":90,"completionRate":60}]},"exposurePages":[{"url":"https://example.com/welcome","pathname":"/welcome","hostname":"example.com","uniqueVisitors":90,"executions":100}],"pathFlows":{"paths":[],"dropOffs":[],"runsAnalyzed":100,"analyzedFrom":"2026-10-01T00:00:00Z"}}
```

## `get-flow-engagements`

Read individual response runs, including anonymous visitors, displayed questions with no answers, and skip-only runs. Historical runs without display markers need at least one recorded answer to appear in the response list.

| Argument               | Type and behavior                                                                                                                                                                       |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `flow_id`              | Required string.                                                                                                                                                                        |
| `date_from`, `date_to` | Required date strings; the tool advertises `YYYY-MM-DD`. See the date-boundary limitation below.                                                                                        |
| `email`                | Optional case-insensitive substring search; shares the search implementation below. Literal `*`, `?`, and backslashes are escaped rather than interpreted as caller-supplied wildcards. |
| `search`               | Optional string search across email and answer text; takes precedence over `email`. Matching segment names can also locate choice answers.                                              |
| `outcome`              | `all`, `completed`, `abandoned`, or `closed`; default `all`.                                                                                                                            |
| `answer_filters`       | Optional array, combined with AND. Each item requires string `segmentId`; optional string `questionId`, `segmentGroupId`, and `nodeId` narrow that answer match.                        |
| `limit`                | Integer, default 25, clamped to 1–100.                                                                                                                                                  |
| `search_after`         | Optional string cursor copied from the previous result's `searchAfter`.                                                                                                                 |
| `timezone`             | Optional string, default `UTC`, advertised for date display. It does not change the query constructor's date parsing.                                                                   |
| `project_id`           | Optional string under the shared contract.                                                                                                                                              |

**A date-only end means midnight.** This query parses the supplied boundaries directly. It does not expand a date-only `date_to` to the end of that day, unlike `get-flow-performance`. Don't assume the two tools cover identical hours just because you supplied identical dates.

Returns `{engagements, total, totalAll, totalEngaged, summary, searchAfter}`. Results are ordered by `startTime` descending, then ID descending. `searchAfter` is an opaque JSON-encoded string or null. Pass that exact string as `search_after` for the next page.

Each engagement contains `id`, `timestamp`, `startTime`, nullable `email`, nullable `visitorId`, `completionStatus`, nullable `widgetId` and `widgetTrigger`, `durationMs`, nullable `pageUrl`, `nodes`, nullable `deviceType` and `country`, plus:

- `outcome`: `completed` for reaching the end, `closed` for explicit dismissal before the end, otherwise `abandoned`.
- `engaged`: whether a question answer was recorded; skipping alone is not an answer.
- `hasDisplayMarkers`: whether a question-show marker exists.
- `stopNode`: last unanswered displayed question for an unfinished run, or null.
- `exitNode`: terminal content/action node for a completed run, or null.
- `identified`: whether a recorded email-question identification succeeded.
- `outcomeLabel`: human-readable outcome with the last relevant question, offer, or redirect when available.
- `answers`: flat map from question/dimension names to answer values. Multiple-choice names are comma-joined; free text can be prefixed `Other: `; skips use `Skipped`. Repeated labels can overwrite earlier entries, so use `nodes` for the full path.

`nodes` preserves the recorded event objects, not the editable flow-node schema. Events use `nodeType`, `nodeId`, timestamp, and type-specific fields such as `questionId`, `segmentGroupId`, `selectedSegmentId` (string or array), `inputValue`, `freeformInput`, `otherText`, `status`, `offerId`, `conversionType`, or `url`. `stopNode` and `exitNode` have that same recorded-event shape. Resolved resource-name fields may be added.

Only the first page has `summary`. It contains `entered`, `answered`, `answeredRate`, `completed`, `completionRate`, `identified`, `identifiedCoverage`, `stopped`, and `legacyRunsExcluded`. Rates are percentages or null when their denominator is zero. Completion rate uses answered/engaged runs, not all entrants. `identifiedCoverage` is `{status: "available"|"unavailable", reason: string|null, from: string|null}`; unavailable coverage makes `identified` null. `stopped` entries have `nodeId`, `questionId`, and `count`; historical runs without display markers are excluded from those stop counts.

The summary is based on the flow/date range before search, outcome, or answer filters. `total` counts filtered matching rows. `totalAll` and `totalEngaged` mirror first-page `entered` and `answered`; subsequent pages return zero for these two fields and null for `summary`. Preserve the first-page summary instead of treating those later zeros as new totals.

Malformed cursors, invalid outcomes, date parsing, or incomplete analytics queries return errors; unavailable analytics are not a successful empty report. This read can include personal contact and answer data, so share results only with authorized recipients. It changes no configuration or contact data.

Illustrative request and result excerpt:

```json
{"name":"get-flow-engagements","arguments":{"flow_id":"ofn_workforge","date_from":"2026-10-01","date_to":"2026-10-08","outcome":"completed","limit":25}}
```

```json
{"engagements":[{"id":"run_workforge","startTime":"2026-10-03T09:00:00Z","email":null,"visitorId":"visitor_workforge","outcome":"completed","engaged":true,"hasDisplayMarkers":true,"stopNode":null,"identified":false,"outcomeLabel":"Completed · Shown: WorkForge demo","answers":{"Team":"Operations"}}],"total":1,"totalAll":2,"totalEngaged":1,"searchAfter":null}
```
