Shopify Metafields
Overview
Metafields let you attach structured custom data to Shopify resources — products, variants, orders, customers, collections, and pages — without building a separate database. Metafield definitions enforce type validation (text, number, date, URL, JSON, file, product reference, etc.) and make metafields available in the Liquid template editor and Storefront API. Metaobjects extend this concept to create reusable, standalone custom data structures.
When to Use This Skill
- When products need additional attributes beyond Shopify's default fields (care instructions, dimensions, certifications)
- When storing per-customer data such as loyalty tier, subscription status, or B2B account number
- When building content-managed sections in a theme using metafield references (FAQs, size guides, feature callouts)
- When attaching order-level custom data from checkout (gift message, delivery instructions)
- When creating reusable structured content entries with Metaobjects (team members, press mentions, specs)
Core Instructions
Create metafield definitions via the Admin API
Definitions enforce type and make metafields storefront-accessible:
// Create a metafield definition for product care instructions
const response = await adminClient.request(`
mutation CreateMetafieldDefinition($definition: MetafieldDefinitionInput!) {
metafieldDefinitionCreate(definition: $definition) {
createdDefinition {
id
name
namespace
key
type { name }
}
userErrors { field message code }
}
}
`, {
variables: {
definition: {
name: "Care Instructions",
namespace: "custom",
key: "care_instructions",
type: "multi_line_text_field",
ownerType: "PRODUCT",
description: "Washing and care instructions for the product",
visibleToStorefrontApi: true,
// Optional: pin to product admin UI (use pinnedPosition in API version 2023-10+)
pinnedPosition: 1,
},
},
});
Available types: single_line_text_field, multi_line_text_field, number_integer, number_decimal, date, date_time, boolean, url, json, color, weight, volume, dimension, rating, file_reference, product_reference, variant_reference, collection_reference, page_reference, metaobject_reference, list.<type>.
Write metafields via the Admin API
// Set a metafield on a product
export async function setProductMetafield(
productId: string,
namespace: string,
key: string,
value: string,
type: string
) {
const response = await adminClient.request(`
mutation SetMetafield($metafields: [MetafieldsSetInput!]!) {
metafieldsSet(metafields: $metafields) {
metafields {
id key namespace value
}
userErrors { field message code }
}
}
`, {
variables: {
metafields: [
{
ownerId: productId,
namespace,
key,
value,
type,
},
],
},
});
return response.data.metafieldsSet;
}
// Example: Set care instructions on a product
await setProductMetafield(
"gid://shopify/Product/1234567890",
"custom",
"care_instructions",
"Machine wash cold. Tumble dry low. Do not bleach.",
"multi_line_text_field"
);
Read metafields in Liquid templates
Once a definition exists with visibleToStorefrontApi: true, metafields are available in Liquid via the metafields object:
{% comment %} product.metafields.namespace.key {% endcomment %}
{% if product.metafields.custom.care_instructions != blank %}
<div class="care-instructions">
<h3>Care Instructions</h3>
{{ product.metafields.custom.care_instructions | metafield_tag }}
</div>
{% endif %}
{% comment %} Access a product reference metafield {% endcomment %}
{% assign related = product.metafields.custom.related_product.value %}
{% if related %}
<a href="{{ related.url }}">{{ related.title }}</a>
{% endif %}
{% comment %} Access a list of file references (images) {% endcomment %}
{% for image in product.metafields.custom.gallery_images.value %}
<img src="{{ image | image_url: width: 800 }}" alt="{{ image.alt }}">
{% endfor %}
Read metafields via the Storefront API
// Query product with metafields in the Storefront API
const { data } = await storefront.request(`
query GetProductWithMetafields($handle: String!) {
product(handle: $handle) {
id
title
# Metafields must be explicitly requested by namespace + key
careInstructions: metafield(namespace: "custom", key: "care_instructions") {
value
type
}
relatedProduct: metafield(namespace: "custom", key: "related_product") {
reference {
... on Product {
id title handle
featuredImage { url altText }
}
}
}
certifications: metafield(namespace: "custom", key: "certifications") {
references(first: 5) {
edges {
node {
... on Metaobject {
id
fields {
key value
}
}
}
}
}
}
}
}
`, { variables: { handle: "my-product" } });
Create and use Metaobjects
Metaobjects are standalone custom data entries — useful for FAQs, testimonials, or any structured content:
// Create a Metaobject definition
await adminClient.request(`
mutation {
metaobjectDefinitionCreate(definition: {
name: "FAQ Entry"
type: "faq_entry"
fieldDefinitions: [
{ name: "Question", key: "question", type: "single_line_text_field", required: true }
{ name: "Answer", key: "answer", type: "multi_line_text_field", required: true }
{ name: "Sort Order", key: "sort_order", type: "number_integer" }
]
}) {
metaobjectDefinition { id type }
userErrors { field message }
}
}
`);
// Create a Metaobject entry
await adminClient.request(`
mutation {
metaobjectCreate(metaobject: {
type: "faq_entry"
fields: [
{ key: "question", value: "What is your return policy?" }
{ key: "answer", value: "We accept returns within 30 days of purchase." }
{ key: "sort_order", value: "1" }
]
}) {
metaobject { id handle }
userErrors { field message }
}
}
`);
Examples
Bulk metafield import for product attributes
// Import dimensions for multiple products at once (up to 25 per request)
export async function bulkSetDimensions(
products: Array<{ id: string; weight: number; length: number; width: number; height: number }>
) {
const metafields = products.flatMap(({ id, weight, length, width, height }) => [
{ ownerId: id, namespace: "custom", key: "weight_grams", value: weight.toString(), type: "number_integer" },
{ ownerId: id, namespace: "custom", key: "length_cm", value: length.toString(), type: "number_decimal" },
{ ownerId: id, namespace: "custom", key: "width_cm", value: width.toString(), type: "number_decimal" },
{ ownerId: id, namespace: "custom", key: "height_cm", value: height.toString(), type: "number_decimal" },
]);
// Process in batches of 25 (API limit)
for (let i = 0; i < metafields.length; i += 25) {
const batch = metafields.slice(i, i + 25);
await adminClient.request(`
mutation SetMetafields($metafields: [MetafieldsSetInput!]!) {
metafieldsSet(metafields: $metafields) {
userErrors { field message }
}
}
`, { variables: { metafields: batch } });
}
}
Render FAQ metaobjects in Liquid
{% comment %} sections/faq.liquid {% endcomment %}
{% assign faqs = shop.metafields.custom.faq_entries.value %}
<div class="faq-section">
<h2>Frequently Asked Questions</h2>
{% for faq in faqs %}
<details class="faq-item">
<summary>{{ faq.question.value }}</summary>
<div class="faq-answer">{{ faq.answer.value | newline_to_br }}</div>
</details>
{% endfor %}
</div>
Best Practices
- Always create definitions before writing metafields — definitions enable type validation, storefront API access, and the Admin UI field editor; undefined namespace/key combinations appear as raw JSON
- Use the
custom namespace for merchant-managed data — reserve other namespaces (e.g., app_name) for app-owned data that merchants shouldn't edit directly
- Set
visibleToStorefrontApi: true on definitions that need to be read in themes or headless frontends — metafields are private by default
- Batch metafield writes with
metafieldsSet — it accepts up to 25 metafields per mutation; use it instead of individual productUpdate calls for bulk operations
- Use
metafield_tag filter in Liquid for rich text and file reference metafields — it renders the correct HTML element (img, p, etc.) based on the metafield type
- Prefer Metaobjects over JSON metafields for structured multi-field data — Metaobjects are strongly typed and content-editable in the Shopify Admin UI
- Document your namespaces — establish a convention (
custom.* for merchant, yourapp.* for app) and document keys used so developers can find them
Common Pitfalls
| Problem |
Solution |
| Metafield returns null in Storefront API |
The metafield definition must have visibleToStorefrontApi: true; update the definition if it was created without this flag |
metafields.custom.key is empty in Liquid |
Ensure a definition exists for the namespace/key; Liquid only exposes metafields with registered definitions |
| List metafield value is a JSON string, not array |
Use ` |
metafieldsSet fails with TYPE_MISMATCH |
The value must be a JSON-serialized string matching the type — for number_integer pass "42", not 42 |
| Metaobject fields not updating |
Use metaobjectUpdate mutation with the metaobject GID and provide the fields array; partial updates are supported |
| App namespace conflicts with another app |
Use your app's handle as the namespace prefix (e.g., myapp-handle) to avoid conflicts in shared stores |
Related Skills
- @shopify-admin-api
- @shopify-storefront-api
- @shopify-app-development
- @shopify-theme-development
- @custom-product-attributes
1---2name: shopify-metafields3description: Store custom data on any Shopify resource — products, orders, customers — using typed metafield definitions accessible from Liquid and the Storefront API4---56# Shopify Metafields78## Overview910Metafields let you attach structured custom data to Shopify resources — products, variants, orders, customers, collections, and pages — without building a separate database. Metafield definitions enforce type validation (text, number, date, URL, JSON, file, product reference, etc.) and make metafields available in the Liquid template editor and Storefront API. Metaobjects extend this concept to create reusable, standalone custom data structures.1112## When to Use This Skill1314- When products need additional attributes beyond Shopify's default fields (care instructions, dimensions, certifications)15- When storing per-customer data such as loyalty tier, subscription status, or B2B account number16- When building content-managed sections in a theme using metafield references (FAQs, size guides, feature callouts)17- When attaching order-level custom data from checkout (gift message, delivery instructions)18- When creating reusable structured content entries with Metaobjects (team members, press mentions, specs)1920## Core Instructions21221. **Create metafield definitions via the Admin API**2324 Definitions enforce type and make metafields storefront-accessible:2526 ```typescript27 // Create a metafield definition for product care instructions28 const response = await adminClient.request(`29 mutation CreateMetafieldDefinition($definition: MetafieldDefinitionInput!) {30 metafieldDefinitionCreate(definition: $definition) {31 createdDefinition {32 id33 name34 namespace35 key36 type { name }37 }38 userErrors { field message code }39 }40 }41 `, {42 variables: {43 definition: {44 name: "Care Instructions",45 namespace: "custom",46 key: "care_instructions",47 type: "multi_line_text_field",48 ownerType: "PRODUCT",49 description: "Washing and care instructions for the product",50 visibleToStorefrontApi: true,51 // Optional: pin to product admin UI (use pinnedPosition in API version 2023-10+)52 pinnedPosition: 1,53 },54 },55 });56 ```5758 Available types: `single_line_text_field`, `multi_line_text_field`, `number_integer`, `number_decimal`, `date`, `date_time`, `boolean`, `url`, `json`, `color`, `weight`, `volume`, `dimension`, `rating`, `file_reference`, `product_reference`, `variant_reference`, `collection_reference`, `page_reference`, `metaobject_reference`, `list.<type>`.59602. **Write metafields via the Admin API**6162 ```typescript63 // Set a metafield on a product64 export async function setProductMetafield(65 productId: string,66 namespace: string,67 key: string,68 value: string,69 type: string70 ) {71 const response = await adminClient.request(`72 mutation SetMetafield($metafields: [MetafieldsSetInput!]!) {73 metafieldsSet(metafields: $metafields) {74 metafields {75 id key namespace value76 }77 userErrors { field message code }78 }79 }80 `, {81 variables: {82 metafields: [83 {84 ownerId: productId,85 namespace,86 key,87 value,88 type,89 },90 ],91 },92 });93 return response.data.metafieldsSet;94 }9596 // Example: Set care instructions on a product97 await setProductMetafield(98 "gid://shopify/Product/1234567890",99 "custom",100 "care_instructions",101 "Machine wash cold. Tumble dry low. Do not bleach.",102 "multi_line_text_field"103 );104 ```1051063. **Read metafields in Liquid templates**107108 Once a definition exists with `visibleToStorefrontApi: true`, metafields are available in Liquid via the `metafields` object:109110 ```liquid111 {% comment %} product.metafields.namespace.key {% endcomment %}112113 {% if product.metafields.custom.care_instructions != blank %}114 <div class="care-instructions">115 <h3>Care Instructions</h3>116 {{ product.metafields.custom.care_instructions | metafield_tag }}117 </div>118 {% endif %}119120 {% comment %} Access a product reference metafield {% endcomment %}121 {% assign related = product.metafields.custom.related_product.value %}122 {% if related %}123 <a href="{{ related.url }}">{{ related.title }}</a>124 {% endif %}125126 {% comment %} Access a list of file references (images) {% endcomment %}127 {% for image in product.metafields.custom.gallery_images.value %}128 <img src="{{ image | image_url: width: 800 }}" alt="{{ image.alt }}">129 {% endfor %}130 ```1311324. **Read metafields via the Storefront API**133134 ```typescript135 // Query product with metafields in the Storefront API136 const { data } = await storefront.request(`137 query GetProductWithMetafields($handle: String!) {138 product(handle: $handle) {139 id140 title141 # Metafields must be explicitly requested by namespace + key142 careInstructions: metafield(namespace: "custom", key: "care_instructions") {143 value144 type145 }146 relatedProduct: metafield(namespace: "custom", key: "related_product") {147 reference {148 ... on Product {149 id title handle150 featuredImage { url altText }151 }152 }153 }154 certifications: metafield(namespace: "custom", key: "certifications") {155 references(first: 5) {156 edges {157 node {158 ... on Metaobject {159 id160 fields {161 key value162 }163 }164 }165 }166 }167 }168 }169 }170 `, { variables: { handle: "my-product" } });171 ```1721735. **Create and use Metaobjects**174175 Metaobjects are standalone custom data entries — useful for FAQs, testimonials, or any structured content:176177 ```typescript178 // Create a Metaobject definition179 await adminClient.request(`180 mutation {181 metaobjectDefinitionCreate(definition: {182 name: "FAQ Entry"183 type: "faq_entry"184 fieldDefinitions: [185 { name: "Question", key: "question", type: "single_line_text_field", required: true }186 { name: "Answer", key: "answer", type: "multi_line_text_field", required: true }187 { name: "Sort Order", key: "sort_order", type: "number_integer" }188 ]189 }) {190 metaobjectDefinition { id type }191 userErrors { field message }192 }193 }194 `);195196 // Create a Metaobject entry197 await adminClient.request(`198 mutation {199 metaobjectCreate(metaobject: {200 type: "faq_entry"201 fields: [202 { key: "question", value: "What is your return policy?" }203 { key: "answer", value: "We accept returns within 30 days of purchase." }204 { key: "sort_order", value: "1" }205 ]206 }) {207 metaobject { id handle }208 userErrors { field message }209 }210 }211 `);212 ```213214## Examples215216### Bulk metafield import for product attributes217218```typescript219// Import dimensions for multiple products at once (up to 25 per request)220export async function bulkSetDimensions(221 products: Array<{ id: string; weight: number; length: number; width: number; height: number }>222) {223 const metafields = products.flatMap(({ id, weight, length, width, height }) => [224 { ownerId: id, namespace: "custom", key: "weight_grams", value: weight.toString(), type: "number_integer" },225 { ownerId: id, namespace: "custom", key: "length_cm", value: length.toString(), type: "number_decimal" },226 { ownerId: id, namespace: "custom", key: "width_cm", value: width.toString(), type: "number_decimal" },227 { ownerId: id, namespace: "custom", key: "height_cm", value: height.toString(), type: "number_decimal" },228 ]);229230 // Process in batches of 25 (API limit)231 for (let i = 0; i < metafields.length; i += 25) {232 const batch = metafields.slice(i, i + 25);233 await adminClient.request(`234 mutation SetMetafields($metafields: [MetafieldsSetInput!]!) {235 metafieldsSet(metafields: $metafields) {236 userErrors { field message }237 }238 }239 `, { variables: { metafields: batch } });240 }241}242```243244### Render FAQ metaobjects in Liquid245246```liquid247{% comment %} sections/faq.liquid {% endcomment %}248{% assign faqs = shop.metafields.custom.faq_entries.value %}249250<div class="faq-section">251 <h2>Frequently Asked Questions</h2>252 {% for faq in faqs %}253 <details class="faq-item">254 <summary>{{ faq.question.value }}</summary>255 <div class="faq-answer">{{ faq.answer.value | newline_to_br }}</div>256 </details>257 {% endfor %}258</div>259```260261## Best Practices262263- **Always create definitions before writing metafields** — definitions enable type validation, storefront API access, and the Admin UI field editor; undefined namespace/key combinations appear as raw JSON264- **Use the `custom` namespace for merchant-managed data** — reserve other namespaces (e.g., `app_name`) for app-owned data that merchants shouldn't edit directly265- **Set `visibleToStorefrontApi: true` on definitions** that need to be read in themes or headless frontends — metafields are private by default266- **Batch metafield writes with `metafieldsSet`** — it accepts up to 25 metafields per mutation; use it instead of individual `productUpdate` calls for bulk operations267- **Use `metafield_tag` filter in Liquid** for rich text and file reference metafields — it renders the correct HTML element (img, p, etc.) based on the metafield type268- **Prefer Metaobjects over JSON metafields** for structured multi-field data — Metaobjects are strongly typed and content-editable in the Shopify Admin UI269- **Document your namespaces** — establish a convention (`custom.*` for merchant, `yourapp.*` for app) and document keys used so developers can find them270271## Common Pitfalls272273| Problem | Solution |274|---------|----------|275| Metafield returns null in Storefront API | The metafield definition must have `visibleToStorefrontApi: true`; update the definition if it was created without this flag |276| `metafields.custom.key` is empty in Liquid | Ensure a definition exists for the namespace/key; Liquid only exposes metafields with registered definitions |277| List metafield value is a JSON string, not array | Use `| parse_json` filter in Liquid or `.value` property in Storefront API for list-type metafields |278| `metafieldsSet` fails with TYPE_MISMATCH | The `value` must be a JSON-serialized string matching the type — for `number_integer` pass `"42"`, not `42` |279| Metaobject fields not updating | Use `metaobjectUpdate` mutation with the metaobject GID and provide the `fields` array; partial updates are supported |280| App namespace conflicts with another app | Use your app's handle as the namespace prefix (e.g., `myapp-handle`) to avoid conflicts in shared stores |281282## Related Skills283284- @shopify-admin-api285- @shopify-storefront-api286- @shopify-app-development287- @shopify-theme-development288- @custom-product-attributes