Team plan and above

Site platform API (`window.atrio`)

Site platform API (window.atrio)

Team plan and above. Use this for new agent-built apps that keep state in JSON (trackers, forms, timeline data).

Migrating Vincent-style Circulon review sites (copy-edit-in-place, devclient.js, /capi/comments, devlog + undo) is not this API — see CIRCULON_HOST.md. Those pages need HTML + a same-origin /api/circulon adapter on Atrio; do not rewrite them to atrio.store unless rebuilding the UX.

Sites opt in with platformApi: true (push_site sets this). The viewer injects a bridge; the iframe calls window.atrio, the parent posts to /api/site, and every call re-checks access.

Quick start

const { signedIn, email } = await atrio.user();

// Shared state for everyone allowed to edit (org members on an org-scoped link, etc.)
const doc = await atrio.store.get("circulon-timeline", { scope: "shared" });
let revision = doc.revision ?? 0;
let state = doc.value ?? { rows: [] };

state.rows.push({ id: 1, label: "Milestone A" });
const saved = await atrio.store.set("circulon-timeline", state, {
  scope: "shared",
  ifRevision: revision,
});
if (saved.error === "conflict") {
  // Another editor saved first — reload and merge, or prompt the user.
  const fresh = await atrio.store.get("circulon-timeline", { scope: "shared" });
  revision = fresh.revision;
  // ...
} else {
  revision = saved.revision;
}

// Undo last change to this key (server-side history, up to 50 steps per key)
await atrio.store.undo("circulon-timeline", { scope: "shared" });

Store (atrio.store)

MethodDescription
get(key, { scope })Returns { value, revision, deleted, actorEmail, updatedAt }. Missing key → revision 0.
set(key, value, { scope, ifRevision })Saves JSON (max 64 KB). ifRevision is required for shared writes. Returns the new revision or a conflict with the newer value.
list({ scope }){ items: [{ key, value, revision }] } (up to 1000 keys; excludes tombstones).
revisions(keys, { scope })Lightweight { items: [{ key, revision, deleted, updatedAt }] } for polling without downloading values.
delete(key, { scope, ifRevision })Writes a tombstone only if the current revision matches; history remains recoverable.
history(key, { scope, limit })Prior snapshots { items: [{ revision, value, at, by, deleted }] } for recovery UI.
revert(key, { scope, revision, ifRevision })Restores a historical revision as a new head if the current head matches ifRevision.
undo(key, { scope, ifRevision })Restores the previous revision with the same current-head conflict check.

Scopes and roles

  • shared — one namespace per site. Writes require a named Editor, an Artifact editor, or a legacy write-enabled link. Shared state is readable by anyone allowed to view the app, so never put secrets in it.
  • me — per signed-in user, useful for recovery drafts. Requires sign-in.
  • Viewer can read but not comment or write. Commenter can read and comment but not write. Editor can read, comment, and write shared state. A share Editor cannot publish code, change audiences, or manage the Artifact.

Limits: 64 KB per value, 1,000 live keys and 1 MB total JSON per scope-owner, and 50 historical revisions per key. API calls are rate-limited per identity and artifact.

Simultaneous edits: There is no live merge/cursor sync. Every shared set, delete, revert, and undo requires ifRevision. On conflict, keep the local draft, reload get, and reconcile. Poll revisions every few seconds and fetch only changed keys. Presence and the App history dialog appear only when collaboration is enabled; restored keys emit atrio:storechange inside the iframe.

Presence (atrio.presence)

  • heartbeat(clientId) — call every 10 seconds with a random ID per tab. Presence expires after 30 seconds.
  • list() — returns active signed-in viewers allowed to see the app.
  • leave(clientId) — best-effort departure; expiry remains authoritative.

Sources (atrio.sources)

  • refresh() imports the configured Google Sheets range and returns { values, rowsAffected }.
  • importOnOpen({ isDirty, apply, onConflict, onError }) imports on startup without overwriting unsaved edits. apply(values) runs only when local state is clean; dirty state goes to onConflict(result), and failures go to onError(result).
  • export(values) backs up the output range, clears it, and writes validated rows. If the update fails, the previous values are restored. Strings that look like formulas are escaped.

Both require shared-state write access and re-check permission on every call. An admin configures the connection in Collaboration settings with import, export, or bidirectional mode; credentials are encrypted and never returned to the app. Imports and exports have fixed spreadsheet IDs/ranges, a 1 MB payload cap, a 10-second per-connection rate limit, and an audited sync run. Sheet failures never clear local app state.

Default sync policy: import on open, use refresh() for manual re-imports, and export(values) only on an explicit user action. Shared app state is authoritative while editing. Do not run an automatic bidirectional sync loop. On open, use importOnOpen; dirty local state is never replaced automatically.

Data (atrio.data.query)

Read-only SQL over attached datasets (same DuckDB sandbox as Ask the data). Requires data access on the link.

const rows = await atrio.data.query(
  "select region, sum(amount) as total from data group by 1",
);

User (atrio.user)

{ signedIn: boolean, email: string | null }

The iframe cannot read the Atrio session cookie; this is the supported way to know who is signed in.

HTML / agent versions (separate from store)

  • Each MCP push_site / push_dashboard creates a new content version (what you see under History in the library).
  • publish_version chooses which version viewers see.
  • Undo for the whole page = restore an older content version in the library, or republish via MCP.
  • Undo for in-app edits = atrio.store.undo / revert on your state keys.

Migrating from Cloudflare / Dots

Pick one:

SourceDoc
Circulon Worker (devclient.js, /capi/*, pin comments, copy publish)CIRCULON_HOST.md (spec; host on Atrio with circulon-compat API)
New interactive app (state in JSON, no /capi)This file (window.atrio.store + MCP below)

New apps (this API):

  1. Connect MCP to https://app.atrio.cc/api/mcp; call atrio_whoami with Global Meyer workspace_id.
  2. Publish with push_site (multi-file) or push_dashboard (single HTML). Use set_audience (org for all of Meyer, private for Restricted).
  3. Move persisted state from Workers KV / localStorage into atrio.store (scope: "shared").
  4. On load: get → render; on save: set with ifRevision; wire Undo to store.undo or history + revert.
  5. Point Dots at https://app.atrio.cc/site-api and MCP_TOOLS.md.

Security

Iframe is sandboxed; only the bridge can call the API. Values are JSON only; no arbitrary server code from the site.