Skip to content

Cart Dynamic Fields

Each Commerce cart is a ticket actor, and any app can store state on it in the ticket's dynamic map using the setDynamicFields command. This is how PSP, shipping, tax and tracking apps carry their own data alongside the cart.

Most of that state is private to the app that wrote it. But some of it needs to survive cart completion and end up on the created order — an external payment reference, a shipping selection, a consent flag. Commerce projects those fields onto the order using key prefixes.

Key prefixes

When a cart completes, the cart's dynamic fields are projected to the order:

Cart dynamic keyLands onAsNotes
order:<key>Order<key>Verbatim — vendor-prefix your key names
delivery:<key>The order's delivery<key>
field:<key>Ordercart:<key>Storefront-facing; also returned in the rendered cart's fields
anything elsenot projectedApp-private cart state

Everything without a recognised prefix stays on the cart and is never copied to the order. That is the default, and it's the right place for working state such as sync hashes, session pointers or cached external payloads.

order: and delivery:

These are for apps. Keys land on the target verbatim, with the prefix stripped:

cart.dynamic:
  order:pspOrderId          = 'ord_1a2b3c'
  delivery:shippingOptionId = '4f2c...'

→ order.dynamic:
  pspOrderId                = 'ord_1a2b3c'
→ order.deliveries[0].dynamic:
  shippingOptionId       = '4f2c...'

Because keys are not namespaced on arrival, the order's dynamic map is a shared space across every installed app. setDynamicFields merges per top-level key, so apps writing different keys never interfere — even concurrently. Prefix your key names with your app or vendor so two apps never fight over a generic name like reference or status.

field:

field:<key> is the storefront-facing namespace, written through the set-field ingress:

bash
POST /ingress/commerce/carts/{cartId}/set-field/giftMessage
{ "value": "Happy birthday!" }

These are returned to the storefront in the rendered cart's fields property, and projected onto the order under the cart: namespace (cart:giftMessage). The extra namespace is deliberate: this data originates from the client, so it is kept clearly separated from app-authored order fields.

Writing prefixed fields

Prefixed keys contain a :, which isn't valid in a bare identifier. Wrap the name in backticks and it behaves like any other record field:

filtrera
from [{
  type = 'setDynamicFields'
  fields = {
    `delivery:nShiftCheckoutOptionId` = optionId
    `delivery:nShiftCheckoutPickupPointId` = pickupPointId
  }
}]

Backticked and plain field names mix freely in the same record literal, so a prefixed field never forces you to restructure the rest:

filtrera
from [{
  type = 'setDynamicFields'
  fields = {
    email = email
    address = shippingAddress
    `order:marketingConsent` = marketingConsent
  }
}]

Backticks aren't specific to dynamic fields

Backtick-quoting works anywhere Filtrera expects a symbol — record fields in literals and in type declarations, and member access on a record. Reach for it whenever a name isn't a plain identifier.

Map-literal syntax ('key' -> value) is equally valid, and is the natural choice when the key is computed rather than written out:

filtrera
from [{
  type = 'setDynamicFields'
  fields = {
    ('delivery:' + fieldName) -> fieldValue
  }
}]

Write the fields before sending the complete message — cart-to-order reads the cart's state at completion time. See PSP Integration.

Reading fields back

Off the cart, while it is still open:

filtrera
let optionId = input.ticket.dynamic->'delivery:nShiftCheckoutOptionId' match
  (id: text) |> id
  |> nothing

Off the order or delivery, after completion — the prefix is gone:

filtrera
param input: OnOrderValidate

from
  input.order.deliveries
  select d => d.dynamic->'nShiftCheckoutOptionId'

UUIDs come back as text

Dynamic values are stored as JSON, which has no UUID type. A uuid written to a dynamic field is always read back as text, so match it as text and convert:

filtrera
let sessionId = d.dynamic->'nShiftCheckoutSessionActorId' match
  (id: text) |> id::uuid
  |> nothing

To surface a field in the portal or the query graph, register it as a graph field in your app manifest with a dynamic-> source:

yaml
registryEntries:
  - path: graph/ticket/cart/kustomOrderId
    value:
      source: "dynamic->'field:kustomOrderId'"

See Also

© 2026 Hantera AB. All rights reserved.