September 2026 (version 2.26.09.16)

Reviewed: 16 September 2026

🚀 New Features

Async Page Rendering

Introduced <@page/async> to allow rendering page elements asynchronously, improving page responsiveness when elements are slow to produce or depend on external resources.

VSCode diagnostics

Elements can define a placeholder that renders immediately, while the deferred content executes in the background. Once complete, the placeholder is seamlessly replaced with the result, without blocking or reloading the rest of the page.

<@page/async
    placeholder: <@page/icon class: 'fa-spin m-0 p-0 h4' name: 'spinner'>
    content: $function ()
        sleep(tdg.number(1000, 5000))
        return format(tdg.number(0, 2000), '#,###')
    end-function
>

In the example above, a spinning icon is displayed instantly while the content function sleeps for 1–5 seconds and formats its result.

This makes <@page/async> ideal for dashboards, reports, or any element backed by slow queries, external API calls, or heavy computation.

Non-Qualified Qualifiers

Exposed support for non-qualified qualifiers via <@!...> syntax, allowing developers to opt out of qualification where needed.

Internally, this concept has always existed to prevent recursion when parsing the <@qualifier/schema> and <@qualifier/schema/item> qualifiers. We have now exposed the functionality as a developer-facing feature.

Prefixing a qualifier name with an exclamation mark, i.e. <@!docx/text>, means that when the property bag is interpreted at runtime, the qualifier will not be applied. This is useful when needing to refer to a qualifier inside of itself, or in recursive lookups or inheritance scenarios.

IntelliSense support is unaffected, and VS Code will still resolve the qualifier properties. However, be aware that since the property bag is not actually qualified:

  • Default property values will not be applied
  • The property bag cannot be invoked

Assigning Qualifiers at Runtime

Added support for assigning an @qualifier to an existing property bag at runtime, complementing the new non-qualified qualifier syntax.

Where <@!...> lets you opt out of qualification, qualify() lets you opt in, enabling qualification of property bags created outside of qualifier syntax, or after the fact. This is particularly useful for working around recursive qualification scenarios.

function main()
    set('pb', <'hello, world'>)

    pb.qualify([pb], '@docx/text')

    return page.generate([pb])

    // <w:t xml:space="preserve">hello, world</w:t>

end-function

Relative Inheritance

Inheritance now supports relative layer resolution using the new //~/... syntax. Instead of referencing a layer by name (e.g., //runtime/...), paths are resolved relative to your position in the layer stack.

When developing layers, relying on name paths (e.g., //runtime/...) creates a blind spot. If you override a base script, you cannot determine if a layer below yours has also overridden it. In such cases, your override inadvertently bypasses the lower layer's changes, breaking the intended inheritance chain.

The new relative syntax //~/... resolves this by anchoring the lookup to the current position in the layer stack. It ensures that your layer correctly inherits from the nearest definition below it, preserving the integrity of the override hierarchy regardless of how many layers sit beneath you.

Shared Request Context

Introduced a new [!/...] shared request context, enabling cross-thread state sharing within a request.

The existing [//...] request context is scoped to the request, but when threaded work occurs it is cloned into each thread and only merged back once all threads have completed. This means changes made to the [//...] context affect only the thread where they are set, threads cannot see each other's changes while running.

The new [!/...] context is also scoped to the request, but unlike [//...] it is shared across threads. Any thread can read and write to [!/...] during execution, making it the place to store state that should be visible to all parallel work within a request.

set('//thread-local', 'value')   // Visible to this thread only

set('!/shared', 'value')         // Visible to all threads in this request

🔧 Updates

Apostrophe Support in pCase()

pCase() now correctly capitalises names containing apostrophes (e.g. O'Brien, D'Angelo) rather than lowercasing the letter following the apostrophe.

Mixed Email Address Separators

The SMTP handler now supports mixed comma and semicolon separators when specifying multiple email addresses, accepting inputs such as:

john@example.com, jane@example.com; billing@example.com

🐞 Fixes

Corrected HMAC-SHA512 Output

Fixed an issue where hmacSha512() was incorrectly returning a SHA256 hash rather than SHA512.

Cloned Unhandled Dynamic Properties

Fixed an issue where cloning an unhandled dynamic business object attribute resulted in a reference rather than a deep copy.

When cloning a dynamic attribute using clone(bo.attr([bo], '_attribute')), if that attribute had not yet been read (i.e., was "unhandled"), the resulting clone acted as a live reference to the original attribute.

This fix ensures that dynamic attributes are correctly evaluated/resolved before cloning, preventing unintended side effects when manipulating cloned data structures.