For the complete documentation index, see llms.txt. This page is also available as Markdown.

Dynamic Fields in Product Cards

Static cards go stale. When a card needs to show something that changes — the signed-in visitor's name, their account status, the tenant's catalog — you make it dynamic with the binding contract: {{field}} for author-set values and {{data.<name>.path}} for platform-resolved data.

One thing to be clear about first: this contract belongs to code-type custom components, which render as HTML inside a sandboxed iframe. The built-in Product Card is a React component with typed fields, not a {{ }} template — it has no placeholder syntax. To get a dynamic card, build one in Content → Component Library, or duplicate the built-in card and edit the copy.

{{field}} — author-editable values

Every field you declare on a component becomes a {{fieldName}} token in your HTML. Available field types are text, number, textarea, select, radio, plus two pickers that render a runtime dropdown: product-set-picker and hosted-page-picker.

Substitution is deliberately dumb, and that is the contract:

  • It is pure replacement — there is no expression evaluation inside {{ }}. A token is a name or a dotted path, never JavaScript.

  • Only declared field names substitute. An undeclared token is left in the output verbatim, so a typo is visible rather than silently blank.

  • Every substituted value is HTML-escaped, so a value cannot inject markup.

{{data.<name>.path}} — live data

Declare a data binding on the component: a name you choose, a kind from the platform's registry, and optional params. Reference it as {{data.<name>}}, or drill in with a dotted path such as {{data.account.accountNumber}}. Bindings resolve on the server before the iframe mounts, so the component never holds a token, and every resolution is role-checked and audited.

The built-in kinds are viewer, currentAccount, invoices, payments, subscriptions, usage, productCatalog, conversations, and csrAccountList. viewer and productCatalog are public-callable; the rest require a signed-in viewer plus a scope check.

For account-scoped kinds, pass {"$ctx":"account.id"} as the accountId param rather than a literal id — the resolver fills it from the authenticated viewer, so a customer can only ever read their own record.

Gotchas

  • productCatalog returns no prices. Its shape is { id, name, description, category } only. You cannot print a live price from it — resolve pricing through the runtime bridge instead.

  • A failed binding substitutes an empty string, not an error message. A blank card region usually means the binding was rejected, not that the data is empty.

  • {{data.…}} prints one value. For arrays — a plan list, an invoice table — call PeakBindings.query("name", params) or window.PC.call("data", "pricing-resolve" | "product-set-resolved", …) from inside the component. Direct fetch is blocked by the sandbox.

Last updated

Was this helpful?