Skip to content

Commerce Hooks

The Commerce app provides custom hooks that other apps can listen to.

OnCartMutation

Fires after every successful cart mutation. The rendered cart record is passed directly as the hook input — listening rules receive the cart fields at root level alongside hook: 'OnCartMutation'.

When It Fires

  • After every successful cart mutation ingress: add-item, remove-item, set-quantity, set-address, set-email, set-phone, set-field, remove-field
  • Only fires when the mutation succeeds — if the mutation returns an error (e.g., validation failure, item not found), the hook does not fire
  • Does not fire on read-only operations: get-cart, cart SSE events
  • Does not fire on cart creation (create-cart) — the cart has no items yet

Hook Input

The input is the full rendered cart object with hook added by the runtime:

filtrera
{
  hook: 'OnCartMutation'
  cartId: uuid
  cartNumber: text
  cartState: 'open' | 'completed' | 'rejected'
  channelKey: text
  currencyCode: text
  taxIncluded: bool
  orderTotal: number
  orderTaxTotal: number
  shippingTotal: number
  productTotal: number
  items: [{
    cartItemId: uuid
    productNumber: text
    quantity: number
    unitPrice: number
    total: number
    tax: number
    discount: number
  }]
  fields: { text -> value }
  email: text | nothing
  phone: text | nothing
  address: value | nothing
  invoiceAddress: value | nothing
  locale: text | nothing
  profileKey: text | nothing
}

Listening Rule Example

Rules listen by declaring param input with hook: 'OnCartMutation' and the fields they need:

filtrera
param input: {
  hook: 'OnCartMutation'
  cartId: uuid
  currencyCode: text
  orderTotal: number
  orderTaxTotal: number
  items: [{
    productNumber: text
    quantity: number
    unitPrice: number
    total: number
    tax: number
  }]
  fields: { text -> value }
}

import 'iterators'

// Only process carts that have a PSP integration
let hasPaymentProvider = input.fields->'paymentProvider' is text

from hasPaymentProvider match
  true |>
    // Build update payload and schedule sync
    from {
      effect = 'ticketCommand'
      type = 'setDynamicFields'
      fields = { `field:lastSyncedTotal` = input.orderTotal }
    }
    from {
      effect = 'scheduleJob'
      definition = 'apps/my-psp/syncOrder'
      at = now
      parameters = { cartId = input.cartId, total = input.orderTotal }
    }

TIP

You only need to declare the fields your rule uses. Filtrera's type system automatically matches rules to hooks based on their declared input shape.

Supported Effects

The Commerce hook dispatcher processes these effect types:

EffectHow it's applied
ticketCommandBatched into a single applyCommands call on the cart
scheduleJobScheduled via scheduleJob runtime function

Other effects are silently ignored.

No Feedback Loop

The OnCartMutation hook fires from the Commerce reactor runtime (ingresses), not from OnTicketCommands. When hook effects apply commands to the cart (e.g., setting metadata fields), those commands trigger OnTicketCommands on the ticket actor — but no Commerce hook fires again. This breaks the feedback loop that would occur if you used OnTicketCommands directly.

Evaluation Semantics

The hook is evaluated synchronously within each mutation ingress, after the cart has been rendered but before the response is returned. This means:

  • ticketCommand effects (e.g., setting sync hashes) are applied before the response
  • scheduleJob effects are scheduled before the response
  • The rendered cart returned to the storefront does not include changes from hook effects — it reflects the state at the time of rendering, before hook effects are applied

See Also

© 2024 Hantera AB. All rights reserved.