Triggers
A trigger fires before the server processes a request. Triggers can prepare state, write to transient elements, queue outbound HTTP requests, or terminate the request chain by throwing an HTTP response.
Triggers are stored as HTML Microdata items in the database and discovered automatically via selector queries. Any document on the host can contain trigger definitions.
Quick example
This trigger fires on every PUT or POST to paths under /blog/ and sends a webhook notification:
<div itemscope itemtype="https://pagelove.org/Trigger">
<meta itemprop="resource" content="/blog/*">
<meta itemprop="method" content="PUT">
<meta itemprop="method" content="POST">
<div itemprop="action" itemscope itemtype="https://pagelove.org/HttpRequest">
<meta itemprop="url" content="https://hooks.example.com/notify">
<meta itemprop="method" content="POST">
</div>
</div>
Request lifecycle
Triggers sit at the beginning of the request lifecycle:
- Request arrives
- Trigger phase — matching triggers execute in order
- Core processing (GET, PUT, POST, DELETE, etc.)
- Processor phase — matching processors execute
- Async dispatch — queued outbound HTTP requests fire
- Response sent
If a trigger throws an HTTP response, steps 3--4 are skipped. The thrown response is returned directly.
Properties
All filter properties are optional. Omitting a filter means "match all."
resource
One or more glob patterns. The trigger fires only when the request path matches at least one pattern.
<meta itemprop="resource" content="/blog/*">
<meta itemprop="resource" content="/news/*">
<meta itemprop="resource" content="/announcements/*">
Multiple resource values are OR'd — any match is sufficient.
method
HTTP methods to match. The trigger fires only when the request method matches one of the listed values. Matching is case-insensitive. GET implies HEAD.
<meta itemprop="method" content="PUT">
<meta itemprop="method" content="POST">
<meta itemprop="method" content="DELETE">
selector
A CSS selector. The trigger fires only when the element targeted by the request appears in the set of elements selected by this selector.
Both the trigger's selector and the request's selector are resolved against the actual document (semantic matching). If the request does not carry a selector (e.g. a plain GET /page), the selector filter is not evaluated and the trigger matches as if the filter were omitted.
<meta itemprop="selector" content="article">
when
A Sessel or JavaScript expression that gates execution. If the expression evaluates to a falsy value (null, false, 0, empty string), the trigger is skipped. Omitting when means the trigger fires whenever its declarative filters match.
<div itemprop="when" itemscope itemtype="https://pagelove.org/Sessel">
<script itemprop="source" type="text/sessel">
@schema Context url("https://pagelove.org/Context");
Context.request.headers["Authorization"] != null
</script>
</div>
The same gate written in JavaScript uses the https://pagelove.org/JavaScript/Module typed item instead — dispatch is by itemtype, not by the <script> tag:
<div itemprop="when" itemscope itemtype="https://pagelove.org/JavaScript/Module">
<script type="module" itemprop="source">
export default function(ctx) {
return ctx.request.method === "PUT";
}
</script>
</div>
The module's default export is called with the request context as its first positional argument — see Context below. this is not bound to anything meaningful for a when gate.
action
Actions to execute when the trigger fires. Actions execute in document order. Three action types are supported. A trigger needs at least one action or at least one otherwise (see below) — a trigger with neither is skipped:
- Sessel — a synchronous expression with access to
Context.request - JavaScript — a synchronous
export default function(ctx)module with access to the same request context (see Context) - Outbound HTTP request — an asynchronous outbound call, queued for dispatch after the response is sent
<div itemprop="action" itemscope itemtype="https://pagelove.org/JavaScript/Module">
<script type="module" itemprop="source">
export default function(ctx) {
if (ctx.request.path.startsWith("/admin/")) {
throw {
schema_url: "https://pagelove.org/HTTPResponse",
status: 403,
message: "Admin area is restricted"
};
}
return true;
}
</script>
</div>
A JavaScript action's return value is discarded — only side effects (an outbound request queued elsewhere in the chain, or a thrown HTTPResponse) have any effect. See Chain termination for the throw shape, and Writing from triggers for a capability JavaScript actions do not have.
<!-- Sessel action -->
<div itemprop="action" itemscope itemtype="https://pagelove.org/Sessel">
<script itemprop="source" type="text/sessel">
@schema Context url("https://pagelove.org/Context");
Context.request.headers["Authorization"] != null
</script>
</div>
<!-- Outbound HTTP request action -->
<div itemprop="action" itemscope itemtype="https://pagelove.org/HttpRequest">
<meta itemprop="url" content="https://hooks.example.com/notify">
<meta itemprop="method" content="POST">
</div>
otherwise
One or more actions to execute when when evaluates to a falsy value, instead of action. Same action types, same document-order execution, same Context. otherwise is only meaningful alongside a when gate — if when is omitted, otherwise is never evaluated, since there is no falsy case for it to handle.
<div itemprop="when" itemscope itemtype="https://pagelove.org/Sessel">
<script itemprop="source" type="text/sessel">
@schema Context url("https://pagelove.org/Context");
Context.request.headers["Authorization"] != null
</script>
</div>
<div itemprop="action" itemscope itemtype="https://pagelove.org/Sessel">
<script itemprop="source" type="text/sessel">1</script>
</div>
<div itemprop="otherwise" itemscope itemtype="https://pagelove.org/Sessel">
<script itemprop="source" type="text/sessel">
@schema HTTPResponse url("https://pagelove.org/HTTPResponse");
throw new HTTPResponse { status: 401, body: "Authorization required" }
</script>
</div>
This is a shorthand for the common "require X, otherwise reject" shape — it saves inverting the when condition and repeating it, and keeps the accepted and rejected paths next to each other in the markup.
Context
Sessel expressions in triggers have access to Context.request:
| Property | Type | Description |
|---|---|---|
Context.request.method |
String | HTTP method (GET, PUT, POST, etc.) |
Context.request.path |
String | Request path |
Context.request.headers |
Map | Request headers |
Context.request.query |
Map | Query string parameters |
Context.request.body |
String | Request body (raw string) |
Context.request.rawBody |
String | Request body as raw UTF-8 bytes |
Context.response is not available during trigger execution — the response does not exist yet.
JavaScript when/action modules receive the equivalent request context as their first positional argument (conventionally named ctx):
| Property | Type | Description |
|---|---|---|
ctx.request.method |
String | HTTP method (GET, PUT, POST, etc.) |
ctx.request.path |
String | Request path |
ctx.request.headers |
Object | Request headers |
ctx.request.host |
String | The host name |
ctx.request.query |
Object | Query string parameters |
ctx.request.body |
String | Request body, when available |
ctx.request.auth |
Object | Authentication claims, when available |
Chain termination
A Sessel action can terminate the trigger chain by throwing an HTTPResponse. The server does not process the request — the thrown response is returned directly to the client.
<div itemscope itemtype="https://pagelove.org/Trigger">
<meta itemprop="method" content="PUT">
<meta itemprop="method" content="POST">
<meta itemprop="method" content="DELETE">
<div itemprop="action" itemscope itemtype="https://pagelove.org/Sessel">
<script itemprop="source" type="text/sessel">
@schema Context url("https://pagelove.org/Context");
@schema HTTPResponse url("https://pagelove.org/HTTPResponse");
if (Context.request.headers["Authorization"] == null) {
throw new HTTPResponse {
status: 403,
message: "<p>Forbidden</p>"
}
}
</script>
</div>
</div>
This is the mechanism for request guards, authentication checks, and access control at the trigger level.
A JavaScript action terminates the chain the same way, by throwing a plain object with schema_url (or itemtype) set to https://pagelove.org/HTTPResponse:
<div itemprop="action" itemscope itemtype="https://pagelove.org/JavaScript/Module">
<script type="module" itemprop="source">
export default function(ctx) {
if (ctx.request.headers["Authorization"] == null) {
throw {
schema_url: "https://pagelove.org/HTTPResponse",
status: 403,
message: "Forbidden"
};
}
}
</script>
</div>
Supported properties on the thrown object: status (defaults to 500), message (fallback body when body is absent), body, and headers (an object of additional response headers).
Response headers
An HTTPResponse can include custom response headers using the header property with Pair values:
@schema HTTPResponse url("https://pagelove.org/HTTPResponse");
@schema Pair url("https://pagelove.org/Pair");
throw new HTTPResponse {
status: 303,
header: new Pair { key: "Location", value: "/other-page.html" }
}
Multiple headers can be set by repeating the header property (it has 0..n cardinality):
throw new HTTPResponse {
status: 200,
body: "Hello
",
header: new Pair { key: "X-Custom", value: "one" },
header: new Pair { key: "Cache-Control", value: "no-cache" }
}
Writing from triggers
Trigger Sessel actions can write to the database using the Pagelove.PUT() and Pagelove.DELETE() methods. Import the Pagelove schema first:
This is a Sessel-only capability. JavaScript actions do not have access to Pagelove.PUT()/Pagelove.DELETE() — they run the same evaluation path used for when gates, without a write provider. A JavaScript action that needs to write should queue an outbound HTTP request back to the host instead.
<div itemprop="action" itemscope itemtype="https://pagelove.org/Sessel">
<script itemprop="source" type="text/sessel">
@schema Pagelove url("https://pagelove.org/1.0");
@schema Thing url("https://schema.org/Thing");
let item = new Thing { name: "side-effect-doc" };
Pagelove.PUT(item, "/side-effects/record.html")
</script>
</div>
Side-effect writes
Pagelove.PUT(item, "/path.html") writes a document to the specified path. The write goes through the normal request pipeline, including authorization checks and schema validation.
Body transformation
Pagelove.PUT(item, Context.request.path) writes to the same path as the incoming request. This replaces the request body in-flight — the server processes the transformed content instead of the original. No throw is needed; core processing runs normally on the modified body.
<div itemprop="action" itemscope itemtype="https://pagelove.org/Sessel">
<script itemprop="source" type="text/sessel">
@schema Pagelove url("https://pagelove.org/1.0");
@schema Article url("https://schema.org/Article");
let transformed = new Article {
name: "transformed-by-trigger"
};
Pagelove.PUT(transformed, Context.request.path)
</script>
</div>
Chaining
Each trigger in the chain sees the body as left by the previous trigger's Pagelove.PUT(). The last writer wins.
Error handling for writes
Side-effect writes committed via Pagelove.PUT(item, "/path") are not rolled back if a later trigger throws or errors. Body transformations via Pagelove.PUT(item, Context.request.path) are transient — they are discarded if the chain terminates with a throw or error.
Static vs dynamic properties
Any property on an outbound HTTP request action can be either a static literal or a dynamic Sessel or JavaScript expression:
<!-- Static URL -->
<meta itemprop="url" content="https://hooks.example.com/notify">
<!-- Dynamic URL (Sessel) -->
<div itemprop="url" itemscope itemtype="https://pagelove.org/Sessel">
<script itemprop="source" type="text/sessel">
@schema Context url("https://pagelove.org/Context");
"https://hooks.example.com/" + Context.request.path
</script>
</div>
<!-- Dynamic URL (JavaScript) -->
<div itemprop="url" itemscope itemtype="https://pagelove.org/JavaScript/Module">
<script type="module" itemprop="source">
export default function(ctx) { return "https://hooks.example.com/" + ctx.request.path; }
</script>
</div>
Declarative by default, imperative when needed.
Execution order
Within a single document, triggers execute in document order (top to bottom). When triggers are spread across multiple documents, documents are ordered lexicographically by path. Within each document, triggers execute in document order.
This produces a deterministic total ordering: triggers in /app/auth fire before triggers in /app/logging, and within each document, top to bottom.
Chain termination stops all subsequent triggers, including those from later documents.
Authentication example
A common pattern: a trigger fires on every request to check for a JWT and write identity claims to a transient element:
<div itemscope itemtype="https://pagelove.org/Trigger">
<meta itemprop="method" content="GET">
<meta itemprop="method" content="PUT">
<meta itemprop="method" content="POST">
<meta itemprop="method" content="DELETE">
<div itemprop="when" itemscope itemtype="https://pagelove.org/Sessel">
<script itemprop="source" type="text/sessel">
@schema Context url("https://pagelove.org/Context");
Context.request.headers["Authorization"] != null
</script>
</div>
<div itemprop="action" itemscope itemtype="https://pagelove.org/Sessel">
<script itemprop="source" type="text/sessel">
@schema Context url("https://pagelove.org/Context");
// Decode JWT claims and write to transient identity element
</script>
</div>
</div>
Authentication becomes another trigger — no separate login handler required.
Error handling
Malformed Microdata: If a trigger cannot be parsed from its Microdata (missing required fields, invalid structure), it is skipped. It does not break the request.
Sessel or JavaScript runtime error: If a when/action expression throws an unhandled error (one that isn't a recognized HTTPResponse shape), the request fails with an HTML Microdata error body describing the failure. Runtime errors are surfaced, not swallowed, regardless of which language raised them.
See also
- Processors — fire after core processing
- Outbound HTTP requests — async outbound HTTP action
- Transient elements — session-scoped DOM elements
- Sessel language reference — expression language
- JavaScript in schemas — JavaScript as a Pagelove binding language