Skip to help content
RightMessage
MCP referenceReference

MCP tools for campaigns and page elements

Look up campaign arguments, edit one audience safely, set up and end A/B tests, validate page targeting, and read live performance without publishing your draft.

Available on the Grow, Scale, and Pro plans.

Updated

View as Markdown

Use these tools to inspect page elements, build audience-specific website changes, and read campaign results. Configuration reads can show an unpublished draft. Performance reports measure the live campaign.

MCP campaign 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 it explicitly when working across projects.

The tools follow the shared MCP authorization and project-selection rules. Grow includes MCP access. Campaign personalization and goals require Scale or above to publish. Creating a draft through MCP does not override that publishing requirement.

Create, update, duplicate, and delete operate on the draft. They save a configuration snapshot, validate the result, and update the draft. None publishes or changes a live page immediately. Resource reads use the draft first, otherwise the published configuration. Shared failures include missing or inaccessible projects, missing entitlement, and configuration validation errors. See publication, backups, and undo before making changes live.

Examples are illustrative, not executed. Requests show the tool-call name and arguments. JSON results show the content, not the MCP transport envelope. Excerpts are labeled.

Campaign write arguments

Creation and update accept the following fields. Creation requires name; update requires campaign_id and at least one changed field.

ArgumentType and behavior
nameString. Internal campaign name.
is_activeBoolean. Creation default: true.
base_urlString. Campaign base URL. Creation trims it and adds https:// if no HTTP scheme is present. Creation default: null. Update does not perform that normalization.
testingObject. The campaign holdback ("Campaign vs holdback"), separate from audience A/B tests. is_enabled is boolean; on creation it defaults to true only when the campaign has audiences (changes), and to false for a campaign without audiences. withhold is the held-back percentage, a whole number from 1 to 100. Creation defaults it to 10.
goal_idsArray of goal-ID strings, default [] on creation. Order matters for primary_goal. Starting an A/B test needs at least one goal.
outcomeany_goal or primary_goal. Creation default any_goal. Legacy campaigns without the field use primary_goal.
changesArray of change objects described below. Omission creates an empty campaign or preserves an existing campaign's audiences.
audience_testsUpdate only. Array of A/B test setups, described in audience A/B test setup. Omission preserves existing tests.

any_goal counts a visitor once when they convert on any attached goal. primary_goal uses the first ID in goal_ids. Use goal tools to create or inspect those goals.

On update, supplied top-level fields replace the corresponding stored fields; omitted fields remain. testing replaces the object rather than deep-merging it, although an omitted or nonnumeric withhold keeps the stored percentage. Save-time repair resets an omitted or nonboolean is_enabled to true, even if testing was previously off. Send both is_enabled and withhold when changing testing. Top-level null arguments are ignored, not a way to clear a field.

Each changes item

PropertyType and behavior
idExisting action ID, used on update to merge into that action. Omit for a new action.
variant_idExisting variant ID returned with id. Keep it to distinguish actions whose IDs repeat across variants. "original" is reserved for the original page test's challenger rows.
version_idA/B test version: A, B, C, or D. Omit it to edit the audience's own changes (version A). Rows with variant_id: "original" must name a challenger, B–D.
kindoverride for a challenger row that changes or skips one of version A's actions, described below.
segment_idsA list of segment IDs or a comma-separated string, such as ["seg_a", "seg_b"] or "seg_a, seg_b". Anything else is rejected. New changes require at least one valid segment ID. Keep IDs in the same order across new changes that should share an audience. Omission on an existing change preserves its targeting; empty or null targeting is rejected.
selectorCSS-selector string. Use unless supplying targeting. A new change that edits content with neither selector nor targeting is rejected with Each new change that edits the page needs the element it changes: targeting from get-page-elements, or a selector.
targetingSmart Match object. Prefer the complete object returned by get-page-elements.
pageComma-separated domain/path patterns, such as example.com/pricing, example.com/demo. No URL scheme. Omission on a new action scopes it to the campaign's base_url page (its host and path), or all pages when the campaign has no base_url. Send * to target every page. Omission on an existing action preserves its page rules.
contentReplacement string, or newline-separated /find/replace/ expressions for partial text changes.
visibilityshow or hide.
styleObject mapping CSS properties to overrides.
attrObject mapping HTML attributes to overrides.
add_classes, remove_classesCSS-class strings, separated by commas or spaces. A leading . is ignored.

A domain and its www. form match each other. Other subdomains must be targeted explicitly. */blog* targets matching paths on any domain. Each page token becomes {domain, path}; a token without a slash gets path *.

The changes array is a replacement list, not an append operation. On update:

  • Start with the complete changes returned by get-campaign.
  • Keep every action you want to retain. Unrepresented actions are removed; variants emptied by that removal are removed too. Variants that already had no actions are retained.
  • An existing id plus variant_id merges supplied properties into the stored action. Omitted action properties survive. Explicit null removes targeting or modification values; null selector becomes an empty string, and null page resets targeting to all domains and paths.
  • Matching segment sets retain the existing variant ID and name. An unchanged round-trip retains the stored campaign content.
  • A change without id creates a new action.

Do not reconstruct the update from the presentation-oriented variants field. Its long text can be truncated.

Resolve every segment ID before saving. Unknown segment IDs are rejected. Every audience must target at least one segment; Original page is the unchanged page, not a targetable audience. Empty targeting returns Each audience must target at least one segment. Provide non-empty segment_ids; Original is the unchanged page. The one exception is an original page test: its challenger rows use variant_id: "original" and a version_id of B–D, and carry no segment targeting. New audiences group by the supplied ID order, while matching an existing audience compares the segment set without regard to order.

Audience A/B test setup

An A/B test splits one audience, or the original page, into whole versions: A is the audience's existing changes, and B, C, and D are challengers. For the original page, A (Control) is the unchanged page and has no changes. The editor workflow is described in run an A/B test on a page or audience.

Each audience_tests item is {variant_id, test: {id, state, run?, versions: [{id, name, weight}]}}. Use an existing audience ID, or "original" for the original page.

  • {variant_id, test: {}} creates a setup with versions A and B at 50/50. It doesn't touch changes.
  • To add C or D, send the full versions list. Weights are integers from 1 to 99 and must total 100. A test has at most four versions, and must keep A plus at least one challenger.
  • state and run can't change here. Lifecycle changes go through start-audience-test, restart-audience-test, and end-audience-test, otherwise the request returns Use the start, restart or end audience test operation to change its lifecycle. A new setup sent with a non-setup state returns Create a setup first; use start-audience-test to start it.
  • Names can change while a test is live. Versions, weights, content, and targeting change only through a restart.
  • Deleting a tested audience while its test runs returns End the test before deleting its audience, or restart the test before changing its lifecycle.

Challenger content goes in changes. A challenger-only element is a normal change row with version_id. A change to one of version A's actions is {variant_id, version_id: "B", kind: "override", action_id, skip: true} to drop that action, or the same row with fields: [{path: ["text"], op: "set", value: "New copy"}] (op is set or remove) to alter it. Send the whole authored changes array from get-campaign, not resolved copies. Original page rows can't use kind: "override", because Control has no actions.

Smart Match object

targeting requires mode: fingerprint or selector. It can contain fallbackSelector and selector strings. A fingerprint object requires text; its nested fields are:

FieldSchema
textRequired content string; optional matchType: exact, contains, or startsWith, default exact.
elementtagName string and candidateTags string array, default [].
structureInteger siblingIndex, siblingCount, and depth; string parentChain.
semanticsNullable strings id, ariaRole, ariaLabel; nullable object dataAttributes; string array meaningfulClasses, default [].
contentInteger charCount and wordCount; string textPrefix.
visualfontSize: {min, max}, fontWeight: {min}, and positionY: {min, max}, with numeric bounds; boolean isAboveFold. These signals are not returned by the server-side page extractor.

Preserve the returned fingerprint instead of guessing it. Identical text on two elements still requires two actions, because their structure signals distinguish the elements.

Returned campaign object

Create, update, and duplicate return the saved campaign, including:

  • id (cpn_…), name, is_active, base_url, goal_ids, outcome, and testing.
  • original, the original page node. It holds the original page test and any deployment from an ended original page test.
  • Configuration fields recipe_code, dimension_id, and condition; new campaigns use null, null, and true respectively.
  • variants: the stored audiences, each with id, name, rules, and actions, plus test when the audience has an A/B test. New audience rules use rules.all entries with dimensionId and segment. Existing audiences can use rules.any. All audiences require segment targeting.
  • Each action includes its id, target/selector, page-rule array, and modifications. Modifications can contain text, findReplace entries {find, replace}, visibility, style, attr, and classes with add/remove arrays.

Create and update also add unique_variant_count when it is greater than zero. This is the number of segment combinations across the dimensions used in variant rules, not simply the length of variants.

get-page-elements

Fetch a page's rendered HTML and identify elements available for personalization. Requires url (full HTTP or HTTPS URL). Optional no_cache (boolean, default false) bypasses the 24-hour HTML cache. Optional project_id uses the shared contract.

Returns {url, title, element_count, elements_capped, elements}. element_count is the total found before the 200-element cap; elements_capped indicates truncation. Each element contains uppercase type, content, selector_stable, text_unique, and targeting.

The extractor includes headings, paragraphs, buttons, links, and list items with at least three bytes of text. It skips scripts, styles, noscript, SVG, head, navigation, and footer subtrees. A header can still contain eligible content. Fingerprint parent chains contain at most five ancestors, while depth describes the full depth. Semantic hints omit empty values, skip internal data-rm-* attributes, and keep at most three meaningful classes.

This is read-only with respect to RightMessage configuration, but it makes an external page-fetch request through Firecrawl and can populate its cache. It does not install a snippet or edit the page.

Invalid URLs return Invalid URL format. Provide a full URL including https://. Fetch failures return Failed to fetch page HTML: …, or an error saying the page may be unreachable or Firecrawl may not be configured.

Illustrative request and result:

JSON
{"name":"get-page-elements","arguments":{"url":"https://example.com/pricing","no_cache":false}}
JSON
{"url":"https://example.com/pricing","title":"WorkForge pricing","element_count":1,"elements_capped":false,"elements":[{"type":"H1","content":"WorkForge pricing","selector_stable":true,"text_unique":true,"targeting":{"mode":"fingerprint","fingerprint":{"text":{"content":"WorkForge pricing","matchType":"exact"},"element":{"tagName":"h1","candidateTags":["h1","div","span","p"]},"structure":{"siblingIndex":0,"siblingCount":1,"parentChain":"BODY > MAIN","depth":2},"semantics":{"id":"pricing-title"},"content":{"charCount":17,"wordCount":2,"textPrefix":"WorkForge pricing"}},"fallbackSelector":"#pricing-title"}}]}

list-campaigns

List compact campaign summaries. The only argument is optional project_id. No configuration changes occur.

Returns an array of {id, name, is_active, variantCount, goalCount, abTestSummary} and outcome when that field exists. abTestSummary is a string such as 10% control group, or null when testing is off. variantCount counts saved variants, not all possible combinations. Shared access errors apply; an empty configuration returns [].

Illustrative request and result:

JSON
{"name":"list-campaigns","arguments":{}}
JSON
[{"id":"cpn_workforge","name":"WorkForge pricing","is_active":true,"variantCount":1,"goalCount":1,"outcome":"any_goal","abTestSummary":"10% control group"}]

get-campaign

Read a campaign and its lossless editable changes. Requires campaign_id (string); accepts optional project_id.

Returns the campaign object, plus changes in the write format above, audience_tests, and original_deployment. Presentation fields add isDefault to variants, pageFormatted to actions, and resolved names alongside known IDs. Modification strings in variants are shortened after 100 bytes and can include phrase annotations. The separate changes array is untruncated and preserves empty JSON objects.

  • audience_tests lists every A/B test as {variant_id, test: {id, state, run?, versions: [{id, name, weight}]}}, with the original page's first under variant_id: "original". state is setup or running. Copy it with changes for a lossless round trip.
  • original_deployment is null unless an original page test ended with deploy: true. Then it is {test_id, version_id, name, deployed_at, proven, changes}: the version everyone who sees the original page still gets until undeploy-original-winner removes it. deployed_at is null on deployments made before it was recorded. proven is true only when that version's recorded verdict was Winner. Deploying is a bridge until your website shows the changes itself, not a permanent patch.

Missing ID returns campaign_id is required. Unknown ID returns Campaign '…' not found. No configuration changes occur.

Illustrative request and result excerpt:

JSON
{"name":"get-campaign","arguments":{"campaign_id":"cpn_workforge"}}
JSON
{"id":"cpn_workforge","name":"WorkForge pricing","changes":[{"id":"act_title001","variant_id":"var_agencies","segment_ids":"seg_agency","selector":"#pricing-title","targeting":null,"page":"workforge.rightmessage.com/pricing","content":"Choose your agency's WorkForge plan","visibility":null,"style":null,"attr":null,"add_classes":null,"remove_classes":null}],"audience_tests":[{"variant_id":"var_agencies","test":{"id":"abt_agencies","state":"setup","versions":[{"id":"A","name":"Version A","weight":50},{"id":"B","name":"Version B","weight":50}]}}],"original_deployment":null}

create-campaign

Create a campaign in the draft. Requires name and accepts the campaign write arguments plus optional project_id.

Returns the saved campaign object. Without changes, it creates no audiences and its holdback starts off. Each new change needs non-empty, valid segment_ids. Empty targeting and unknown segment IDs reject the save. Missing name returns name is required. Shared save and plan boundaries apply. A campaign without audiences can still A/B test the page itself: add an original page test with update-campaign and audience_tests: [{variant_id: "original", test: {}}].

Illustrative request and result excerpt:

JSON
{"name":"create-campaign","arguments":{"name":"WorkForge pricing","base_url":"https://workforge.rightmessage.com/pricing","goal_ids":["gol_workdemo"],"outcome":"any_goal","testing":{"is_enabled":true,"withhold":10},"changes":[{"selector":"#pricing-title","segment_ids":"seg_agency","page":"workforge.rightmessage.com/pricing","content":"Choose your agency's WorkForge plan"}]}}
JSON
{"id":"cpn_workforge","name":"WorkForge pricing","is_active":true,"base_url":"https://example.com/pricing","goal_ids":["gol_workdemo"],"outcome":"any_goal","testing":{"is_enabled":true,"withhold":10}}

Use the Smart Match targeting from get-page-elements when available; the shorter selector example assumes a known stable ID.

update-campaign

Change a draft campaign. Requires campaign_id (string) and at least one campaign write argument. Optional project_id uses the shared contract.

Returns the saved campaign object. A supplied changes list can remove actions and variants, so retain the complete list you intend to keep. To rename a campaign, omit changes entirely. Unknown ID returns Campaign '…' not found. Missing fields return At least one field to update must be provided. Invalid configuration returns an error rather than a saved result.

Illustrative request and result excerpt:

JSON
{"name":"update-campaign","arguments":{"campaign_id":"cpn_workforge","name":"WorkForge pricing headline"}}
JSON
{"id":"cpn_workforge","name":"WorkForge pricing headline","is_active":true,"base_url":"https://example.com/pricing","goal_ids":["gol_workdemo"],"outcome":"any_goal","testing":{"is_enabled":true,"withhold":10}}

set-audience-changes

Change one existing audience, or one version of an A/B test, without resending the campaign's complete changes list. The hosted MCP name is set-audience-changes; Righty uses the internal name set_audience_changes. This tool saves a draft, preserves the audience's ALL or ANY segment rules, and leaves other audiences unchanged. It never deletes saved changes or audiences.

ArgumentType and behavior
project_idOptional string under the shared project-selection contract.
campaign_idRequired string. Existing campaign ID, maximum 128 characters.
variant_idRequired string. Exact existing audience ID, maximum 128 characters. The audience must have segment targeting. Use "original" to write a challenger for the original page test.
version_idOptional: A, B, C, or D; default A. Writes that A/B test version. Challenger edits never change A. For "original", use B–D, because Control is the unchanged page.
modeRequired: fill or rewrite. Use fill unless the user explicitly asks to replace existing changes. fill skips tested audiences. There is no replace mode.
changesRequired array of 1–200 objects, totaling at most 128 KiB of JSON.
changes[].selectorRequired CSS-selector string, maximum 4,096 characters.
changes[].targetingOptional stored Smart Match object. Preserve the returned object rather than guessing its fields.
changes[].pageOptional array of 1–50 page-rule objects. Outside an editor context, omission uses the campaign's base_url page; it's required only when the campaign has no base_url. It is not the comma-separated string used by create-campaign and update-campaign.
changes[].modificationsRequired object containing supported stored modification fields, described below.

Fill or replace existing changes

fill skips an audience-and-element pair when it already has a change on an overlapping page. That includes styling, attributes, classes, and visibility changes, not just text. The check uses the latest saved draft and any supplied editor observations. Selector aliases and Smart Match fingerprints help identify the same target. Page scopes that can't be proven separate are treated as overlapping.

rewrite updates one matching saved change, or creates a new one when none matches. If several saved changes match the target and page, the request is rejected instead of choosing one.

A text-only rewrite patches modifications.text and keeps the existing styling, attributes, classes, and visibility. If you supply other modification fields too, the supplied modifications object replaces the existing object. Include every modification you want to retain in that case. Rewriting preserves the saved change's target and page rules.

Supported fields are:

  • text: string, at most 65,536 characters.
  • visibility: show or hide.
  • style: object with at most 100 CSS properties; each value is a string up to 4,096 characters.
  • attr: object with at most 100 attributes; each value is a string up to 8,192 characters.
  • classes: object with add and/or remove string arrays, each with at most 100 class names of up to 256 characters. An entry can name several classes separated by commas or spaces, and the 100-name limit counts them after splitting.

This tool does not accept the flattened content, add_classes, or remove_classes fields from campaign CRUD, or a findReplace field.

Page rules and validation

Each page-rule object requires domain and path. domain is a hostname or *, normalized to lowercase without a scheme or port. Its input is limited to 253 characters; path is a string up to 8,192 characters.

Optional operator accepts equals, does not equal, contains, does not contain, starts with, ends with, or glob. Without an operator, a path without * matches exactly; a path with * uses the legacy wildcard behavior. Prefer an explicit operator.

With saved editor context, omitting page uses the current editor URL as an exact page rule. Outside that context, omitting page uses the campaign's base_url page as an exact rule. An empty array is invalid and returns Page rules must contain at least one rule. Omit page to use the current editor URL. A missing page outside the editor on a campaign without base_url returns Page rules are required outside the campaign editor when the campaign has no base_url.

When editor observations are available, fill requests on that page reject copy unchanged from the original, including whitespace-only differences. Known elements that can't hold text reject text changes. A request that replaces a container's text and also changes one of its children on an overlapping page is rejected.

Matrix selection conversations cannot write during their opening recap. Later writes must stay within the saved selected cells and exact page. A selected-cell rewrite is rejected if its existing change also covers other pages. Matrix fill requests are similarly limited to their saved empty-cell scope.

To change a hosted page (Pages), name the element as [data-rm-id="…"] and pass the page's page_rule from get-page as page. MCP tools for pages covers page audiences and tests.

Request and result

Illustrative request using IDs from the chosen project:

JSON
{"name":"set-audience-changes","arguments":{"campaign_id":"cpn_workforge","variant_id":"var_agencies","mode":"fill","changes":[{"selector":"#pricing-title","page":[{"domain":"workforge.rightmessage.com","path":"/pricing","operator":"equals"}],"modifications":{"text":"A plan for your agency's next project"}}]}}

The result is {campaignId, variantId, variantName, applied, skipped}. applied contains {actionId, label} for each saved change. skipped contains {label, reason} for each skipped element. A fully skipped request doesn't create a change receipt.

For the corresponding editor workflows, see write page copy with Righty, personalize page content, and personalization audiences.

duplicate-campaign

Copy a campaign into the draft. Requires campaign_id (string). Optional new_name defaults to the original name plus (Copy); optional project_id uses the shared contract.

Returns the copied campaign with a fresh campaign ID and fresh variant/action IDs. Goals and segment references remain references to the existing resources. The copy retains the campaign's configuration; it is not a copy of historical performance. Nothing is published.

Missing ID, an unknown campaign, or invalid copied configuration returns an error.

Illustrative request and result excerpt:

JSON
{"name":"duplicate-campaign","arguments":{"campaign_id":"cpn_workforge","new_name":"WorkForge pricing alternative"}}
JSON
{"id":"cpn_alternate","name":"WorkForge pricing alternative","is_active":true,"goal_ids":["gol_workdemo"],"outcome":"any_goal","testing":{"is_enabled":true,"withhold":10}}

delete-campaign

Remove a campaign from the draft. Requires campaign_id (string); accepts optional project_id.

Returns {deleted: true, campaign_id}. It removes the campaign configuration, not the goal resources or previously recorded events. The live campaign is unaffected until publication. A campaign with a live A/B test returns End the live A/B test before deleting this campaign. Missing ID returns campaign_id is required. Unknown ID returns Campaign '…' not found.

Illustrative request and result:

JSON
{"name":"delete-campaign","arguments":{"campaign_id":"cpn_alternate"}}
JSON
{"deleted":true,"campaign_id":"cpn_alternate"}

A/B test lifecycle tools

start-audience-test, restart-audience-test, end-audience-test, and undeploy-original-winner change the draft only. Each saves a configuration snapshot and returns {campaign, requires_publish: true}. Visitors see nothing new until publish-config. Publishing is what starts or restarts the test for visitors.

Starting or restarting a test requires Pro. Ending a test and undeploying a winner don't need Pro. If the project moves to a plan without Pro, a running test stops on the live site when the plan changes, or at the next publish at the latest, and stays in the draft; publish-config still publishes the project's other changes. Grow doesn't include campaigns, so campaigns and their tests come off the live site.

All four accept optional project_id and require campaign_id. The three test tools also require audience_id: an audience ID from the campaign's variants, or "original" for the original page test.

ToolExtra argumentsWhat it does to the draft
start-audience-testNoneStarts a test in setup state with a fresh run.
restart-audience-testNoneGives a running test a fresh run so its versions, weights, content, or targeting can change. Earlier results stay in history. Visitors keep the published run until you publish.
end-audience-testkeep_version (required: A, B, C, or D); deploy (optional boolean)Removes the test. For an audience, the kept version becomes the audience's changes. For the original page, nothing is kept unless deploy is true. Results stay in history.
undeploy-original-winnerNoneRemoves the original_deployment winner. Use it once your website shows those changes itself.

deploy: true applies only to audience_id: "original" with a keep_version of B, C, or D. It keeps showing that version to everyone who sees the original page until undeploy-original-winner, while you update your website.

Without Pro, start and restart are refused before anything changes: Starting an A/B test needs the Pro plan. The test setup stays in the draft and ending a test is always allowed; upgrade the project to Pro (Settings › Billing) to start it. No snapshot is saved.

On Pro, start and restart refuse the request until the test is ready. The error joins every blocker:

  • Choose a campaign goal before starting a test.
  • Add at least one challenger version.
  • Version B must differ from A. (one per identical challenger)

Other refusals:

  • This operation requires a setup test. (start on a running test) or This operation requires a running test. (restart on a setup test).
  • This audience has no test. and Unknown audience ….
  • Only the original page deploys a winner. Ending an audience test keeps the version as its changes. (deploy: true on an audience).
  • Control is the original page, so there is nothing to deploy. (deploy: true with keep_version: "A").
  • The original page has no deployed winner. (undeploy with nothing deployed).
  • keep_version must be A, B, C, or D., campaign_id is required., audience_id is required., and Campaign not found: ….

Illustrative request and result excerpt:

JSON
{"name":"end-audience-test","arguments":{"campaign_id":"cpn_workforge","audience_id":"original","keep_version":"B","deploy":true}}
JSON
{"campaign":{"id":"cpn_workforge","original":{"deployment":{"testId":"abt_page","versionId":"B","name":"Version B","deployedAt":"2026-10-18T09:00:00.000Z","proven":true,"actions":[]}}},"requires_publish":true}

For when to end a test and what Deploy means, see end a test and deploy the winner.

get-campaign-performance

Read live campaign results for an inclusive local-date range, plus every audience and original page A/B test. This tool does not measure draft-only changes and does not alter a test.

ArgumentType and behavior
date_fromRequired string: YYYY-MM-DD or ISO 8601 timestamp. The date is taken in timezone.
date_toRequired string in the same format. Includes that local date. Must not precede date_from; maximum 90 inclusive days.
timezoneIANA timezone string, default UTC.
campaign_idOptional string to filter returned campaign rows.
project_idOptional string under the shared access contract.

Range figures count unique visitors. A conversion must follow exposure in the same arm. Visitors exposed to both arms are excluded. The selected range's descriptive lift is not a fresh experiment decision.

Response states

A cold or stale computation can return:

JSON
{"status":"computing","retry_after_seconds":5,"message":"Campaign results are computing. Call get-campaign-performance again with the same arguments after 5 seconds.","audienceTests":[]}

Call again with the same arguments after the stated delay. This queues or reads reporting work; it does not publish. audienceTests is already filled in while campaign results compute.

Other responses have status, period ({start, end, timezone} or null), computedAt (timestamp or null), and stale (boolean). Non-ready states include capped, not_live, and unavailable; they add reason and totals. Missing data is not zero.

A ready response adds:

FieldContract
headlinePortfolio comparison: status, basis, relativeLift, observedLift, campaignCount, basisCounts, visitors, reason, display, and text. basis is holdback, proven, mixed, or null. basisCounts counts the campaigns in the headline by basis; its baseline count is always 0. display has value and label strings. The headline is the visitor-weighted mean of controlled lifts only (holdback and proven); baseline lifts stay on their campaigns and never enter it. reason not_controlled means only baseline lifts were available.
experimentCountsCounts keyed by experiment state. This is separate from the date-range measurement.
campaignsArray of campaign result objects described below.
notLiveArray of {id, name, reason} for draft_only or inactive campaigns. A draft_goals entry also contains goalIds.
totalsvisitorsReached and goalConversions measurement objects, not bare numbers.
audienceTestsArray of A/B test results, described in A/B test results.

Filtering by campaign_id filters campaigns, notLive, audienceTests, and the totals; the portfolio headline and experiment counts are assembled before that filter.

Each campaign result has id, name, testingEnabled, holdbackPercent, daysOfData, exposedVisitorsPerDay, visitors: {personalized, holdback}, excludedBothArmVisitors, status, statusLabel, statusReason, measuredFrom, goals, outcome, comparison, experiment, and revenue.

goals is an array; outcome is a goal-shaped object or null. Each contains id, name, primary, personalized, holdback, lift, relativeLift, ci95, pValue, status, statusLabel, and statusReason. Both arms contain visitors, converters, and nullable conversionRate. Rates and relativeLift are proportions, not percentages. ci95 is {lower, upper} or null. Null lift means it cannot be measured, not no effect.

comparison is null or {basis, label, lift, relativeLift, rate, reference}:

  • holdback compares personalized visitors with the control group in the selected range. With testing on, comparison is null until both groups have at least 10 converters.
  • proven is a prior A/B result retained after rollout, not a remeasurement of this range. Its rate and reference are null.
  • baseline compares same-page visitors who did not see the campaign, weighted to the personalized group's engagement and known-contact mix. It is not a controlled test.

When present, reference has visitors, converters, and conversionRate. Per-goal status text still describes holdback diagnostics even when the campaign comparison uses another basis.

experiment is null or the separately persisted decision: state, label, versionNumber, startedAt, seriesStartWeek, provenance, metricId, lookNumber, cutoffAt, decidedAt, arms, observedLift, decidedLift, decidedVisitors, haltReason, and proven. Timestamps, metrics, arm results, and lift values can be null when there is not enough decision-grade data. Experiment arms use enrollment and converter counts rather than the range's exposed-visitor totals. proven, when present, contains state, label, versionNumber, startedAt, endedAt, decidedAt, metricId, observedLift, decidedLift, decidedVisitors, and unchanged.

Experiment state is collecting, winner, loser, inconclusive, halted, not_decision_grade, or not_measurable; those are also the experimentCounts keys. When available, arms.personalized and arms.holdback contain enrolled, converters, and nullable pageviews. decidedVisitors is the combined enrolled-visitor count at the decision, not a visitor list. haltReason is srm, contamination, or null. experiment describes the campaign holdback only; A/B test results are in audienceTests.

Campaign revenue

revenue is revenue per visitor by arm from the connected payment source (Connect Stripe). It is null when no payment source is connected. Otherwise it has a status:

  • computing with retryAfterSeconds: call again with the same arguments later.
  • unavailable with reason.
  • ready, which adds source, matching, excludedPeople, identified, minBuyers, minVisitors, and currencies.

Revenue counts each identified visitor's payments after their first exposure in their arm. Visitors never identified by email count as $0 in both arms. People exposed in both arms are left out and counted in excludedPeople. matching is complete, or capped when the campaign had too many visitors to match; a capped result has no currencies.

Each currencies item has currency, personalized, holdback, and difference. Each arm has visitors, buyers, net, and perVisitor. perVisitor is null while that arm has fewer than minBuyers (5) buyers or minVisitors (30) visitors. difference is personalized minus held back per visitor, as {perVisitor, low, high} with a 95% range, and is null unless both arms pass those thresholds. Currencies are never added together.

identified is null or {personalized, holdback, differs}: each arm's percentage of visitors with an email. differs: true means the arms were identified at different rates, so matched revenue can differ for reasons other than buying.

Campaign revenue is descriptive only. It never changes a comparison, a test result, or the experiment state.

A/B test results

Each audienceTests item is one run of an audience or original page test: testId, campaignId, audienceId (original for the original page), run, status, startedAt, endedAt, weeksRunning, lookNumber, nextCheckAt, haltReason, missedChecks, cannotDecideReason, versions, and heldBackVisitors. Verdicts are checked weekly; nextCheckAt is the next weekly check while the test runs, otherwise null.

status is one of:

  • running: collecting weekly checks.
  • ready_to_end: every challenger has a verdict.
  • cannot_decide: cannotDecideReason is missed_checks or late_weeks.
  • paused_health: results are on hold. haltReason is allocation, contamination, or unconfirmed_publish. unconfirmed_publish means RightMessage couldn't confirm when the last publish went live, so results are on hold and version counts are withheld, not zero. It isn't a split or traffic problem.
  • paused_campaign_off: the campaign is off.
  • ended.

Each versions item has id, name, weight, visitors, conversions, rate, diffFromA, verdict, likelyRange ({low, high} or null), conversionValue, valuePerVisitor, and decidedLookNumber. verdict is baseline for A, then collecting, winner, loser, inconclusive, cannot_decide, or no_verdict. no_verdict means the test ended before that version reached a verdict; it is not still collecting. conversionValue comes from goal values and never decides a winner. How to read these is covered in read A/B test results.

Each totals measurement uses one of three shapes:

  • Ready: {status: "ready", value: number, computedAt: string, stale: boolean}.
  • Computing: {status: "computing", retryAfterSeconds: number}.
  • Unavailable: {status: "unavailable", reason: string, explanation: string, availableFrom: string|null}. There is no numeric value in this state.

Errors and example

Invalid timezone returns timezone must be a valid IANA timezone. Too many days returns The date range must be no more than 90 inclusive days. Invalid or reversed dates return an error describing the accepted date formats. A specific campaign absent from both live and draft/inactive entries returns a not-found error. Other reporting failures return Failed to fetch campaign performance for project ….

Illustrative request and result excerpt from a ready response:

JSON
{"name":"get-campaign-performance","arguments":{"campaign_id":"cpn_workforge","date_from":"2026-10-01","date_to":"2026-10-07","timezone":"UTC"}}
JSON
{"status":"ready","period":{"start":"2026-10-01","end":"2026-10-07","timezone":"UTC"},"computedAt":"2026-10-08T09:00:00Z","stale":false,"campaigns":[{"id":"cpn_workforge","name":"WorkForge pricing","testingEnabled":true,"holdbackPercent":10,"visitors":{"personalized":900,"holdback":100},"excludedBothArmVisitors":0,"comparison":{"basis":"holdback","label":"vs holdback","lift":"+20%","relativeLift":0.2,"rate":0.12,"reference":{"visitors":100,"converters":10,"conversionRate":0.1}}}],"notLive":[]}

Still need a hand?

Tell us what you’re trying to do and where you’re getting stuck.

support@rightmessage.com