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:

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]:

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