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
| Prefix | Source | Where it lives | Example |
|---|---|---|---|
\{\{ form:fieldName \}\} | An answer from the intake conversation | Response.userData | \{\{ form:firstName \}\} |
\{\{ node:nodeVar \}\} | The full output of an AI node | Response.result[nodeVar] | \{\{ node:report \}\} |
\{\{ object:fieldName \}\} | A structured field from an AI agent | Response.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 variable | TenantVariable | \{\{ custom:business_name \}\} |
| `{{ url:fieldName | fallback }}` | A URL query parameter (outbound personalisation) | The request URL |
When each prefix resolves
| Surface | Form | Node | Object | Chart | Custom | URL |
|---|---|---|---|---|---|---|
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:
- Looks up
tier_descriptionin the agent'sobjectVariables - Runs the value through a markdown renderer (so
**bold**and lists work) - Wraps the result in HTML (typically a
<p>tag) - 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:
- Looks up the value (either the literal field, or a computed token for the special chart keys)
- Returns it raw, as a string, with no markdown processing
- 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.
| Token | What it returns | Common use |
|---|---|---|
chart:dimN_pct | dimN_score × 10 (0–100) | Bar fill width style |
chart:dimN_color | Semantic 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_avg | Mean of dim scores, 1 decimal place | Centre label of a radar chart |
chart:radar | Six "x,y" polygon points for a 6-axis spider chart | SVG 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_urlto 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_namecta_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:
- 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. - 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.