-
Notifications
You must be signed in to change notification settings - Fork 0
Home
Self-hosted Writ: record a browser session once, get a callable API, an MCP tool, or a monitor. This wiki is for running it. For what it is, start at the README.
-
Quickstart —
gen-env.sh,docker compose up, openlocalhost:8000. Two minutes, no domain needed. - Connecting agents — the coordinator runs no browsers itself, so nothing executes until one agent is connected. Start here if the app is up but a run never starts.
- Production deployment — putting it on a public domain with HTTPS, upgrading it, and what to do when it breaks.
These live in the repo so they version with the code:
-
docs/DEPLOYMENT.md— every production setting, bring-your-own-proxy, rate limiting, backups. -
.env.example— the annotated configuration reference. Every variable is documented where it is defined. -
SECURITY.md— the security model and how to report a vulnerability. -
CHANGELOG.md.
A crawl row is one run: its settings live on the row and its id dies with that run. So a crawl could not be exposed as an API the way a workflow can — the URL would change every time you re-crawled — and "crawl that site again with the same settings" meant refilling the form.
A saved crawl fixes that. Save a crawl's settings once and you get a permanent endpoint plus a history, which is what makes freshness answerable:
# Save the settings a crawl actually ran with (the server reads the row, so nothing
# is lost — a crawl's status view does not echo every knob).
curl -X POST https://writ.example.com/api/crawl/definitions \
-H "Authorization: Bearer wt_..." -H 'Content-Type: application/json' \
-d '{"name":"Docs","from_crawl_id":41,"default_max_age_seconds":86400}'
# Then call it. Within the window you get the pages it already collected — instantly,
# with nothing crawled. Older than that, it re-crawls with those exact settings.
curl -X POST https://writ.example.com/api/crawl/definitions/docs/run \
-H "Authorization: Bearer wt_..." -H 'Content-Type: application/json' \
-d '{"max_age": 86400}'Every answer says which you got: _cache.hit and _cache.age_seconds. A fresh crawl
answers 202 with a crawl id to poll, because a whole-site crawl outlives an HTTP
request; "wait": true blocks instead, and a 504 still carries the crawl id so the
work stays collectable.
Two rules keep a reused answer trustworthy: only a completed run qualifies, and a run that fetched nothing never does — that is the shape a host that 403s every page produces, and it must not pin an empty answer behind a day-long window.
The key needs crawl:execute to run one and crawl:read to read its data. Mint one in
Settings → API keys, or use the Call this crawl button on any crawl's page, which
saves the settings and mints the key for you. Assistants get the same thing as MCP
tools — writ_saved_crawls, writ_run_saved_crawl, writ_saved_crawl_data.
Two routes run a workflow over HTTP, and they differ in what authenticates the call — pick by how the URL will be handed out.
| Route | Credential | Use it for |
|---|---|---|
POST /api/webhooks/hook/{token} |
The unguessable token is the credential, plus a mandatory HMAC signature (X-Writ-Signature + X-Writ-Timestamp) |
Third-party senders (Stripe, GitHub) that sign their payloads |
POST /api/v1/webhooks/{custom_path} |
An API key with triggers:execute, scoped to the target workflow |
Your own code, where a readable URL matters more than an opaque one |
A custom path is human-chosen (myapp/getUser), so it is guessable and cannot be its
own credential — that route requires a key and fails closed without one. A signature is
optional there, and still verified if you send one.
curl -X POST https://writ.example.com/api/v1/webhooks/myapp/getUser?wait=true \
-H "Authorization: Bearer wt_..." -H 'Content-Type: application/json' \
-d '{"user_id": 42}'?wait=true returns the workflow's extracted data in the same call (&timeout= bounds
it, 10–300s); without it you get the dispatch record and poll the run. Both routes are
rate-limited to 30 calls/minute.
Set a custom path when creating the trigger, or read custom_webhook_path back from
GET /api/webhooks/triggers. Recording an API-style workflow mints one endpoint per
function automatically and returns them as endpoints on the create response.
./scripts/gen-env.sh # fresh secrets in .env
docker compose up -d --build # local, loopback only
./scripts/deploy.sh writ.example.com you@here.com # public, with HTTPS- Discussions — questions and "how do I…".
-
Issues — bugs. Include
docker compose logs --tail 50 coordinator. - Security problems: use private vulnerability reporting, never a public issue.
usewrit/writ · AGPL-3.0-only · Issues · Discussions · Report a vulnerability
Getting started
Using it
Integrations
Operations
Reference