You write content in StaticOwl as drafts. Nothing you type is on the web until you build — the build compiles your content + templates into plain HTML and deploys it to a host. This guide covers the mental model, the environments every site ships with, how to run a build (dashboard, API, and MCP), how to validate before you publish, how to check status and cancel, and how a custom domain takes over from the preview host at cutover.
The mental model
edit content (drafts) → publish to an environment → build → live output at a host
- Drafts live in your site's graph. Editing content, adding a template, or tweaking a type changes nothing on the web.
- Publishing (
content_publish) stamps a content item as live for a given environment. It still doesn't put bytes on a CDN. - Building compiles every published item + template into static artifacts and
deploys them. The build's response hands you back the
publicUrlwhere the output is served.
A build is always scoped to one environment. You build dev, you build
prod — each is a separate compile + deploy to a separate host.
Environments and their preview hosts
Every site is created with three environments:
| id | name | auto-builds on publish | who can publish |
|---|---|---|---|
dev |
Dev | yes | admin, editor, publisher |
staging |
Staging | no | admin, publisher |
prod |
Production | no | admin, publisher |
Each environment serves at its own preview host, one per environment:
https://<slug>-<env>.preview.staticowl.com
So a site with slug nightowl gets nightowl-dev.preview.staticowl.com and
nightowl-prod.preview.staticowl.com. The preview hosts are always live — no DNS
or TLS setup on your side. Building an environment updates that environment's
preview host and nothing else.
Tip: build
devfreely while you iterate. Promote toprodonly when it looks right — aprodbuild requires that your content is published toprodfirst.
Running a build
Dashboard
Open Site → Builds → Build now, pick the environment, and choose a mode. When
the build finishes, the panel links you to the published publicUrl.
API
# Trigger a build for an environment (default mode: incremental, default env: dev)
curl -X POST https://app.staticowl.com/api/build \
-H "Authorization: Bearer $STATICOWL_API_KEY" \
-H "X-Site-Id: site:mysite" \
-H "Content-Type: application/json" \
-d '{"mode":"full","env":"dev"}'
# Long build? Run it async — returns 202 + a statusUrl immediately, build runs in the background
curl -X POST https://app.staticowl.com/api/build \
-H "Authorization: Bearer $STATICOWL_API_KEY" \
-H "X-Site-Id: site:mysite" \
-H "Content-Type: application/json" \
-d '{"mode":"full","env":"prod","async":true}'
A successful synchronous build responds with the deploy result and every page it emitted:
{
"ok": true,
"envId": "dev",
"buildId": "build:1699999999999:ab12cd",
"artifacts": 42,
"totalBytes": 1048576,
"durationMs": 5123,
"publicUrl": "https://mysite-dev.preview.staticowl.com",
"deploy": { "ok": true, "bucket": "staticowl-sites", "filesUploaded": 42, "serveCheck": { "ok": true, "status": 200 } },
"pages": [ { "path": "/index.html", "type": "page", "title": "Home", "bytes": 8123 } ],
"transformErrors": [],
"error": null
}
An async build responds 202 with { ok, buildId, envId, status: "running", statusUrl } —
poll statusUrl until the status flips.
Build modes:
| mode | what it does |
|---|---|
incremental |
rebuild only artifacts impacted by recent changes (default) |
full |
rebuild every page |
auto |
pick full vs. incremental based on the change surface |
validate |
dry-run — compile in memory, return the artifact list + transform errors, deploy nothing, record no build |
MCP (AI agents)
build_run { mode: "full", env: "dev" }
build_run { mode: "full", env: "prod", async: true }
build_run returns publicUrl and (in a real build) pages[]. Related build
tools:
| Tool | Required | What it does |
|---|---|---|
build_run |
— | Trigger a build (mode, env, async) |
build_status |
buildId |
Poll one build by id — running → ok/error |
build_last |
— | Status + stats of the single most recent build |
build_history |
— | List recent builds (filter with env, cap with limit) |
build_impact |
nodeIds |
Dry-run blast-radius: which pages would rebuild |
build_rollback |
buildId |
Revert the live site to a prior build (destructive) |
Agents need a write role (admin, editor, or publisher) on the site.
Validate before you publish
validate mode is the cheap sanity check. It runs the full compile in memory and
returns the artifact list plus any template/transform errors, but deploys
nothing and records no build — so it's the fast way to catch a broken template
before spending a real build slot.
curl -X POST https://app.staticowl.com/api/build \
-H "Authorization: Bearer $STATICOWL_API_KEY" \
-H "X-Site-Id: site:mysite" \
-H "Content-Type: application/json" \
-d '{"mode":"validate","env":"dev"}'
build_run { mode: "validate", env: "dev" }
The response is distinct from a real build — note validate: true, a null
buildId, and a per-artifact list:
{
"ok": true,
"validate": true,
"buildId": null,
"envId": "dev",
"artifactCount": 42,
"artifacts": [ { "path": "/index.html", "type": "page", "id": "page:home", "title": "Home", "bytes": 8123 } ],
"totalBytes": 1048576,
"durationMs": 3200,
"transformErrors": [],
"note": "Validate mode — nothing deployed, no BuildEvent recorded."
}
If transformErrors is non-empty, fix your templates before running the real
build. (?dryRun=1 on the query string is an equivalent trigger.)
Checking status and cancelling
Status
Every real build gets a buildId. Poll it:
curl https://app.staticowl.com/api/build/status/build:1699999999999:ab12cd \
-H "Authorization: Bearer $STATICOWL_API_KEY" -H "X-Site-Id: site:mysite"
build_status { buildId: "build:1699999999999:ab12cd" }
The status endpoint returns status (running | ok | error | cancelled)
along with phase, artifacts, bytes, durationMs, publicUrl, and error.
Keep polling while status is running. To see the latest build or a list, use
GET /api/build/last and GET /api/build/history?env=prod&limit=20.
Cancelling
Only one full/incremental build can run per site + environment at a time — a
second POST /api/build for the same pair bounces with 409 build_in_progress
and points you at the running build. If a build gets stuck, release its lock:
# Either verb works — both hit the same handler
curl -X DELETE https://app.staticowl.com/api/build/build:1699999999999:ab12cd \
-H "Authorization: Bearer $STATICOWL_API_KEY" -H "X-Site-Id: site:mysite"
curl -X POST https://app.staticowl.com/api/build/build:1699999999999:ab12cd/cancel \
-H "Authorization: Bearer $STATICOWL_API_KEY" -H "X-Site-Id: site:mysite"
Cancel releases the lock (so a fresh build can start) and marks the build
cancelled. It does not kill compile work already in flight — that finishes
on the box on its own. It's a way to unstick the queue, not an abort button.
Custom domains
Preview hosts are enough to launch and share. When you want your own domain, that
lives in the site's publishConfig:
| field | meaning |
|---|---|
customDomain |
your domain (e.g. www.example.com) — drives publicUrl |
distributionId |
the CloudFront distribution serving the domain (null = shared preview) |
bucket |
a BYO S3 bucket (null = shared staticowl-sites) |
bucketRegion |
region of a BYO bucket |
prefix |
key prefix inside the bucket (defaults to the site slug) |
assumeRoleArn |
IAM role to assume for a BYO bucket |
Who gets one: custom domains are a Pro / Business feature. The Free plan is limited to one site on a preview subdomain, with no custom domain — attempting to attach one returns a plan-gate error.
How cutover works: once your domain is configured, StaticOwl provisions its
CloudFront distribution and attaches the edge redirect/rewrite function to it.
From that point a build sets publicUrl to https://<customDomain> instead of
the preview host, and your redirects fire on the live domain
automatically — the redirect table is keyed on the custom-domain host the moment
the domain is set, so a rule you create applies to both the preview hosts and the
live domain. No per-redirect action is needed at cutover.
Deploy targets
Where a build's artifacts go is a separate, operator-level choice controlled by
STATICOWL_DEPLOY_TARGET: plain static paths on S3 (static-paths, the default
"host anywhere" mode), a managed atomic-release CDN (manifest-pointer), a mirror
to a Git repo you own (github), or both. Most sites never need to touch this —
the default serves the preview hosts and custom domains described above.
See Deploy targets for the full comparison and the env vars each mode needs.
Gotchas & tips
- Builds are per-environment. Building
devnever touchesprod, and vice versa. Publish to the target env first, then build it — aprodbuild needs content published toprod. - Always
validatebefore a realprodbuild. It's free, records nothing, and surfaces template/transform errors in seconds. A non-emptytransformErrorsis your signal to fix templates first. - One build per site + env at a time. A concurrent
POST /api/buildreturns409 build_in_progresswith a pointer to the running build. This is deliberate — two overlapping full builds on the same site are dramatically slower than running them one after another. - Cancel unsticks; it doesn't abort.
DELETE /api/build/:buildId(orPOST /api/build/:buildId/cancel) frees the lock and marks the buildcancelled, but compile work already running continues to completion on the box. - Large sites: use
async: true. Multi-thousand-page builds can exceed the gateway timeout; async returns astatusUrlimmediately and you poll it. The status response carries a livephase+ partialartifactscount so you can watch progress. incrementalis the default and rebuilds only impacted artifacts. Reach forfullafter a template or theme change that touches every page; usebuild_impactto preview a change's blast radius first.- Status 404 after a build? Either the
buildIdis wrong or the build's record failed to persist — the endpoint'shintfield tells you which. The output may still be live; check thepublicUrl.
See also
- Zero to deployed — the 5-minute quickstart from no account to a live page.
- Deploy targets — S3, GitHub, managed-CDN, and
both, with the env vars each needs. - Redirects — real HTTP 301/302s, and how they fire on preview hosts and your custom domain.
- Content modeling — types, fields, and route patterns that shape what a build emits.