Skip to content

Runtime overview

Code you write — triggers, jobs, controllers, views — is called guest code. It runs in a sandboxed subprocess with no filesystem, no network, and no Deno global. Everything it can reach comes through an explicit module.

That is the tradeoff: a narrow surface, but nothing you write can take the platform down or read another workspace’s data.

SpecifierWhat it gives you
platform:stdlibsql, tableRef, PlatformController, createController, trigger types
platform:entitiesGenerated classes for your entities
platform:triggersBeforeInsert, AfterUpdate, and the rest
platform:scheduled-jobThe ScheduledJob interface
platform:schemaRuntime schema inspection
platform:notificationsnotify.send
platform:emailemail.send
platform:inbound-emailInboundEmailHandler for received mail
platform:aiText generation, structured output, embeddings, chat
platform:blobFile upload and attachment
platform:processPlatformProcess
platform:workflow, platform:workflow-stepsMulti-step workflow definitions
platform:zodZod, for schema validation

platform:entities only exists once your entities have been generated — run polymesa types to get local definitions your editor can see.

KindTriggerDirectory
TriggersA record changesautomations/<Entity>/
Scheduled jobsA cron scheduleguest_files/cron_jobs/
ControllersA view calls a methodguest_files/controllers/
ViewsA user opens a pageguest_files/views/

Triggers receive arrays. A save of one record and a save of five hundred call the same method — the only difference is the length. Code written for a single record silently becomes N round trips under an import.

Query once for the batch, build a Map, then loop:

const { rows } = await sql.query<Component>(`
SELECT ${co.component_id}, ${co.default_priority}
FROM ${co}
WHERE ${co.component_id} IN (${sql.delimit(ids)})
`);
const byId = new Map(rows.map((c) => [c.component_id, c]));
for (const issue of records) {
issue.priority ??= byId.get(issue.component_id)?.default_priority ?? "P3";
}

Guest code runs as the user who triggered it, and plain sql is filtered by sharing rules accordingly. Scheduled jobs have no triggering user and run as SYSTEM.

sql.systemMode opts out of that filtering. It is the one place where a mistake becomes a data leak, so prefer plain sql wherever the operation has a user behind it.