Skip to content

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.validationError effect, or
  • Auto-fix it by emitting more commands.
filtrera
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>:

yaml
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).
  • exchangeRate is a normalized scalar — the rate between two currencies A and B is rate(B) / rate(A). There is no system-defined base currency.
  • Invoices snapshot the rate. At invoice creation, the platform reads currencies/<code>.exchangeRate once and writes it to the invoice's exchangeRate field, 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:

yaml
# 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.graph against 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".

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

filtrera
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 present
  • some — 1 or more edges must be present
  • none — 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.

© 2024 Hantera AB. All rights reserved.