skills/platform-shopify/shopify-metafields/SKILL.md
Store custom data on any Shopify resource — products, orders, customers — using typed metafield definitions accessible from Liquid and the Storefront API
npx skillsauth add finsilabs/awesome-ecommerce-skills shopify-metafieldsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
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 }
}
}
`);
// 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 } });
}
}
{% 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>
custom namespace for merchant-managed data — reserve other namespaces (e.g., app_name) for app-owned data that merchants shouldn't edit directlyvisibleToStorefrontApi: true on definitions that need to be read in themes or headless frontends — metafields are private by defaultmetafieldsSet — it accepts up to 25 metafields per mutation; use it instead of individual productUpdate calls for bulk operationsmetafield_tag filter in Liquid for rich text and file reference metafields — it renders the correct HTML element (img, p, etc.) based on the metafield typecustom.* for merchant, yourapp.* for app) and document keys used so developers can find them| 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 | parse_json filter in Liquid or .value property in Storefront API for list-type metafields |
| 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 |
tools
Let shoppers save products to a wishlist, share it with friends, and get notified when saved items come back in stock or drop in price
development
Build a themeable storefront with design tokens and CSS custom properties that supports white-labeling, multi-brand variants, and dark mode
development
Speed up product discovery with instant search suggestions, fuzzy typo matching, and category-aware results powered by Algolia or Elasticsearch
development
Build a mobile-first storefront with thumb-friendly navigation, sticky add-to-cart buttons, and touch-optimized components for high mobile conversion