Metafields
Read Shopify metafields from blocks and integrations.
Metafields are custom data attached to Shopify resources: a product, a collection, or the shop itself. They are the right home for any store-specific content your block renders: care instructions, size charts, announcement text. Merchants edit them in Shopify; your block picks up the change with no code deploy.
All metafield reads are asynchronous, so values are undefined until they load. Type every result with an interface and null-check before access.
Which hook to use
| Metafields on… | Read with | Import from |
|---|---|---|
| a product | useMultipleMetafields(handle, fields) | @gauntlet/state |
| a collection | useCollectionMetafields(id, fields) | @gauntlet/state |
| the shop | useShopifyQuery(query) | @gauntlet/state |
fields is an array of { namespace, key } descriptors.
Product metafields
useMultipleMetafields returns the values directly, keyed as result[namespace][key]. Define the shape you expect and assert it:
import { useMultipleMetafields } from '@gauntlet/state'
interface CareMetafields {
custom?: {
care_instructions?: string
specs?: string
}
}
const metafields = useMultipleMetafields(productHandle, [
{ namespace: 'custom', key: 'care_instructions' },
{ namespace: 'custom', key: 'specs' },
]) as CareMetafields | undefined
if (!metafields?.custom?.care_instructions) {
return null
}
let specs: Array<{ label: string; value: string }> = []
try {
specs = JSON.parse(metafields.custom.specs ?? '[]')
} catch {}The product handle comes from your block's props or the product hooks. One quirk to know: a dash in a namespace becomes an underscore in the result, so a my-fields namespace is read as metafields.my_fields.
Collection metafields
Same call shape, but the result holds metafield nodes, so read .value off each:
import { useCollectionMetafields } from '@gauntlet/state'
interface FooterMetafields {
custom?: { footer_text?: { value?: string } }
}
const metafields = useCollectionMetafields(collectionId, [
{ namespace: 'custom', key: 'footer_text' },
]) as FooterMetafields | undefined
const footer = metafields?.custom?.footer_text?.value
if (!footer) return nullOnly have the handle? useCollectionMetafieldsByHandle(handle, fields) takes the same arguments and returns the same shape.
Shop metafields
There is no dedicated hook. Write a small Storefront query against shop, alias each metafield, and run it with useShopifyQuery, typed with an interface:
import { useShopifyQuery } from '@gauntlet/state'
const SHOP_QUERY = `query {
shop {
announcement: metafield(namespace: "custom", key: "announcement") { value }
}
}`
interface ShopResult {
shop: {
announcement: { value: string } | null
}
}
const data = useShopifyQuery<ShopResult>(SHOP_QUERY)
const announcement = data?.shop?.announcement?.value
if (!announcement) return nulluseShopifyQuery returns the data or null. There are no separate loading or error flags, and no variables argument; interpolate values into the query string.
Working with the values
- A metafield value is always a string, numbers included. JSON-typed metafields arrive as a stringified blob, so parse inside
try/catch; merchant data is not guaranteed valid. - To resolve a linked resource instead of its id string, add
referenceto the field descriptor:{ namespace, key, reference: 'image' }(also'product','variant'). The result is the referenced object. - Match namespace and key exactly to what is configured in Shopify, minding the dash-to-underscore rule on product results.
Namespace conventions
Give your integration its own namespace, usually the integration name, and keep every metafield it reads under it. That prevents collisions with the store's custom namespace and with other integrations. It also makes your metafield requirements easy to state in the README: one namespace, a list of keys, and a type for each.

