ReferenceToken resolution

Token resolution

How double-curly-brace tokens in landing pages, result pages, and prompts resolve per end user — and which prefix to use where.

Productised uses \{\{ token \}\} syntax to inject dynamic values at render time. Six prefixes, each backed by a different source. Five of them resolve from your end user's collected data (form answers, AI agent output, computed visualisations, workspace variables); the sixth — url: — resolves from the request URL itself, for outbound personalisation.


The six token prefixes

PrefixSourceWhere it livesExample
\{\{ form:fieldName \}\}An answer from the intake conversationResponse.userData\{\{ form:firstName \}\}
\{\{ node:nodeVar \}\}The full output of an AI nodeResponse.result[nodeVar]\{\{ node:report \}\}
\{\{ object:fieldName \}\}A structured field from an AI agentResponse.result[nodeVar].object[fieldName]\{\{ object:overall_score \}\}
\{\{ chart:key \}\}A computed visualisation token (or raw passthrough of any agent field)Computed from result at render time\{\{ chart:dim1_pct \}\}
\{\{ custom:key \}\}A workspace variableTenantVariable\{\{ custom:business_name \}\}
`{{ url:fieldNamefallback }}`A URL query parameter (outbound personalisation)The request URL

When each prefix resolves

SurfaceFormNodeObjectChartCustomURL
Landing page (/c/<product>)
AI conversation✅ (in system prompts)✅ (in system prompts)
Result page (/pages/<page>/<response>)— (use form: instead)
Standard output (streamed report)— (use form: instead)

Your end user hasn't started yet at the landing-page stage, so form:, node:, object:, and chart: don't have data to resolve from. Only the workspace-wide custom: tokens and the request-URL url: tokens work there.

By the time the result page or standard output renders, the URL params (if any) have been captured into the response's userData. Reference them via \{\{ form:fieldName \}\} instead of \{\{ url:fieldName \}\} for consistency.


object: vs chart: — the most common confusion

Both pull from the AI agent's structured output. The difference is what the renderer does to the value.

object: — runs through markdown

When you write \{\{ object:tier_description \}\} in element content like a <p> or <div>, the renderer:

  1. Looks up tier_description in the agent's objectVariables
  2. Runs the value through a markdown renderer (so **bold** and lists work)
  3. Wraps the result in HTML (typically a <p> tag)
  4. Inserts it into the element

That's perfect for prose content. But it's a problem inside an attribute.

chart: — raw passthrough

When you write stroke-dasharray="\{\{ chart:overall_score \}\} 100" or style="width:\{\{ chart:dim1_pct \}\}%", the renderer:

  1. Looks up the value (either the literal field, or a computed token for the special chart keys)
  2. Returns it raw, as a string, with no markdown processing
  3. Inserts it as-is into the attribute

Use chart: inside any HTML/SVG attribute. Use object: inside element content.


The five computed chart: tokens

These keys aren't agent fields — they're derived from the agent's dimN_score values at render time. Use them in dimension-bar widths and radar/gauge geometry without asking the AI agent to compute the maths.

TokenWhat it returnsCommon use
chart:dimN_pctdimN_score × 10 (0–100)Bar fill width style
chart:dimN_colorSemantic colour (green ≥7, amber ≥4.5, red below 4.5)Background colour for a dimension
chart:dimN_level"Strong" / "Developing" / "Opportunity"Label inside a dimension badge
chart:overall_avgMean of dim scores, 1 decimal placeCentre label of a radar chart
chart:radarSix "x,y" polygon points for a 6-axis spider chartSVG polygon points attribute
<!-- Example usage in HTML/SVG attributes -->
<div style="width:{{ chart:dim1_pct }}%"></div>
<div style="background:{{ chart:dim1_color }}"></div>
<span class="lvl">{{ chart:dim1_level }}</span>
<polygon points="{{ chart:radar }}" />

Any other chart:<fieldName> is a raw passthrough of that field from the agent's output. Convenient when you need an object: field but inside an attribute (e.g. \{\{ chart:overall_score \}\} for the gauge arc).


The coverage guard

When you save a page via create_page or update_page, the connector cross-checks every \{\{ object:fieldName \}\} token against the AI agent's configured fields. If any token references a field the agent doesn't produce, the tool returns:

{
  "warning": "⚠️ This page uses 3 object tokens the AI Agent does NOT output yet …",
  "fix": "Call set_agent_fields with the suggested_agent_fields below …",
  "suggested_agent_fields": [
    { "name": "sleepScore", "description": "Sleep score from 0 to 10 (one decimal place)." },
    { "name": "topStrength", "description": "The single strongest area, named, with a brief reason." },

  ]
}

Claude can call set_agent_fields with the suggestions in the same turn. The next submission populates the page correctly.


\{\{ form:* \}\} tolerance

form: resolution is tolerant of snake_case ↔ camelCase mismatches. If your template uses \{\{ form:first_name \}\} but the field is stored as firstName, the renderer normalises both sides (lowercased, separators stripped) and finds the match. This handles common templating mistakes silently.


\{\{ custom:* \}\} — the workspace layer

custom: tokens are key/value pairs scoped to your entire workspace. Set them via set_variable:

"Set my workspace variable cta_url to https://cal.com/me/strategy."

From now on, any product (landing, result, system prompt) that references \{\{ custom:cta_url \}\} reads the new value automatically. Change once, every product picks it up on next render.

Common variables to set on day one:

  • business_name
  • cta_url (your default booking link)
  • score_word (e.g. "Readiness" — appears in result page headlines like "Your Readiness Score")
  • product_name (overridable per product but useful as a workspace default)
  • brand_voice (a short style guide your AI agent prompts reference)

\{\{ url:* | fallback \}\} — the request layer

url: tokens resolve from the request URL's query parameters — not from any Productised data structure. They're the foundation of Outbound Personalisation: send a unique URL per prospect with their details baked in, and the welcome screen + AI agent's hidden system prompt substitute the values at render time.

Two things make url: distinct from the other prefixes:

  1. Fallback syntax. Always include a fallback after the pipe: \{\{ url:firstName | there \}\}. If the URL parameter is missing or empty, the fallback renders — so the bare share link still reads cleanly.
  2. Reserved keys. Platform-owned parameters (preview, demo, utm_*, fbclid, gclid, lng) are stripped before tokens resolve. You can't accidentally read or write them.

For the full picture — the conceptual model, security posture, length limits, and the dedicated MCP tools that wire it up — see the Outbound Personalisation docs.