Resolvers
Resolvers (also called property pipelines) are bindings declared on Property definitions that transform property values on the write and read paths. @write resolvers run before storage. @read resolvers run before the response is sent to the client. Resolvers can be written in Sessel or JavaScript.
When to reach for it
Use a resolver when a stored value should be normalised before it reaches storage (trim whitespace, lowercase an email, compute a slug) or transformed before it reaches the reader (format a timestamp, enrich a value with a lookup, compute a derived field).
Shape
A resolver is a typed microdata item with itemprop="@write" or itemprop="@read", placed inside a Property definition. The itemtype selects the language.
Sessel resolver
<div itemprop="property" itemscope itemtype="https://pagelove.org/Property">
<meta itemprop="name" content="email">
<meta itemprop="type" content="https://schema.host/Text">
<meta itemprop="cardinality" content="1..1">
<div itemprop="@write" itemscope itemtype="https://pagelove.org/Sessel">
<script itemprop="source" type="text/sessel">
self.map((el) => el.set_text(el.text().trim().lowercase()))
</script>
</div>
</div>
JavaScript resolver
<div itemprop="property" itemscope itemtype="https://pagelove.org/Property">
<meta itemprop="name" content="email">
<meta itemprop="type" content="https://schema.host/Text">
<meta itemprop="cardinality" content="1..1">
<div itemprop="@write" itemscope itemtype="https://pagelove.org/JavaScript/Module">
<script itemprop="source" type="module">
export default (val) => typeof val === 'string' ? val.trim().toLowerCase() : val;
</script>
</div>
</div>
The pipeline value
In both @read and @write bindings, the binding receives the property value as its primary input:
- Sessel:
selfis the list of all[itemprop]elements with this property's name within the item, excluding anything inside nested[itemscope]boundaries. The expression must return a list of elements (or a single element, auto-wrapped). - JavaScript: The first positional argument is the property value flowing through the chain. The function must return the transformed value. See JavaScript bindings for the full contract, including marshalling, error variants, and the
pagelove:schemaimport.
When each hook runs
| Hook | Runs | Effect |
|---|---|---|
@write |
Before validation. The data that validation sees is the post-transform result. | Normalise values. |
@read |
After the value is fetched from storage, before it is sent to the client. | Format or enrich values. |
Inheritance ordering
When a schema uses inheritance, resolvers from the entire chain form a sequence. The output of one step becomes self for the next. Schemas without a resolver for a given property are skipped.
| Hook | Order | Rationale |
|---|---|---|
@write |
Child-first (leaf → root) | The child normalises before the parent applies broader rules. |
@read |
Ancestor-first (root → leaf) | The parent provides a base transformation; the child refines it. |
Example with chain [Entity, Person, Employee]:
@write: Employee → Person → Entity@read: Entity → Person → Employee
Cross-document queries
Resolver expressions can read other documents on the host via selector queries. A lookup that reads a related record and attaches its name as an attribute:
<script type="text/sessel" itemprop="@read">
let org_id = self.first().text();
let org_name = ${ [itemtype*="Org"]:has([itemprop="identifier"]) }
.filter((el) => el.microdata()["identifier"].first() == org_id)
.first()
.microdata()["name"]
.first();
self.map((el) => el.set_attr("data-org-name", org_name))
</script>
Mixed-language chaining
Pipeline chains can mix Sessel and JavaScript across the inheritance hierarchy. A parent schema may declare a Sessel @read and a child schema may declare a JavaScript @read; both fire in ancestor-first order, with the Sessel stage's output feeding the JavaScript stage's input.
Examples
Normalise email on write (Sessel)
<div itemprop="@write" itemscope itemtype="https://pagelove.org/Sessel">
<script itemprop="source" type="text/sessel">
self.map((el) => el.set_text(el.text().trim().lowercase()))
</script>
</div>
Stores "alice@example.com" even if the user submits " Alice@Example.COM ".
Trim whitespace on write (JavaScript)
<div itemprop="@write" itemscope itemtype="https://pagelove.org/JavaScript/Module">
<script itemprop="source" type="module">
export default (val) => typeof val === 'string' ? val.trim() : val;
</script>
</div>
Uppercase on read (JavaScript)
<div itemprop="@read" itemscope itemtype="https://pagelove.org/JavaScript/Module">
<script itemprop="source" type="module">
export default (val) => typeof val === 'string' ? val.toUpperCase() : val;
</script>
</div>
Generate a slug on write (Sessel)
<div itemprop="@write" itemscope itemtype="https://pagelove.org/Sessel">
<script itemprop="source" type="text/sessel">
self.map((el) => el.set_text(el.text().slugify()))
</script>
</div>
Identity resolver
A resolver that returns self unchanged. Acts as a placeholder in an inheritance chain:
<div itemprop="@read" itemscope itemtype="https://pagelove.org/Sessel">
<script itemprop="source" type="text/sessel">self</script>
</div>
Error cases
| Condition | Result |
|---|---|
| Expression fails to compile | The step is skipped. No transform runs for this property on this schema. |
| Expression returns something other than an element or list of elements | The write or read request fails. |
| Expression throws at runtime | The write or read request fails. |
| Expression returns an empty list | The property is effectively removed from the output. |
Resolver errors on the write path cause the HTTP request to fail before the write reaches storage. Resolver errors on the read path cause the GET request to fail.
See also
- Property — where resolvers are declared
- Schema — inheritance and how chains are built
- JavaScript bindings — the JavaScript binding contract for
@read,@write,@validate, anddefault - Transforming data on write and read — the task-oriented recipe for resolver use cases