Transforms — run your own JavaScript against your site

A transform is a small piece of JavaScript you run against your site's data. Use one when the template tags ({% list %}, {% query %}, …) can't express what you need — an aggregation, a multi-hop lookup, a shape the templates don't produce, or a one-off calculation. Run code ad hoc to see a result, or save it as a reusable transform that fires on a trigger.

Transforms run in a hardened sandbox. They can read your site's graph and make outbound HTTP calls, but they cannot touch the host — no filesystem, no environment secrets, no require.


The sandbox

Your code runs as the body of an async function, so top-level await and return both work. The value you return comes back as result. Available inside the sandbox:

The default execution ceiling is 5000 ms; pass timeoutMs to change it. Every run returns { ok, result, logs, error, durationMs }.

// Count posts by category.
const r = await graph.query(
  "MATCH (p:post) RETURN p.category AS category, count(p) AS n ORDER BY n DESC"
);
return r.data;

Run code inline

The fastest path — run a snippet and get the result back, without saving anything:

curl -X POST https://app.staticowl.com/api/transforms/_run \
  -H "Authorization: Bearer $STATICOWL_API_KEY" \
  -H "X-Site-Id: site:mysite" \
  -H "Content-Type: application/json" \
  -d '{"code":"return (await graph.query(\"MATCH (p:post) RETURN count(p) AS n\")).data[0].n;"}'

Inline runs are recorded in the run history (as an inline run) so you can look back at what you ran.


Save a reusable transform

Save a transform to run it again, or to have it fire automatically on a trigger:

curl -X POST https://app.staticowl.com/api/transforms \
  -H "Authorization: Bearer $STATICOWL_API_KEY" \
  -H "X-Site-Id: site:mysite" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "popular-posts",
    "description": "Top posts by view count",
    "code": "return (await graph.query(\"MATCH (p:post) RETURN p.slug AS slug ORDER BY p.views DESC LIMIT 5\")).data;",
    "trigger": "manual"
  }'

A saved transform has a stable id (e.g. transform:ab12cd34). Run it by id:

curl -X POST https://app.staticowl.com/api/transforms/<id>/run \
  -H "Authorization: Bearer $STATICOWL_API_KEY" -H "X-Site-Id: site:mysite" \
  -H "Content-Type: application/json" -d '{"input": {"limit": 5}}'

Triggers — when a transform fires

Trigger Fires
manual Only when you run it (the default).
onSave After a content item of the scoped type is saved.
beforeRender During a build, to shape data before templates render.
onPublish When a build is published.

Narrow an automatic trigger with scope: scopeType limits it to one content type, and scopeTemplate limits it to one template. A transform with no scope applies to all types (and all templates). Scope is ignored for manual transforms. Disable a transform (enabled: false) to stop it firing without deleting it.


Review runs

Each run of a saved transform is recorded with its result, captured logs, error (if any), and duration. The most recent runs are kept per transform.

curl "https://app.staticowl.com/api/transforms/<id>/runs?limit=20" \
  -H "Authorization: Bearer $STATICOWL_API_KEY" -H "X-Site-Id: site:mysite"

API and MCP reference

All transform routes require a write role (admin) on the site.

Route MCP tool Purpose
POST /api/transforms/_run transforms_run_inline Run a snippet ad hoc.
GET /api/transforms transforms_list List saved transforms (name, trigger, enabled).
GET /api/transforms/{id} transforms_get Get one, including its code.
POST /api/transforms transforms_create Save a new transform.
PUT /api/transforms/{id} transforms_update Update one (send only the fields you change).
POST /api/transforms/{id}/run transforms_run Run a saved transform.
GET /api/transforms/{id}/runs transforms_runs List recent run records.
DELETE /api/transforms/{id} transforms_delete Delete a transform and its run history.

Deleting a transform that has a live trigger (onSave / beforeRender / onPublish) stops that behavior on the next save, build, or publish — make sure no template or content flow still depends on it. To pause one instead, update it with enabled: false.


See also

Related docs

Resolved at build time via {% similar %} — cosine similarity over embeddings, not tag overlap. Zero arguments.

HTTP API
64% match
Zero to deployed — the 5-minute quickstart
61% match
AI features
60% match