2026.2
Platform & Core
On<Actor>Validate Rule Hooks Feature
A second rule-evaluation phase now runs after On<Actor>Commands, available for all actor types that already expose a *Commands hook: Order, Payment, Asset, Ticket, and Sku.
On<Actor>Commands rules all run in parallel and see the same before snapshot — none of their emitted commands are visible to the others. That breaks when one rule's output is another rule's input. The validate phase closes this gap by running once, after the *Commands-emitted commands have been applied, with the fully-settled actor state in scope.
A validate rule can either:
- Reject the state by emitting a
common.validationErroreffect, or - Auto-fix it by emitting more commands.
param input: {
hook: 'OnOrderValidate'
before: Order
order: Order
}before is the state before the incoming message's commands were applied; order (or payment/asset/ticket/sku) is the state after the *Commands phase. The validate phase runs a single pass — there is no second *Commands phase afterward and no validate loop. If validation errors are returned, the actor state is reset and the message fails, identical to *Commands behavior.
A typical use case is inventory routing: an enrichment rule copies SKU data onto order lines in OnOrderCommands, and a routing rule subscribed to OnOrderValidate reliably sees that data when assigning deliveries to inventories.
Standardized Currency Model Feature
Currency is now modeled as a property of a bounded context rather than a dimension of individual values. Orders, invoices, and payments are each single-currency contexts — every monetary value within one shares the same currency — which keeps all platform-internal math inside a single currency and avoids mixed-currency arithmetic entirely.
Currencies are defined as registry entries at currencies/<code>:
currencies/SEK:
label: 'Swedish Krona'
decimals: 2
exchangeRate: 1.0
currencies/EUR:
label: 'Euro'
decimals: 2
exchangeRate: 11.5- Codes are arbitrary identifiers following the ISO 4217 convention by recommendation, but any string matching
^[A-Za-z0-9_]{1,16}$is accepted (e.g.BTC,LOYALTY_POINTS). exchangeRateis a normalized scalar — the rate between two currencies A and B israte(B) / rate(A). There is no system-defined base currency.- Invoices snapshot the rate. At invoice creation, the platform reads
currencies/<code>.exchangeRateonce and writes it to the invoice'sexchangeRatefield, where accounting requires a pinned, point-in-time value. This is the only place the platform reads the registry rate at runtime — orders deliberately do not store a rate. - Formatting is presentation-layer — symbol placement, separators, and decimals are derived in the browser via
Intl.NumberFormat, not stored in the registry.
Cross-currency conversion remains a reporting concern: the platform never auto-converts between currencies.
A new Currencies settings view in the portal lets you manage currencies natively — add ISO currencies from a picker (prefilling label and decimals) or define custom codes, with inline editing of labels, decimals, and exchange rates.
Conventional Inventory Address Model Feature
Inventory and delivery addresses now follow a standardized, conventional model rather than ad-hoc per-app shapes. A consistent address structure across deliveries and inventories makes routing, validation, and integration logic portable between apps and removes the need for each app to define its own address conventions.
inventoryKey and inventoryDate Removed from Deliveries Improvement Breaking
The inventoryKey and inventoryDate fields are no longer built-in system fields on deliveries. They are now conventional custom fields on the delivery, aligning delivery inventory data with the same extensibility model used elsewhere in the platform. The delivery's inventoryDate custom field supports the date-only type, consistent with the date-only planning-date semantics introduced in 2026.1.
Stock models — stock positions, reservations, and incoming stock — are unaffected and continue to carry inventoryKey and inventoryDate as system fields.
invoiceAddress Renamed to invoiceRecipient Breaking
The invoiceAddress field on orders has been renamed to invoiceRecipient. The new name better reflects that the value identifies who is invoiced — a recipient that carries address and identity details — rather than a plain address. Graph and search fields are updated accordingly.
WARNING
This is a breaking change. Update any API requests, queries, or components that reference invoiceAddress to use invoiceRecipient.
App Development
App Contracts — requires: Declarations Feature
Apps can now declare the external surfaces they depend on through a requires: block in h_app.yaml. When an app is installed, its components are compiled in an isolated context that cannot see anything outside the app's own boundary — which previously meant an app could not type-check against graph shapes or modules contributed by another app.
requires: closes that gap without breaking isolation. Declared dependencies are merged into the isolated compile context as stubs, giving the compiler enough information to type-check:
# in h_app.yaml
requires:
graph: { ... } # graph shapes (sets / fields / edges) from other apps
modules: { ... } # exported module symbols and their types- Graph requirements are provider-agnostic — any app contributing the required shape satisfies them, regardless of its id.
- Module requirements remain bound to a specific producer by URI.
- Phased validation — at install time the server compares the declared
requires.graphagainst the tenant's actual state and emits warnings; at activation it recompiles the consumer's components against the real producer modules, surfacing any type mismatch as a standard Filtrera error that blocks activation.
This also enables the language server to resolve cross-app modules and graph shapes offline, using only the app's own manifest plus the embedded base graph.
Component Runtimes
Optional Record Fields Feature Breaking
Filtrera now has first-class syntax for optional record fields: field?: T. Previously, optionality was expressed by convention as T | nothing, which conflated two distinct meanings — "this field may be absent" versus "this field is present but holds nothing".
let Patch: {
name: text | nothing // required field; nothing means "explicitly cleared"
email?: text // optional field; absence means "no change"
}field?: T desugars to a union of records over the power-set of optional fields, so the existing union-walking and pattern-matching logic handles it naturally:
r match { email?: text } |> ...WARNING
This is a breaking change. Scripts that relied on T | nothing to mean "optional field" will fail to type-check and must migrate to field?: T. Use field: T | nothing only when a field is required but may explicitly hold nothing.
Graph & Queries
Graph Query require Modes Improvement
The require flag on graph navigations now accepts an enum value for finer control over edge presence:
any— 0 or more edges may be presentsome— 1 or more edges must be presentnone— exactly 0 edges must be present
Boolean values continue to work: true maps to some and false maps to any. The query editor lets you set the require value per navigation.
The none mode is especially useful for orphan and stale-source cleanup queries — for example, finding entities whose referenced edge no longer exists.
Query Macro Chaining Fix Bug fix
Query macros can now be chained correctly. Previously, chaining a query macro produced an error.
Ingresses
Raw HTTP Ingress Responses Feature
HTTP ingresses can now produce raw HTTP responses, giving the component full control over the response status, headers, and body — rather than only returning component output serialized as JSON. This enables ingresses that serve HTML, redirects, custom content types, or non-standard status codes.
General
Numerous bug fixes and performance improvements throughout the platform, portal, and design system.