Order / Collection / InventoryItem / InventoryShipment / Location can now be subscribed to. On top of that, for Product, Order, Customer, Collection, and Location you can now subscribe to metafield changes themselves by namespace and key. No more handlers that take every update and diff it yourself.
Even when you only wanted to act "when this one metafield changes," you had to subscribe to the resource's entire update and compare payloads in your handler.
List the target metafield under triggers and the event fires only when that value changes. You then query the changed resource yourself and receive just the data you need.
| Resource | Metafield subscriptions | Notes |
|---|---|---|
| Product | Supported | ProductVariant metafields are also via the Product topicsupported |
| Order | Supported | — |
| Customer | Supported | — |
| Collection | Supported | — |
| Location | Supported | — |
| Anything else | Not supported | For unsupported topics, keep using webhooks as well (the two can coexist in the same shopify.app.toml ) |
$app metafields and standard metafields are both supported.App-reserved namespaces and merchant-defined ones such as custom can be listed side by side in the same subscription.query_variables. Since "which metafield changed" is passed into the query as a variable,a single query definition can be reused across multiple metafields. You don't need to write a separate query for each trigger.The official example: target two Product metafields and fetch whichever one actually changed with query_variables .
[events] api_version = "unstable" [[events.subscription]] handle = "product_material_sync" topic = "Product" actions = ["update"] triggers = [ "product.metafield(namespace: 'custom', key: 'material').value", "product.metafield(namespace: '$app', key: 'sourcing_status').value" ] uri = "/events/product" query = """ query ProductMetafieldSync( $productId: ID! $metafieldNamespace: String! $metafieldKey: String! ) { product(id: $productId) { id title metafield(namespace: $metafieldNamespace, key: $metafieldKey) { namespace key value type } } } """
product.metafield(namespace: ..., key: ...).value — that's the format.custom -style regular namespaces and $app can be listed in the same array.
Write the GraphQL query inside the toml and let your app decide the shape of the payload. If you turn namespace / key into variables, you can share it across multiple triggers.
In this example, actions = ["update"].handle is also included in the payload as-is, so you can use it as a routing identifier.
With the configuration above, this is the payload delivered when custom.material changes.
{
"topic": "Product",
"action": "update",
"handle": "product_material_sync",
"data": {
"product": {
"id": "gid://shopify/Product/1234567890",
"title": "Canvas Tote",
"metafield": {
"namespace": "custom",
"key": "material",
"value": "Cotton",
"type": "single_line_text_field"
}
}
},
"fields_changed": [
"product.metafield(namespace: 'custom', key: 'material').value"
],
"query_variables": {
"productId": "gid://shopify/Product/1234567890",
"metafieldNamespace": "custom",
"metafieldKey": "material"
}
}
| Key | Contents | Where it's useful in your handler |
|---|---|---|
topic / action | Product / update | High-level routing |
handle | The handle of the subscription | Identifies which subscription config it came from |
data | The result of the query written in the toml | Can be fed straight into your business logic |
fields_changed | Array of the field paths that actually changed | Branching when multiple triggers are bundled into one subscription |
query_variables | The actual values of the variables passed to the query | Reuse for logs, idempotency keys, and as input when re-fetching |
Metafield change events for supported resources are also delivered to you. Nothing is missed, but the notification volume is at its highest.
If you specify the parent, metafield change events arrive in the same way. The scope widens while your existing subscription config stays as is.
Adding a metafield trigger that explicitly specifies namespace and key meansonly that changeis delivered. Notification volume stays minimal.
The appmust have access to that metafield— that is the condition for both subscribing and receiving data. Without permission, you can neither subscribe nor receive query results.
Events is in developer preview and isavailable only on the unstable API version. No timeline for general availability is given.
For topics Events doesn't cover yet, keep using webhooks as before.In the same shopify.app.toml , declare both Events and webhooks— that's the official guidance.
Variant metafields don't get their own topic; they'resupported through the Product topic. Write the subscription config on the Product side.
The "stash the previous value somewhere and compare" logic that used to live in the handler is no longer needed.From a stateful receiver to a stateless one. There's room to remove the previous-value cache in your KVS.
query_variables lets you consolidate queriesSince namespace / key can be taken as variables, multiple metafield triggers can be handled with a single query definition. More triggers no longer means the toml grows linearly.
$app and regular namespaces can be mixedApp-owned $app metafields and merchant-side custom can go in the same triggers array. "The app's internal state" and "the merchant's input" arrive through the same path.
Metafields the app has no access to can't be subscribed to or read.Missing scope / metafield access requests tend to surface as "the events never arrive,"so suspect permissions first when narrowing down the problem.
With no triggers, or with product as a parent trigger, apps subscribed this way will, with this change,now also fire on metafield changes. Endpoint call counts, billing, and job queue inflow may all grow more than expected, so monitor inbound volume after the release and narrow things down with target specifications if needed.
triggers , and usequery to fetch only the value and type of the metafields that changed.fields_changed to branch on which column to update in the destination system.$app metafields, status transitions written by the app itself can be picked up through the same mechanism.query to fetch the fields you need from the start. For topics that are not supported yet, keep webhooks in the same shopify.app.toml and use both side by side.shopify.dev/docs/api/events/latest/intentory-item
* URL exactly as written in the original (the original has the notation intentory-item )