Tabs & Spaces Developers

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

  1. 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.
  2. The workspace answers with the context, sent to your origin only.
  3. From then on, your page sends to the workspace's origin (from the context message's event.origin), and ignores messages from anywhere else.
  4. 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.
  5. 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

json
{ "tands": 1, "type": "hello" }

token

Asks for a fresh token. id is yours (up to 64 characters), and the answer carries it back.

json
{ "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.

json
{
  "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.

json
{
  "tands": 1,
  "type": "event",
  "id": "r2",
  "name": "deal_won",
  "data": { "title": "Globex", "amount": 12000 }
}

From the workspace

context

json
{
  "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

json
{
  "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.

json
{ "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).

json
{ "tands": 1, "type": "error", "id": "r2", "code": "INVALID", "message": "name is required." }

theme

json
{ "tands": 1, "type": "theme", "theme": "dark" }

Rules

  • Only accept messages whose event.source is window.parent. After the first context, also check that event.origin is 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.