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.

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.