Press / to search

All documentation
docs Watch and act Automations and webhooks

Runs onWrit CloudSelf-hosted

Reference & guide

Automations & webhooks

An automation connects an event to an action: a detected change, an inbound webhook, a schedule, or a run event fires a trigger, its conditions are checked, and its actions run. Webhooks carry events in and results out — signed in both directions.

Triggers, conditions, actions

An automation is built from blocks: a trigger (the firing event), optional conditions, and one or more actions. Triggers fire on these event types — change_detected is the default:

Event typeFires when
change_detectedA monitor check finds real change against its baseline.
webhook_receivedAn external system calls your inbound hook or a custom_path door.
ai_session_started / ai_session_completedAn AI session begins or settles.
workflow_started / workflow_completedA workflow run begins or settles.
monitor_down / monitor_stale / monitor_recoveredA monitor stops answering, stops reporting, or comes back.
crawl_started / crawl_completed / crawl_failedA crawl begins, finishes, or fails.
scheduledA schedule block at the root of the automation fires on time.

Actions are notification, ai_session, workflow, crawl or create_persona — plus return_data in block form for answering a synchronous caller. When several rules match, priority is the execution order. Conditions use the same eleven operators, template context and filters documented in monitors.

Writ runs on your own accounts, with your own credentials and data, on sites you are authorized to use.

Inbound webhooks (signed)

Every inbound hook has a signing secret, assigned when the hook is created — it cannot be cleared, and unsigned calls are rejected. The signature is HMAC-SHA256, hex-encoded, over "{timestamp}." + raw body:

POST /api/webhooks/hook/{token}
Content-Type: application/json
X-Writ-Timestamp: 1718980000
X-Writ-Signature: sha256=<hex>

{ "sku": "SKU-123" }
  • X-Writ-Timestamp is mandatory; missing, invalid or older than 300 seconds is answered 401.
  • The same signature seen again within 300 seconds is rejected with 403 — a captured call cannot be replayed.
  • A GitHub-style X-Hub-Signature-256 header is accepted as an alternative to X-Writ-Signature.
  • Each hook token is rate-limited to 30 calls per 60 seconds; beyond that the call is answered 429.

custom_path doors

A webhook trigger can also claim a custom_path — a readable path of up to 100 characters, unique within your workspace — served at a stable URL and authenticated with an API key instead of a per-call signature:

POST /api/v1/webhooks/{custom_path}?wait=true&timeout=120
Authorization: Bearer wt_xxxxxxxxxxxx
Content-Type: application/json

{ "sku": "SKU-123" }
  • Authorization: Bearer with an API key is mandatory — calls without a valid key are answered 401. The path resolves inside the calling key's workspace.
  • The door's action is run_workflow (default) or check_target.
  • A run_workflow door counts against your plan's published-endpoints quota.
  • Synchronous calls: set wait_for_result on the trigger (default false) with wait_timeout 10–300 seconds (default 120) — or override per call with ?wait= and ?timeout=.

Outbound deliveries

The webhook notification channel posts results to your endpoint, signed so you can verify them. Deliveries behave predictably:

  • POST or PUT only, with User-Agent: Writ-Webhook/1.0 and X-Writ-Timestamp on every request.
  • Verify X-Writ-Signature-V1: it covers "{timestamp}." + raw body, the same material an inbound call signs, so one recipe serves both directions and a captured delivery expires with its timestamp.
  • X-Writ-Signature is sent alongside it and covers the JSON body only. It exists so handlers written before V1 keep working — do not reach for it in new code.
  • Redirects are never followed, and deliveries to private-network destinations are refused — a refused destination is not retried.
  • Up to 3 attempts with a 30-second timeout each and exponential backoff capped at 30 seconds.

What next

  • Monitors: the trigger pipeline, condition operators and template filters.
  • Workflows: what a run_workflow action executes.
  • Managed endpoints: the published-endpoints quota that custom_path doors share.

Two directions, two signatures

Webhooks flow both ways: an external system can start a Writ automation, and Writ can post back to your endpoint. Both directions are HMAC-signed — but they sign different material, so verify each the right way.

Inbound — you call Writ

POST to your hook URL with a timestamp header and a signature over "{timestamp}." + the raw body. The timestamp must be fresh (within 300 seconds) and a repeated signature is rejected as a replay.

Outbound — Writ calls you

Writ delivers a JSON payload with a signature over the body only. The timestamp travels as a header beside the signature, not inside the MAC.

Sign an inbound trigger call

Compute HMAC-SHA256 with the hook’s secret over "{timestamp}." + body, hex-encode it, and send both headers. The signature is mandatory — unsigned calls are rejected, and the secret is assigned with the hook and cannot be turned off. A GitHub-style X-Hub-Signature-256 header is accepted as an alternative.

send.py

import hashlib, hmac, json, os, time
import requests

secret = os.environ["WEBHOOK_SECRET"]        # shown when the inbound hook is created
body = json.dumps({"sku": "SKU-123"})
ts = str(int(time.time()))
sig = hmac.new(secret.encode(), f"{ts}.{body}".encode(), hashlib.sha256).hexdigest()

requests.post(
    "https://api.usewrit.app/api/webhooks/hook/{token}",
    data=body,
    headers={
        "Content-Type": "application/json",
        "X-Writ-Timestamp": ts,
        "X-Writ-Signature": f"sha256={sig}",
    },
    timeout=30,
)

Verify an outbound delivery

Take X-Writ-Signature-V1, strip the sha256= prefix, recompute HMAC-SHA256 over "{timestamp}." + raw body with your endpoint’s secret, and compare in constant time. The older X-Writ-Signature covers the body alone and is still sent for handlers written before V1 — new code should verify V1.

verify.py

import hashlib, hmac, os

def verify(raw_body: bytes, signature: str) -> bool:
    secret = os.environ["WRIT_WEBHOOK_SECRET"].encode()
    expected = hmac.new(secret, raw_body, hashlib.sha256).hexdigest()
    # Constant-time compare - never use ==
    return hmac.compare_digest(expected, signature)

Always verify before you act. Use the raw, unparsed body — parsing and re-serializing first will change the bytes and break the signature. Check X-Writ-Timestamp for freshness and skip payloads you have already processed.

What a delivery looks like

A change_detected delivery carries the event, a timestamp, the target, the selector that changed, and the content before and after with their hashes. Deliveries go out as POST or PUT, with User-Agent Writ-Webhook/1.0, and redirects are never followed.

POST /your/webhook/handler HTTP/1.1
Content-Type: application/json
User-Agent: Writ-Webhook/1.0
X-Writ-Timestamp: 1718980000
X-Writ-Signature-V1: sha256=6b3a9c…
X-Writ-Signature: sha256=9f86d0…

{
  "event": "change_detected",
  "timestamp": "2026-08-03T14:02:11Z",
  "target": { "id": 42, "url": "https://example.com/pricing", "name": "Pricing page" },
  "selector": { "css": ".price", "name": "price" },
  "change": {
    "content_before": "$129",
    "content_after": "$119",
    "content_hash": "…",
    "previous_hash": "…"
  }
}

What fires an automation

Inbound webhook

An external system posts to your signed hook URL — or to a Bearer-authenticated custom_path door.

Detected change

A monitor check finds real change against its baseline and the trigger pipeline dispatches the automation.

Run events

Workflow, AI session and crawl lifecycle events — started, completed, failed — and monitor health transitions.

See the full trigger and action model in automations and the watch-and-act pattern in monitors.

Webhooks FAQ

How are outbound deliveries authenticated?
Every delivery carries X-Writ-Signature-V1: sha256=<hex> — an HMAC-SHA256 over "{timestamp}." + the raw JSON body using your endpoint’s secret. Strip the sha256= prefix, recompute over the raw bytes, and compare in constant time. Reject anything that does not match. A body-only X-Writ-Signature is sent alongside it for handlers written before V1.
How are replays prevented on inbound calls?
The X-Writ-Timestamp header is mandatory and must be within 300 seconds — a missing, invalid or stale timestamp is answered 401. The same signature seen again within 300 seconds is rejected with 403. Each hook token is also rate-limited to 30 calls per 60 seconds (429 beyond that).
Which outbound signature header should I verify?
X-Writ-Signature-V1. It binds the timestamp into the MAC, so a captured delivery cannot be replayed once X-Writ-Timestamp goes stale, and it signs exactly the same material as an inbound call — one recipe for both directions. X-Writ-Signature covers the body alone and is kept only so handlers written before V1 keep working.
Where does the triggered workflow run?
On your own local or BYO agent with no compute charge, or on the managed cloud metered by running time. Writ runs on your own accounts, with your own credentials and data, on sites you are authorized to use.