Sending a webhook when a record changes

This recipe shows how to fire an outbound HTTP request whenever a specific kind of record is created or updated. The pattern uses a Processor to match the write and an HttpRequest action to deliver the payload.

When to use this approach

Use this when an external system needs to know about changes happening inside Pagelove — a Slack notification when a support ticket is filed, an analytics ping when a blog post is published, a build trigger when content under a certain path changes. The outbound request is asynchronous and fire-and-forget: it does not block the response to the original writer.

Attach a Processor to the write

A Processor fires after the server has processed a request and before the response is sent. You filter it by resource path, HTTP method, and optionally a CSS selector so it only fires for the writes you care about.

The example below matches POST requests to any path under /notes/ — meaning new Note instances being created:

<div itemscope itemtype="https://pagelove.org/Processor">
  <meta itemprop="resource" content="/notes/*">
  <meta itemprop="method" content="POST">
  <meta itemprop="selector" content="[itemtype='https://example.com/Note']">
  <div itemprop="action" itemscope itemtype="https://pagelove.org/HttpRequest">
    <meta itemprop="url" content="https://example.com/webhooks/note-created">
    <meta itemprop="method" content="POST">
    <meta itemprop="content-type" content="application/json">
    <div itemprop="body" itemscope itemtype="https://pagelove.org/Sessel">
      <script itemprop="source" type="text/sessel">
        @schema Context url("https://pagelove.org/Context");
        "{\"path\": \"" + Context.request.path + "\"}"
      </script>
    </div>
  </div>
</div>

The Processor declares three filters:

The HttpRequest action

Inside the Processor, the action is an HttpRequest item. Its properties describe the outbound call:

Property Value Purpose
url https://example.com/webhooks/note-created Where to send the webhook
method POST HTTP method for the outbound request
content-type application/json Content-Type header
body Sessel expression A dynamic body built from the request context
header (none above) Any additional request header — see below

The body in this example is a Sessel expression that reads the request path from Context.request.path and wraps it in a JSON string. Any HttpRequest property can be either a static literal or a dynamic Sessel expression — see the HttpRequest reference for the full list.

Authenticating the webhook

Most real endpoints will not accept an anonymous request. Add a header for whatever the receiving service expects — a bearer token, an API key, a signing header. Each one is a Pair of key and value, and you can repeat the property as many times as you need:

<div itemprop="action" itemscope itemtype="https://pagelove.org/HttpRequest">
  <meta itemprop="url" content="https://example.com/webhooks/note-created">
  <meta itemprop="content-type" content="application/json">

  <div itemprop="header" itemscope itemtype="https://pagelove.org/Pair">
    <meta itemprop="key" content="Authorization">
    <meta itemprop="value" content="Bearer sk-live-7f3a91c4">
  </div>
</div>

Header values can be dynamic too, so a signature or a tenant identifier can be computed from the request. See header for the full rules.

Where that token lives. The credential above is stored in the document that declares the processor, and anyone your authorization rules allow to read that document can read the token. Keep processor documents that carry credentials off publicly readable paths.

Execution timing

HttpRequest actions are queued during processor execution and dispatched after the response has been sent to the original client. They do not slow down the writer's round trip. If the outbound request fails, it retries with exponential backoff up to five attempts. After all attempts are exhausted the request is abandoned — failures do not affect the client and are observable through tracing.

Matching updates and deletes

To fire on updates instead of creates, change the method filter to PUT. To fire on deletes, use DELETE. To fire on any write, list all three:

<meta itemprop="method" content="POST">
<meta itemprop="method" content="PUT">
<meta itemprop="method" content="DELETE">

The Sessel expression in the body can inspect Context.request.method to tell the receiving system which kind of change occurred.

See also