Why metafields, not tags
Product tags used to be the go-to way to attach custom data to Shopify products. They still exist, but metafields have surpassed them for structured data: metafields are typed (integer vs string vs date vs file), validated (min/max, allowed values), and namespaced (no collisions across apps). Tags are unstructured strings, easy to overwrite, and impossible to query efficiently at scale. Migrate all personalization data to metafields.
Metafield definitions (the schema layer)
Define a metafield once, then set values against that definition on many resources. Definitions enforce type + validation + visibility across every resource of the owner type:
mutation CreateDefinition {
metafieldDefinitionCreate(definition: {
namespace: "personalization"
key: "engraving_max_chars"
name: "Engraving max characters"
description: "Maximum characters allowed in the engraving field"
type: "number_integer"
ownerType: PRODUCTVARIANT
validations: [
{ name: "min", value: "1" }
{ name: "max", value: "100" }
]
access: {
admin: MERCHANT_READ_WRITE
storefront: PUBLIC_READ
}
}) {
createdDefinition { id }
userErrors { field message }
}
}
Metafield types
| Type | Use for | Example value |
|---|---|---|
single_line_text_field | Short labels, IDs | "engraving_v2" |
multi_line_text_field | Descriptions, print instructions | "Print in center, 300 DPI..." |
number_integer | Character limits, DPI, quantity | 300 |
number_decimal | Prices, weights (precise) | 5.99 |
boolean | Feature flags | true |
date_time | Lead time, cutoff dates | "2026-09-01T12:00:00Z" |
color | Hex swatches | "#008060" |
file_reference | Brand logos, print templates | gid://shopify/GenericFile/... |
list.single_line_text_field | Allowed fonts, allowed positions | ["Inter", "Playfair"] |
metaobject_reference | Reference a structured object | gid://shopify/Metaobject/... |
json | Nested/complex data | {"positions": [{...}]} |
Setting metafield values
mutation SetMetafields {
metafieldsSet(metafields: [
{
ownerId: "gid://shopify/ProductVariant/40123"
namespace: "personalization"
key: "engraving_max_chars"
value: "20"
type: "number_integer"
}
{
ownerId: "gid://shopify/ProductVariant/40123"
namespace: "personalization"
key: "allowed_fonts"
value: "[\"Inter\", \"Playfair Display\", \"Roboto\"]"
type: "list.single_line_text_field"
}
]) {
metafields { id key value }
userErrors { field message }
}
}
Metaobjects — structured personalization schemas
When a single metafield type isn't enough — e.g., a "personalization option" needs fields like name + type + max_chars + required + price_adjustment — use a metaobject. A metaobject is a Shopify-native custom type with its own fields:
mutation CreateMetaobjectDefinition {
metaobjectDefinitionCreate(definition: {
type: "personalization_option"
name: "Personalization option"
fieldDefinitions: [
{ key: "name", type: "single_line_text_field", validations: [{name:"required", value:"true"}] }
{ key: "field_type", type: "single_line_text_field" } // text | dropdown | upload
{ key: "max_chars", type: "number_integer" }
{ key: "required", type: "boolean" }
{ key: "price_adjustment", type: "number_decimal" }
]
access: { storefront: PUBLIC_READ }
}) {
metaobjectDefinition { id }
userErrors { field message }
}
}
Exposing metafields to the Storefront
Metafields with storefront: PUBLIC_READ access are queryable via the Storefront API. Without this flag, they're admin-only:
query VariantMetafields($id: ID!) {
productVariant(id: $id) {
id
metafield(namespace: "personalization", key: "engraving_max_chars") {
value
}
allowed: metafield(namespace: "personalization", key: "allowed_fonts") {
value // JSON-encoded array; parse client-side
}
}
}
Reading metafields in Liquid (themes)
<input type="text"
maxlength="{{ product.selected_variant.metafields.personalization.engraving_max_chars }}">
{% assign allowed = product.selected_variant.metafields.personalization.allowed_fonts %}
{% for font in allowed.value %}
<option value="{{ font }}">{{ font }}</option>
{% endfor %}
Rate limits + bulk operations
Metafield writes cost 10 points per call (of the ~1000 pt/min GraphQL budget). Bulk-updating 500 variants' metafields one at a time takes 5+ minutes. Use metafieldsSet with up to 25 metafields per call, or bulkOperationRunMutation for large-scale updates.
Namespace conventions
Reserve namespaces per feature: personalization, pod_routing, brand_assets, compliance. Never use the custom namespace for app data — that's for merchant-defined fields in the admin. Apps that pollute custom break the merchant's admin UX.
Frequently asked questions
What are metafields on Shopify?
Typed key-value pairs attached to Shopify resources (product, variant, order, customer, shop, market, company). Used for custom data that doesn't fit the built-in schema. Since 2024, metafields have full definitions with type validation, storefront access controls, and namespace organization.
Should I use tags or metafields for personalization data?
Metafields — always. Tags are unstructured strings prone to overwrites and typos. Metafields are typed, validated, and namespaced. Migrate any personalization data currently in tags to metafields.
How do I make a metafield readable from the Storefront API?
Set the metafield definition's access to storefront: PUBLIC_READ. Metafields without this flag are admin-only. Existing metafields without a definition require creating a definition first, then linking existing values.
What's the difference between metafields and metaobjects?
Metafields are single typed key-value pairs attached to a resource. Metaobjects are structured multi-field custom types you define — like a mini-schema. Use metaobjects when a personalization option has multiple related fields (name + type + max_chars + price).
What namespace should my personalization app use for metafields?
Reserve your own — e.g., my_app_personalization. Never use custom (reserved for merchant admin fields) or common names like options (collision risk with other apps).
How many metafields can I set at once via API?
metafieldsSet accepts up to 25 metafields per call. Beyond that, use bulkOperationRunMutation for staged imports of thousands of metafield values.
Do metafields have search/filter capability?
Yes — Shopify's admin search and Storefront API's metafieldValueFilter both support metafield-based filtering. Filterable metafields need to be defined with the appropriate index setting.
Best pattern for storing per-variant DPI requirements?
Metafield definition: namespace: "personalization", key: "dpi_min", type: "number_integer", owner: PRODUCTVARIANT, storefront: PUBLIC_READ. Set 300 (default), 600 (high-res), 1200 (special finishes) per variant.
Related reading
- Admin API for options — GraphQL reference
- Storefront API for variants — client-side reads
- Custom app for options — app architecture
- Webhook variant changes — react to metafield updates
Try it free on Shopify
Print It My Way uses typed metafield definitions for all personalization data — DPI, char limits, allowed fonts, POD routing — exposed via Storefront API for headless-compatible themes.
Install Print It My Way