canvas.yaml reference
Every canvas is one file in your tenant configuration repository:
canvases/{tenant}/{canvas-id}.canvas.yaml
Merging a change to this file makes it live within five minutes — no deployment, no Salesforce change set. This page documents every block. The example threads through a departure rooming list from the Meridian Travel Co. reference implementation.
Full shape
canvas:
id: rooming-list # URL-safe identifier; must match the filename
name: Rooming List # display name
version: "1.0.0"
description: Departure rooming list with per-hotel tables and send actions
connection:
tenant: meridian # owning tenant
environment: uat # default org; overridden by org resolution at runtime
queries: # named SOQL, executed in parallel
passengers: |
SELECT KaptioTravel__FirstName__c, KaptioTravel__LastName__c,
KaptioTravel__AllocationId__c,
KaptioTravel__Itinerary__r.KaptioTravel__Itinerary_No__c
FROM KaptioTravel__Passenger__c
WHERE KaptioTravel__Itinerary__r.Package_Departure__c = :recordId
AND KaptioTravel__Itinerary__r.KaptioTravel__Status__c = 'Booking Confirmed'
ORDER BY KaptioTravel__Itinerary__r.KaptioTravel__Itinerary_No__c
departure: |
SELECT Id, Name, KaptioTravel__Date__c, KaptioTravel__Package__r.Name
FROM KaptioTravel__PackageDeparture__c
WHERE Id = :recordId
LIMIT 1
data_sources: # optional REST sources alongside SOQL
weather:
provider: rest
url: "https://api.example.com/forecast?location={{recordId}}"
result_path: daily # optional dot-path into the JSON response
transform: | # JavaScript view-model precomputation (optional)
const byRoom = new Map();
for (const p of data.passengers) {
const key = p.KaptioTravel__Itinerary__r.KaptioTravel__Itinerary_No__c
+ '-' + (p.KaptioTravel__AllocationId__c || '');
if (!byRoom.has(key)) byRoom.set(key, []);
byRoom.get(key).push(p);
}
return {
rooms: [...byRoom.values()].map((travelers, i) => ({ no: i + 1, travelers })),
departure: data.departure[0] || null,
};
actions: # buttons the rendered HTML can trigger (optional)
compose_final:
type: compose_email
render_canvas: rooming-list-email-final
target_record_id: "{{formData.targetrecordid}}"
parameters:
recordId: "{{recordId}}"
template:
engine: handlebars # handlebars | html
style: slds # slds | standalone | minimal
html: |
<h2>Rooming List for {{departure.KaptioTravel__Package__r.Name}}</h2>
<table>
<tr><th>Room</th><th>Travelers</th></tr>
{{#each rooms}}
<tr>
<td>{{this.no}}</td>
<td>{{#each this.travelers}}{{this.KaptioTravel__FirstName__c}} {{this.KaptioTravel__LastName__c}}{{#unless @last}}, {{/unless}}{{/each}}</td>
</tr>
{{/each}}
</table>
<button class="slds-button slds-button_neutral"
data-action="compose_final" data-compose="true"
data-targetrecordid="{{departure.Id}}">Send Final Rooming List</button>
Block by block
canvas
| Field | Required | Notes |
|---|---|---|
id | yes | Lowercase alphanumeric with dashes. Forms the render URL. |
name | yes | Shown as the page title and in tooling. |
version | no | Semantic version string, defaults to 1.0.0. |
description | no | Free text; use it to record what the canvas replaces or serves. |
connection
Declares the owning tenant and a default Salesforce environment. At runtime, requests arriving through the org route (/canvas/org/{orgId}/...) resolve the environment from your blueprint's salesforce_environments, which overrides this default. The block exists so direct tenant-route requests and local tooling have a sensible fallback.
queries
A map of name → SOQL. All queries run in parallel, server-side, through the tenant's stored OAuth connection.
- Bind variables —
:recordId(and any other parameter passed by the embedding component or an action) is substituted with proper escaping before execution. A request that leaves a bind unresolved is rejected withUNRESOLVED_QUERY_BINDSrather than silently querying wrong data. - Results — each query's rows appear in the template context under its name. Relationship fields arrive as nested objects, exactly as the Salesforce REST API returns them:
record.KaptioTravel__Itinerary__r.Name. - Failure isolation — a failing query does not fail the canvas. Its name is recorded in
_failedQueries(with the message in_queryErrors) and the rest of the data renders. Templates can surface this to the user. - A single
query:(string) is also supported for one-query canvases; its rows land inrecords.
data_sources
Optional REST sources fetched alongside the SOQL, for enriching a canvas with non-Salesforce data. {{recordId}}, {{tenant}}, {{sfEnvironment}}, and platform URL placeholders are substituted into the url. result_path optionally selects a subtree of the JSON response. Failures degrade to an empty result, never a broken canvas.
transform
The body of a JavaScript function, executed server-side, receiving the full data object (all query results plus context keys). Its purpose is view-model precomputation: grouping, sorting, date formatting, flags, derived strings — so the Handlebars template stays presentation-only. Whatever object the transform returns is merged into the template context.
Context keys available to the transform:
| Key | Contents |
|---|---|
data.<queryName> | Rows for each named query |
data._tenant, data._sfEnvironment | Resolved connection context |
data._config | The canvas's config: defaults merged with your blueprint's canvas_config overrides — the supported way to vary display options per tenant without forking the canvas |
data._failedQueries, data._queryErrors | Per-query failure information |
| Request parameters | recordId and any other query parameters, exposed after the transform runs |
Keep transforms deterministic and side-effect free. They have no network or filesystem access.
actions
Buttons in the rendered HTML trigger named actions. See Actions for the two action types (api_call, compose_email), parameter substitution, and button conventions.
template
| Field | Values | Notes |
|---|---|---|
engine | handlebars (default), html | html renders the string verbatim. |
style | slds (default), standalone, minimal | slds wraps output with the Salesforce Lightning Design System stylesheet — use for record-page widgets. standalone emits your HTML with no wrapper — use for email bodies and externally embedded surfaces. |
html | string | The template. Include a <style> block for widget-specific CSS. |
Handlebars helpers available in templates include comparison (eq, ne, gt, lt, ifCond), logic (and, or, not), array (length, first, last, join, findByField), string (uppercase, truncate, contains), math (add, multiply), and formatting (formatDate, formatCurrency, formatNumber) helpers.
mockData
Optional records used as a fallback when a single-query canvas returns no rows — useful for demo tenants and previews. Leave it out for operational canvases, so an empty state renders honestly instead of showing sample data.