# Filters

> Change what Smootify does before it does it, with Smootify.addFilter and its four filters for the lines added to the cart, the checkout address, draft orders and the metafields a page asks for.

Status: Ready (Smootify 2.0)  
1.x page: https://docs.smootify.io/docs/custom-code/js  
URL: https://www.smootify.io/docs/developers/filters  
Updated: 2026-10-01

A filter gets a value just before Smootify uses it, and can change it or stop what is about to happen. Use one to check or enrich the lines added to the cart, to send the visitor to another checkout address or keep them on the page, to change a draft order, or to choose which metafields the page asks Shopify for. Events, in comparison, only report what already happened: see [Events](https://www.smootify.io/docs/developers/events).

## How a filter works

```js
window.Smootify = window.Smootify || [];
window.Smootify.push((Smootify) => {
  const remove = Smootify.addFilter("add_to_cart_lines", (lines, context) => {
    // answer the lines changed, nothing to keep them, or false to add nothing
  });
  // later: remove();
});
```

- **Add filters in a `push` function.** It runs before Smootify asks Shopify for anything, so the filter applies from the first request.
- **A filter can be async**, except `metafield_definitions`.
- **Filters run in the order they were added.** Each gets what the one before answered.
- **`addFilter` returns a function** that removes the filter. A name that is not one of the four only logs a warning.

What a filter answers decides what happens next:

- **The value, changed**: Smootify uses it, and the next filter gets it. For example the lines with an attribute added, or the checkout address with a parameter.
- **Nothing (`undefined`)**: the value stays as it was. For example a filter that only checks, or that leaves draft orders alone.
- **`false`**: Smootify stops, and the visitor sees no message: nothing is added to the cart, or the visitor stays on the page. An empty list of lines does the same. Not for `metafield_definitions`, which cannot stop.
- **An `Error` thrown**: Smootify stops too, and the message shows in the form or in the cart and goes out with the `smootify:errors` event. Write it for the visitor, such as "You can buy at most 5 of each item." For `metafield_definitions`, the error is logged and the filter skipped.

## `add_to_cart_lines`

`(lines, { goToCheckout, element })`: the lines about to be added to the cart. Every addition goes through it: the add-to-cart forms (the main line and its add-ons together), Magic Box boxes, `button[data-is="direct-add-to-cart"]`, buy again in the account, `Smootify.addToCart`, `Smootify.addBoxToCart` and the `/cart/add.js` calls of theme apps.

- `lines` is a list of `{ merchandiseId, quantity, attributes, sellingPlanId }`, where `merchandiseId` is the variant’s global id.
- `element` is the `smootify-add-to-cart` form, or the button that added. It is `undefined` for an addition from a script.
- `goToCheckout` is `true` for a buy now.
- An empty list adds nothing, like `false`.

Refuse a quantity above a limit, with a message in the form:

```js
Smootify.addFilter("add_to_cart_lines", (lines) => {
  if (lines.some((line) => (line.quantity ?? 1) > 5)) {
    throw new Error("You can buy at most 5 of each item.");
  }
});
```

Add a hidden attribute to every line. A key that starts with `_` is saved on the line but not shown at checkout:

```js
Smootify.addFilter("add_to_cart_lines", (lines, { element }) =>
  lines.map((line) => ({
    ...line,
    attributes: [...(line.attributes ?? []), { key: "_source", value: element ? "form" : "script" }],
  })),
);
```

## `checkout_url`

`(url, { cart, draftOrder, draftOrderData })`: where the visitor goes to check out. It runs for the cart’s checkout button, for a buy now and for `Smootify.checkout()`.

- Answer another address, for example with parameters added, or `false` to keep the visitor on the page (terms not accepted, an order below a minimum).
- The message of an error it throws shows in the cart, or in the form for a buy now.
- `draftOrder` is `true` when the address is the invoice of a draft order. Its data is `draftOrderData`.
- A buy now with a custom price (Name your price, Product configurator) skips the cart: there `cart` is `null`, and the line is in `draftOrderData`.
- `smootify:initiate_checkout` goes out only when the visitor actually leaves, after every filter.

Keep the visitor in the cart until a checkbox is ticked:

```js
Smootify.addFilter("checkout_url", () => {
  const terms = document.querySelector("smootify-cart input[name=terms]");
  if (terms instanceof HTMLInputElement && !terms.checked) {
    throw new Error("Please accept the terms of sale.");
  }
});
```

Add a parameter to Shopify’s checkout address:

```js
Smootify.addFilter("checkout_url", (url, { draftOrder }) => {
  if (draftOrder) return;
  const next = new URL(url);
  next.searchParams.set("locale", "it");
  return next.toString();
});
```

## `draft_order_data`

`(data, { reason })`: the draft order about to be created. Draft orders need the **Draft Orders** extension, on the server plan: see [Quotes and B2B](https://www.smootify.io/docs/features/quotes).

- `reason` is `quote` for a quote from a `smootify-cart[data-draft]`, `checkout` for a cart with a custom price, or `buy-now` for a buy now with a custom price.
- `data` is `{ input: { lineItems, customAttributes, note, presentmentCurrencyCode }, customerId, email }`.
- The message of an error it throws shows in the cart, or in the form for a buy now.

Tag every quote with a custom attribute:

```js
Smootify.addFilter("draft_order_data", (data, { reason }) => {
  if (reason !== "quote") return;
  const customAttributes = [...(data.input.customAttributes ?? []), { key: "Source", value: "Website quote" }];
  return { ...data, input: { ...data.input, customAttributes } };
});
```

## `metafield_definitions`

`(definitions, { scope, attribute, selector })`: the metafields the page asks Shopify for, as a list of `{ namespace, key }`. It runs once for each kind of request:

- `scope: "product"`: the products, from the `[metafield]` elements inside `smootify-product`;
- `scope: "variant"`: their variants, from the `[variant-metafield]` elements;
- `scope: "cart"`: the products of the cart lines, from the `[metafield]` elements inside `cart-item`.

Add the metafields a script reads, and remove the ones nobody shows, which makes each request smaller. The filter is synchronous and cannot stop anything: it answers a list, or nothing. An error it throws is logged and the filter skipped. It runs again whenever the page asks for new metafields, so build the answer from the list it gets.

```js
Smootify.addFilter("metafield_definitions", (definitions, { scope }) => {
  if (scope !== "product") return;
  return [...definitions, { namespace: "custom", key: "care_guide" }];
});

Smootify.on("product_loaded", ({ product }) => {
  const guide = product?.metafields.find((m) => m?.namespace === "custom" && m.key === "care_guide");
  if (guide) console.log(guide.value);
});
```

In Shopify, the metafield definition still needs **Storefront access**, or the value never comes back.

## Page hooks from 1.x

1.x read a few functions set on `window`. They still work in 2.0 and are deprecated. On a `.webflow.io` address, the [debugger](https://www.smootify.io/docs/start/debugger) points them out.

- **`window._metafieldsDefsFilter(selector, attribute, definitions)`**: use `metafield_definitions`. The old hook still runs, before the filters.
- **`window._filterDraftOrderData(data)`**: use `draft_order_data`, which applies to every draft order, not only to quotes. The old hook still runs, before the filters.
- **`window.smootifyAddOnsBox(lines)`**, the synchronous box hook: use `window.smootifyAddonsBox`. The old one is used only when the new one is missing.

`window.smootifyAddonsBox` is not deprecated. Smootify calls it when an add-to-cart form adds a product and its add-ons as one box. It gets `{ lines, mainProduct, mainVariant, selectedAddonsVariants }`, can be async, and answers the box’s `title` and `image`, which replace the `box-title` and `box-image` of the form. See [Product add-ons](https://www.smootify.io/docs/features/add-ons).

```js
window.smootifyAddonsBox = ({ mainProduct, selectedAddonsVariants }) => ({
  title: `${mainProduct.title} with ${selectedAddonsVariants.length} extras`,
});
```

## Where people get stuck

> [!STUCK]
> - **The first product ignores the filter.** The filter was added outside a `push` function, after Smootify had already asked Shopify. Add it in a `push` function.
> - **The checkout button does nothing.** A `checkout_url` filter answered `false`. That keeps the visitor on the page without a message: throw an `Error` instead to show one in the cart.
> - **`false` from `metafield_definitions` changes nothing.** This filter cannot stop. Answer the list you want.
> - **A metafield added by the filter is still missing.** Its definition in Shopify has no Storefront access, or the filter checks the wrong `scope`.
> - **A 1.x hook does nothing.** `_metafieldsDefsFilter` must exist before Smootify's first request. Move it to a `metafield_definitions` filter in a `push` function.

## Coming from 1.x

| 1.x | 2.0 |
|---|---|
| `window._metafieldsDefsFilter` | `Smootify.addFilter("metafield_definitions", …)` |
| `window._filterDraftOrderData` (quotes only) | `Smootify.addFilter("draft_order_data", …)` (every draft order) |
| `window.smootifyAddOnsBox` | `window.smootifyAddonsBox` |

`add_to_cart_lines` and `checkout_url` are new in 2.0.
