MCP tools for flows and flow nodes
Exact flow and node tool arguments, tree schemas, mutation safeguards, analytics responses, and draft-only behavior.
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. 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, 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 and its linked question 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:scorename and numericadjustamount.field:fieldidentifier andvalueto render into the connected platform's custom field.variable: stringvariableNameandvariableValue. Values support Liquid; an empty rendered value removes the variable. For timed expiration, setexpiresType: "duration", numericexpiresDuration, andexpiresUnit:minutes,hours,days, orweeks. Otherwise the runtime stores no expiration timestamp. The open action schema does not declare numeric bounds for the duration.add_tagandremove_tag: stringtagId.set_segment: stringsegmentGroupIdandsegmentId. 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.
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}.idmust contain a nonwhitespace character. - Wall-of-love snapshot:
{kind: "wall_of_love", slug: string, permalink: string, name: string}.slugandpermalinkmust 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:
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, andconditionTypes._nodeUuids: array of{_uuid, type, name}for non-root nodes. It is not a connected tree.linkedWidgets: array of{id, name, type, isEnabled}._systemFlowNotefor 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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
sgusesdimensionfor the group ID, optionalask(default true), and starts withsplit: false. Setenable_split: trueto add branches; optionalchildrenis an array of child insertion objects, each with its target segment ID inbranch.offerusesoffer_id, which must identify an existing form or offer.yesnoaccepts a full logic object or simplified{dimension, operator, segments}.dimensionis a group-ID string;segmentsis an array of segment IDs;operatordefaults toinclude_any, with underscores translated to spaces. Omission creates an empty placeholder condition. Optionalyes_pathandno_patharrays 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, andinverse, 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:
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:
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:completedfor reaching the end,closedfor explicit dismissal before the end, otherwiseabandoned.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 prefixedOther:; skips useSkipped. Repeated labels can overwrite earlier entries, so usenodesfor 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: