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:
input— the value you passed in asinput(any JSON).graph.query(cypher, params?)— read-only Cypher against this site's graph (async —awaitit). Rows come back on.data, e.g.(await graph.query("MATCH (n) RETURN count(n) AS c")).data[0].c.utils.slugify(s),utils.iso(date?),utils.now()— pure helpers.utils.http.get(url, opts?),.post(url, body, opts?),.request(method, url, body, opts?)— outbound HTTP, with a 10s timeout and a 1 MB response cap.console.log|warn|error(...)— captured into the run'slogs.
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
- Template language reference — the
{% list %}and{% query %}tags a transform complements. - Content modeling & field types — the content types and fields your Cypher queries read.
- Connect your AI assistant — connect over MCP to drive transforms from an agent.