Theme architecture

Is your Shopify theme agent-ready? Start with the cart

WebMCP lets AI agents drive the cart through standard storefront actions. Themes with hand-rolled cart fetches can now desync. A five-minute audit, and the fix.

Bas Lefeber

Founder, learnshopify.dev · August 24, 2026 · 5 min read

Ready to learn Shopify development?Short, interactive lessons where you write real Liquid against a live storefront and watch it change as you type. Free, and genuinely fun.

Here is a bug that did not exist three weeks ago. A shopper opens your store, asks their browser's AI assistant to add the large size to the cart, and it works. The cart genuinely has the item. Then they click your cart drawer and it is empty. Not stale, not slow. Empty, because your drawer has never once asked the cart what is in it. It only knows what it put there.

On 5 August 2026 Shopify turned on WebMCP for every Liquid storefront, which lets an AI agent in the browser call a tool named update_cart. Those cart tools go through standard storefront actions, the same functions apps use. If your theme writes to the cart with its own fetch and updates its own UI in the response handler, you now have two systems editing one cart and only one of them is listening.

TL;DR

Route cart writes through Shopify.actions.updateCart instead of a raw fetch('/cart/add.js'), and re-render your cart UI from the shopify:cart:lines-update event rather than from your own response handler. That makes your theme correct for agents, for apps, and for every other thing that touches the cart. It was already best practice. It is now a functional requirement.

The difference is not how you write to the cart. It is whether you listen when somebody else does.

The pattern that is about to age badly

Open almost any custom theme built in the last five years and you will find something close to this. It is not incompetent code. It was the normal way to build an AJAX cart, and it works perfectly right up until something other than your own button changes the cart.

assets/cart-drawer.js (the pattern to retire)
// Adds to cart, then hand-updates the drawer from the response.async function addToCart(variantId, quantity) {  const res = await fetch("/cart/add.js", {    method: "POST",    headers: { "Content-Type": "application/json" },    body: JSON.stringify({ id: variantId, quantity }),  });  const line = await res.json();   // The drawer's only source of truth is this response.  renderDrawerLine(line);  incrementBadge(quantity);  openDrawer();}

Three separate assumptions are baked in here, and an agent breaks all of them at once. The drawer assumes it caused every change. The badge assumes it can track the count by arithmetic rather than by reading it. And the open-drawer call assumes a change always came from a human clicking a button, which is why agent-driven adds can pop your drawer open at a moment the shopper did not ask for.

The five-minute audit

You do not need an agent to test this. Every check below runs in your browser console on your own storefront, because you are simulating exactly what the agent does: changing the cart through the standard action rather than through your UI.

Run in the console on a product page
// 1. Does the theme expose the standard actions at all?console.log(typeof Shopify?.actions?.updateCart);   // expect "function" // 2. Change the cart the way an agent would: not via your button.await Shopify.actions.updateCart({  lines: [{ merchandiseId: "gid://shopify/ProductVariant/YOUR_ID", quantity: 1 }],}); // 3. Now look at the page WITHOUT reloading.//    Did the cart count update? Did the drawer contents update?//    If either says zero, your UI is not listening. // 4. Does anything at all hear the event?addEventListener("shopify:cart:lines-update", (e) =>  console.log("theme heard it:", e.detail),);

Step three is the whole test. If the badge still reads zero while /cart.js reports an item, your theme and the cart disagree, and every agent interaction from here will surface that disagreement to a shopper.

Check the badge separately from the drawer

These usually fail independently. Plenty of themes re-fetch the drawer on open (which hides the bug) while the header badge is still counting with arithmetic. A shopper who never opens the drawer sees a wrong number the entire session.

The fix, in two moves

1. Write through the standard action

Shopify.actions.updateCart does the write and emits the event that everything else on the page is listening for, including apps. A raw fetch to /cart/add.js changes the cart silently as far as the rest of the page is concerned.

assets/cart-drawer.js (write path)
async function addToCart(variantId, quantity) {  // One call. It writes AND announces.  await Shopify.actions.updateCart({    lines: [{ merchandiseId: variantId, quantity }],  });  // Note what is NOT here: no rendering. That happens below.}

2. Render from the event, never from the response

This is the move that makes the theme agent-ready, and it is a genuine inversion of how most cart code is written. Your UI stops being something you update after an action and becomes something that reflects cart state whenever cart state changes, no matter who changed it.

assets/cart-drawer.js (render path)
// One listener. Every writer on the page flows through it:// your buttons, installed apps, and now AI agents.addEventListener("shopify:cart:lines-update", (event) => {  renderDrawer(event.detail);  renderBadge(event.detail);}); // Opening the drawer stays tied to intent, not to cart changes.// An agent adding an item should not throw UI at the shopper.addToCartButton.addEventListener("click", () => Shopify.actions.openCart());

Separating the open from the update is the subtle one. Under the old pattern they were the same event, because the only way the cart changed was somebody clicking. Now they are different questions: what does the cart contain is answered by the event, and should the drawer be visible is answered by what the shopper did.

Four checks. Any no is a desync waiting to be reported as a bug you cannot reproduce.

Cart attributes changed too

One day after WebMCP shipped, Shopify extended the standard action set so cart attributes update through it as well, with a matching shopify:cart:attributes-update event. If your theme writes gift messages, delivery notes, or engraving text, the same rule applies: write through the action, re-render from the event.

Attributes are not line item properties

Cart attributes are order-wide, like a gift message for the whole order. Line item properties belong to one line, like engraving text on a single item. They are different mechanisms and reaching for the wrong one produces data that looks right in the cart and wrong on the packing slip.

Why this was always the right answer

Nothing in the fix above is new advice. Shopify has recommended the standard cart mechanisms for years, and the reasons given were mostly about app compatibility: route your writes properly so a subscription app or an upsell app does not fight your theme. It was easy to treat that as a nice-to-have, because most stores could get away with ignoring it.

What changed on 5 August is not the recommendation. It is the cost of ignoring it. A convention you could quietly skip has become a thing that produces a visible, shopper-facing bug, on a surface Shopify enabled without asking. That is a fairly common shape for platform work, and it is the argument for following the documented mechanism even when a shortcut demonstrably works: the shortcut is only correct until the platform adds a second actor.

Learn this properly · free lesson

Add to cart without a page reload: the cart drawer

Build a cart drawer that renders from cart state instead of from its own fetch, against a live storefront emulator with a real cart API. This is the pattern above, hands on.

Try this lesson — free

Frequently asked questions

Will WebMCP break my existing Shopify cart drawer?

It will not break the drawer for normal human use. The risk is desync: if an AI agent changes the cart through the standard storefront actions and your drawer only updates from its own fetch response, your UI and the real cart will disagree until the page reloads.

Should I stop using fetch('/cart/add.js') in my Shopify theme?

For cart writes, prefer Shopify.actions.updateCart. It performs the same write and emits the shopify:cart:lines-update event that apps, other theme code, and now AI agents rely on. A raw fetch changes the cart without announcing it.

How do I test whether my theme is agent-ready?

Open your storefront console and call Shopify.actions.updateCart directly, without using your own add-to-cart button. Then check whether the cart badge and drawer update without a page reload. If they do not, your UI is not listening to cart changes it did not cause.

What is the difference between cart attributes and line item properties?

Cart attributes apply to the whole order, such as a gift message. Line item properties apply to one specific line, such as engraving text on a single product. Since August 2026 both cart attributes and cart lines update through standard storefront actions, each with its own event.

Start free

Ready to become a Shopify developer?

You just read how it works. Now write it yourself: real tickets from a live store, in an editor where the storefront updates as you type. Module 1 is free, no card.

Start your first lesson

Free · No credit card · Your first win in minutes

themescartjavascriptagents

About the author

Bas Lefeber, Founder, learnshopify.dev

Bas builds learnshopify.dev, where developers learn production-grade Shopify theme development against a live storefront. He writes about Liquid, theme architecture, and the parts of the job that still matter now that AI writes the code.

Keep going in the curriculum