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.
The modules
Section titled “The modules”| Specifier | What it gives you |
|---|---|
platform:stdlib | sql, tableRef, PlatformController, createController, trigger types |
platform:entities | Generated classes for your entities |
platform:triggers | BeforeInsert, AfterUpdate, and the rest |
platform:scheduled-job | The ScheduledJob interface |
platform:schema | Runtime schema inspection |
platform:notifications | notify.send |
platform:email | email.send |
platform:inbound-email | InboundEmailHandler for received mail |
platform:ai | Text generation, structured output, embeddings, chat |
platform:blob | File upload and attachment |
platform:process | PlatformProcess |
platform:workflow, platform:workflow-steps | Multi-step workflow definitions |
platform:zod | Zod, for schema validation |
platform:entities only exists once your entities have been generated — run
polymesa types to get local definitions your editor
can see.
What runs where
Section titled “What runs where”| Kind | Trigger | Directory |
|---|---|---|
| Triggers | A record changes | automations/<Entity>/ |
| Scheduled jobs | A cron schedule | guest_files/cron_jobs/ |
| Controllers | A view calls a method | guest_files/controllers/ |
| Views | A user opens a page | guest_files/views/ |
Bulk by default
Section titled “Bulk by default”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";}Identity
Section titled “Identity”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.
- Entities — the generated classes
- Querying with SQL — reads, writes, and safety