From "Decide After It Arrives" to "Declare the Changes You Want": How Shopify Next Gen Events Change Webhook Design
Hi, I'm Keita, a backend engineer at Flagship.
When you build Shopify apps, webhooks become the entry point for all kinds of integrations. When a product is updated, sync it to an external product management system. When an order comes in, send it to the core business system. When inventory changes, notify another service. For connecting changes on Shopify to external systems, webhooks have long been at the center.
But when you actually build integrations, what an app really wants to know is often not a broad event like "the product was updated." It is a more specific "change": "a variant's price changed," "the value of a particular metafield changed," or "this field was added, updated, or removed."
Until now, it has been common for an app to receive a webhook and then decide on its own whether "this update is relevant to us," or to call the Admin API again to fetch the data it needs for processing.
On October 1, 2026, Shopify's Next Gen Events became generally available (GA) in API version 2026-10. It supports 18 topics at GA, and Shopify positions Events as a declarative successor to classic webhooks.
The point of Events is not simply that a new webhook payload has arrived. It moves you from "receive every update, then decide whether you need it" to "declare up front which changes you want, and with which data." It is a mechanism that lets you change the entry point of an integration itself.
What you'll learn in this article
- How Next Gen Events differ from classic webhooks
- What
triggers,query, andquery_filtereach do - How webhook and Events payloads differ for a variant price change
- What changes when you subscribe only to a specific metafield
- What Events can and cannot do
- Where to start migrating from existing webhooks to see results quickly
1. With webhooks, it's hard to tell "what changed"
Consider, for example, an app that syncs Shopify product prices to an external product management system. What the app really cares about is only price changes on product variants.
With classic webhooks, you might subscribe to products/update. But a product changes in many ways besides price. The title changes. The description changes. Tags change. Metafields change. A variant's price changes. Even if the app only wants price changes, the entry point is the same products/update.
Webhooks do have filters
This needs a little care. Classic webhooks today do have filter and include_fields. For example, you can configure a subscription like this:
[webhooks]
api_version = "2026-10"
[[webhooks.subscriptions]]
topics = ["products/update"]
uri = "/webhooks/products-update"
include_fields = [
"id",
"status",
"variants.id",
"variants.price",
"updated_at"
]
filter = "status:active AND variants.price:>=100"include_fields narrows down the fields included in the payload.
filter evaluates a condition against the current values in the payload at delivery time, after the event has occurred, and skips webhooks that do not meet the condition.
However, "a product that has a variant priced at 100 or more was updated" is not the same as "this time, the variant's price itself was changed." Shopify's documentation also explains that when variants.price is used in a filter, a product with a matching variant will still trigger a delivery for updates unrelated to price, such as a title change.
In other words, what a classic webhook filter looks at is not
which field changed this time
but
whether the payload, after the event, currently meets the condition.
Simplified, traditional integrations tended to look like this:
Receive products/update
↓
Was this a change
we care about?
↓
Fetch extra data if needed
↓
Sync to the external systemWhat Events changes is this entry point.
2. With Events, you declare the changes you want up front
With Events, you can specify field-level triggers for updates. If you only want to receive an event when a product variant's price changes, you can write:
triggers = ["product.variants.price"]product.variants.price is a trigger that fires when the price field of a product variant changes. Shopify describes Events as a mechanism that uses field-level triggers, conditional filters, and custom GraphQL payloads so you receive only the changes and data you need.
Trigger / Query / Query Filter
Three concepts are central to understanding Events.
Trigger
Which changes make something an event.
triggers = ["product.variants.price"]means
when a variant's price changes.
Query
Which data to include in the payload when that change occurs. You can define a GraphQL Admin API query on the subscription, and Shopify includes the result of running it in the event payload's data.
Query Filter
Uses the current values fetched by the query to decide whether to deliver in the end. For example,
query_filter = "product.status:'ACTIVE'"means
deliver only if the product is currently ACTIVE.
To use query_filter, you also need a query: Shopify first runs the query to build data, then evaluates query_filter against that result.
To sum up:
What changed?
↓
Trigger
What data do we need?
↓
Query
When do we want to receive it?
↓
Query FilterPart of the decision-making and data fetching you used to write inside a webhook handler can now move into the subscription itself.
3. Webhooks vs. Events, seen through a price change
Let's look at these differences through a case where you want to sync to an external pricing system only when the variant price of an ACTIVE product changes.
With a classic webhook, you get "the product data after the update"
For example, you configure a webhook like this:
[webhooks]
api_version = "2026-10"
[[webhooks.subscriptions]]
topics = ["products/update"]
uri = "/webhooks/products-update"
include_fields = [
"id",
"status",
"variants.id",
"variants.price",
"updated_at"
]
filter = "status:active"In this case, the payload looks something like this:
{
"id": 9554194432293,
"status": "active",
"variants": [
{
"id": 123456789,
"price": "5500.00"
}
],
"updated_at": "2026-10-02T12:00:00Z"
}A classic webhook payload is in a fixed REST format: without include_fields you get the entire resource payload, and with it you get the selected fields.
From this payload, you can tell that
the product is currently ACTIVE
the current price of variant #123456789 is 5,500 yen.
But from this payload alone, you cannot directly tell
whether the price changed in this update.
For example, both an update like
price
5,000 yen → 5,500 yenand an update like
title
"T-Shirt" → "New T-Shirt"
price
still 5,500 yencan trigger products/update. Even in the second case, the payload contains the current value:
{
"price": "5500.00"
}In other words, what a classic webhook payload mainly tells you is "what the resource looks like after the update." include_fields can make the payload smaller, but it narrows down "which fields are sent," not "which fields changed this time."
With Events, you subscribe to "the price changed" itself
Now let's look at the same process with Events.
[events]
api_version = "2026-10"
[[events.subscription]]
handle = "product-price-change"
topic = "Product"
actions = ["update"]
triggers = ["product.variants.price"]
uri = "/events/products"
query = """
query price_change(
$productId: ID!,
$variantsId: ID!
) {
product(id: $productId) {
id
status
}
productVariant(id: $variantsId) {
id
price
}
}
"""
query_filter = "product.status:'ACTIVE'"Shopify's official documentation also shows a similar example that uses product.variants.price as the trigger, queries the Product and ProductVariant, and delivers only when the product is ACTIVE.
With this subscription, you declare the following process on the Shopify side:
Did a variant's price change?
↓
YES
↓
Query the Product / Variant
data we need
↓
Is the product ACTIVE?
↓
YES
↓
Deliver the eventThe event payload looks something like this:
{
"topic": "Product",
"action": "update",
"handle": "product-price-change",
"data": {
"product": {
"id": "gid://shopify/Product/123",
"status": "ACTIVE"
},
"productVariant": {
"id": "gid://shopify/ProductVariant/456",
"price": "5500.00"
}
},
"fields_changed": {
"added": [],
"updated": [
"product[id: 'gid://shopify/Product/123'].variants[id: 'gid://shopify/ProductVariant/456'].price"
],
"removed": []
},
"query_variables": {
"productId": "gid://shopify/Product/123",
"variantsId": "gid://shopify/ProductVariant/456"
}
}An Events delivery includes topic, action, and handle, plus fields_changed, which shows what changed, and query_variables, which shows the IDs of the entities involved in the change. If you set a query, its result is also included in data.
From this payload, you get three kinds of information:
fields_changed:pricewas updated on variant #456 of product #123query_variables: the IDs of the product and variant involved in the changedata: the product is currently ACTIVE, and the variant's current price is 5,500 yen
This is the big difference from classic webhooks.
| Classic webhook | Events | |
|---|---|---|
| Subscription | products/update | Product + product.variants.price |
| What the condition is | A resource update | A change to a specific field |
| Payload | Fixed REST format | Event metadata + custom GraphQL data |
| Current price | Can be included in the resource payload | Can be included in the query's data |
| Whether the price changed this time | Not directly visible from the payload alone | Visible in fields_changed |
| Which variant changed | Inferred from the payload | Identified via fields_changed / query_variables |
| Related data you need | An extra API call if needed | Can be included in the event via query |
Shopify itself describes Events as offering "fewer deliveries, richer payloads, no follow-up queries," because you narrow down to the changes you need and receive the data required for processing along with them.
fields_changed is not before / after
Let's look a little more closely at fields_changed. It contains three arrays,
{
"added": [],
"updated": [],
"removed": []
}each holding the paths of the fields that changed. Because the paths embed GIDs, you can trace exactly "which field, on which variant, of which product" changed.
However, even if there was a change like
price
5,000 yen → 5,500 yenthe event does not automatically contain
before: 5,000 yen
after: 5,500 yenWhat you can tell is:
fields_changed
→ price was updated
data
→ the price fetched by the query is 5,500 yenFurthermore, the query runs after the qualifying change has occurred. So data is not a fixed snapshot of the moment the change happened; it is the current state at the time the query was run. Shopify itself explains it this way: use fields_changed to understand the change, and data for current context.
In short, fields_changed = what changed, and data = what things look like at query time.
If you want to keep a strict before / after history such as "5,000 yen → 5,500 yen," you need a separate mechanism, such as storing previous values in an external database and comparing them. Events make "what changed" easier to handle, but they do not by themselves make a complete change-history system.
4. Metafields make the benefits of Events even clearer
Where Events look especially useful is in integrations that rely heavily on metafields. Suppose a PIM or ERP syncs many metafields to products, such as:
custom.material
custom.country_of_origin
custom.brand_code
custom.external_category
integration.erp_id
integration.sync_status
...But suppose a particular app only cares about
integration.sync_statusWith Events, you can subscribe by specifying the namespace and key:
triggers = [
"product.metafield(namespace: 'integration', key: 'sync_status').value"
]This lets you make an event only when the value of integration.sync_status changes. In the Product Events API, metafieldKey, metafieldNamespace, and productId are provided as query variables for this trigger.
You can also omit the key and subscribe at the namespace level:
triggers = [
"product.metafield(namespace: 'integration').value"
]In this case too, the namespace and key of the metafield that actually changed are included in query_variables, so you can use them as variables in your GraphQL query. For example,
query = """
query product_metafield_change(
$productId: ID!,
$metafieldNamespace: String!,
$metafieldKey: String!
) {
product(id: $productId) {
id
metafield(
namespace: $metafieldNamespace,
key: $metafieldKey
) {
namespace
key
value
}
}
}
"""lets you deliver "the metafield that changed" itself in data.
Simplified, a traditional design that looked like
Many product / metafield updates
↓
products/update
↓
App receives it
↓
Is this change
relevant to us?
↓
Process / discardcan be turned into
Did the value of
integration.sync_status change?
↓
YES
↓
Query the changed metafield
↓
Deliver the event
↓
Process in the appInstead of receiving updates and then throwing away the ones you don't need, declare the changes you need up front. I think metafields are a case where that benefit is especially easy to see.
5. Events don't eliminate app-side logic
Looking at all this, it may seem that much of what you wrote in webhook handlers can move over to Events. And indeed, processing like the following can move into Trigger / Query / Query Filter:
- Receiving only changes to specific fields
- Fetching related data along with them
- Stopping delivery based on current values
However, Events cannot express all of your business logic. For example, query_filter cannot express conditions such as comparing the value before and after the change. Shopify also explains that query_filter does not replace previous-value vs. new-value comparisons or multi-step business logic.
So "the price changed" can be expressed with a trigger. "The current price is 100 or more" can be expressed with a query filter. But logic that needs a before / after comparison, such as "the price rose by 20% or more," still has to be handled on the app side.
The purpose of Events is not to eliminate handlers. It is better to think of it as moving the decisions that can be made before reaching the handler into the subscription.
6. Where should you start migrating from existing webhooks?
Events reaching GA does not mean you need to replace all your existing webhooks at once. Events and classic webhooks can coexist in the same shopify.app.toml. Shopify also recommends keeping existing classic webhook integrations running while moving workflows that Events supports over one at a time.
At this point, the two also have different strengths. Events offer field-level triggers, custom GraphQL queries, and query_filter, but they support only some Shopify resources. Classic webhooks cover a wider range of topics, and you can manage subscriptions per shop through the GraphQL Admin API, not just in shopify.app.toml.
So rather than "use Events because they're new, drop webhooks because they're old," you need to choose based on the requirements of each integration.
Read your handler backwards
If you're looking for candidates to migrate to Events, I think the easiest place to start is your existing webhook handlers. For example, suppose you have this process:
products/update
↓
ACTIVE product?
↓
YES
↓
Did the price change?
↓
YES
↓
Fetch variant data via GraphQL
↓
Sync to the external systemThink about it backwards:
- Which changes should trigger processing? →
product.variants.price - What data does processing need? → Product status, variant ID, price
- In what state should it be processed? → The product is ACTIVE
Then see whether you can move this directly into
Trigger
↓
Query
↓
Query FilterShopify also recommends starting with workflows like these as candidates for migrating to Events:
- Workflows that receive many updates but discard most of them
- Workflows that make an extra API call on every delivery
In other words, it's easier to understand a migration to Events not as "rewriting webhooks against a new API," but as "working out how much of the decision-making and data fetching your handler does today can move into the subscription."
7. From "decide after it arrives" to "declare the changes you want"
If I had to sum up Next Gen Events in one phrase, I'd call it a shift from "decide after it arrives" to "declare the changes you want." An integration that, with classic webhooks, looked like
A resource is updated on Shopify
↓
A webhook arrives
↓
Was it a change we need?
↓
Do we have the data we need?
↓
Extra API call if needed
↓
Process itcan, with Events, be designed like this:
Did a field
we need change?
↓
Query the data we need
↓
Does the current state meet the condition?
↓
Deliver the event
↓
Process itWhat matters here is not simply that handler code gets a little shorter. With classic webhooks, processing basically starts from "receiving the updated resource." With Events, you can declare, as a subscription, everything up to "receiving the changes that matter to our system and the data needed to process them."
E-commerce sites with large volumes of product updates. E-commerce sites that sync many metafields with a PIM or ERP. E-commerce sites that link products and inventory across multiple systems. The more an integration looks like these, the more it piles up steps like "decide after receiving," "go fetch the data we need," and "end up discarding it anyway."
Events let you move part of that decision-making and data fetching to the Shopify side. That's why it's worth opening one of your existing webhook handlers and asking, "Does this process really need to receive every update on this topic?" If the answer is no, that handler may be your first candidate for migrating to Next Gen Events.
As an e-commerce business grows, so do the updates flowing between Shopify and external systems. If you can receive only the changes you need and pass the data you need straight into processing, it becomes easier to keep system integrations simple.
Next Gen Events look set to become an option for designing Shopify-centered integrations more efficiently.
References
- Shopify Developer Changelog: "More control over commerce updates with Next Gen Events" — The GA announcement on October 1, 2026: API version
2026-10, 18 topics, and how Events are positioned - Shopify Developers: "About Events and webhooks" — Feature differences between Events and classic webhooks, coexistence, and subscription management
- Shopify Developers: "Events" — Specifications for
fields_changed,query_variables,data, custom queries, andquery_filter - Shopify Developers: "Product — Events API" — Triggers and query variables available for Product / Variant / Metafield
- Shopify Developers: "Filter webhook deliveries" — How classic webhook
filteris evaluated, and the behavior when filtering on variant fields - Shopify Developers: "Webhooks delivery structure" — Classic webhook payloads and the
include_fieldsspecification