Reference
The frame protocol
Your page and the workspace talk with postMessage. The SDK's connect() does all of this for you. This page is for anyone who'd rather speak it directly; the example app does, in a dozen lines.
Every message is an object with tands: 1, the protocol's version.
The conversation
- Your page loads and says hello to its parent. It doesn't know the workspace's origin yet, so it uses
"*". Hello carries nothing secret. - The workspace answers with the context, sent to your origin only.
- From then on, your page sends to the workspace's origin (from the context message's
event.origin), and ignores messages from anywhere else. - Your page asks for a fresh token when it needs one, or for its data; the workspace answers each request, by its
id, or sends an error. - When the reader switches theme, the workspace sends theme.
The workspace answers hello every time your page loads, so a reload starts over with a fresh token.
From your page
hello
{ "tands": 1, "type": "hello" }token
Asks for a fresh token. id is yours (up to 64 characters), and the answer carries it back.
{ "tands": 1, "type": "token", "id": "t1" }data
A request for your app's data. request is one of the operations in Keeping data: values.get, values.set, values.delete, values.list, records.list, records.get, records.create, records.update or records.delete. The workspace checks it in full.
{
"tands": 1,
"type": "data",
"id": "r1",
"request": { "op": "records.list", "collection": "contacts", "limit": 20 }
}event
Something that happened, as your manifest's events declare it. The workspace answers with a data message ({ "ok": true }), or an error.
{
"tands": 1,
"type": "event",
"id": "r2",
"name": "deal_won",
"data": { "title": "Globex", "amount": 12000 }
}From the workspace
context
{
"tands": 1,
"type": "context",
"context": {
"user": { "id": "6acf…", "name": "Ada Lovelace" },
"workspace": { "id": "6abe…", "slug": "acme", "name": "Acme" },
"space": { "id": "uib59teu0eq9", "kind": "normal", "name": "Launch" },
"tab": { "id": "vuuxldzm", "appTabId": "contacts" },
"app": { "id": "acme-crm" },
"locale": "en"
},
"token": "eyJhbGciOiJFZERTQSIs…",
"expiresAt": "2026-10-14T10:10:00.000Z",
"theme": "light"
}token
{
"tands": 1,
"type": "token",
"id": "t1",
"token": "eyJhbGciOiJFZERTQSIs…",
"expiresAt": "2026-10-14T10:20:00.000Z"
}data
The answer to a data request: { value }, { values }, { ok }, { deleted }, { record }, or { records, cursor }, by operation.
{ "tands": 1, "type": "data", "id": "r1", "result": { "records": [], "cursor": null } }error
The answer to a request the workspace couldn't fulfil, with a code when it has one (see WorkspaceError).
{ "tands": 1, "type": "error", "id": "r2", "code": "INVALID", "message": "name is required." }theme
{ "tands": 1, "type": "theme", "theme": "dark" }Rules
- Only accept messages whose
event.sourceiswindow.parent. After the first context, also check thatevent.originis the workspace's. - Ignore messages without
tands: 1, and types you don't know: later versions may add some. - The workspace ignores messages from anything but your frame, and from any origin but yours.