
Shopify Metafields
- 59 installs
- 41 repo stars
- Updated March 13, 2026
- finsilabs/awesome-ecommerce-skills
Store custom data on Shopify products, orders, and customers using typed metafield definitions accessible from Liquid and the Storefront API.
About
Defines typed Shopify metafields to attach custom data to resources, readable from Liquid and the Storefront API. A developer uses it to extend Shopify's data model without external storage.
- Typed metafield definitions on any Shopify resource
- Accessible from Liquid and the Storefront API
Shopify Metafields by the numbers
- 59 all-time installs (skills.sh)
- Ranked #3,167 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/finsilabs/awesome-ecommerce-skills --skill shopify-metafieldsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 59 |
|---|---|
| repo stars | ★ 41 |
| Last updated | March 13, 2026 |
| Repository | finsilabs/awesome-ecommerce-skills ↗ |
What it does
Store custom data on Shopify products, orders, and customers using typed metafield definitions accessible from Liquid and the Storefront API.
Files
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
1. 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>.
2. 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"
);3. 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 %}4. 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" } });5. 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
productUpdatecalls 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
{
"context": "Tests whether the agent correctly sets up Shopify metafield definitions before writing data, uses the metafieldsSet mutation for bulk writes (not productUpdate), batches requests at the 25-per-mutation limit, serializes values as strings, and uses the correct namespace for merchant-managed data.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Definitions created first",
"max_score": 10,
"description": "The script calls metafieldDefinitionCreate (or equivalent definition setup) before any metafieldsSet write operations — definitions precede data writes in the code flow"
},
{
"name": "metafieldsSet used for writes",
"max_score": 10,
"description": "Metafield values are written using the metafieldsSet mutation, NOT via productUpdate or any other mutation"
},
{
"name": "Batch size limit respected",
"max_score": 12,
"description": "Metafield writes are split into batches of at most 25 items per metafieldsSet call — the code includes logic to chunk the payload at 25"
},
{
"name": "Number values as strings",
"max_score": 12,
"description": "Numeric values (weight, dimensions) are passed as JSON-serialized strings (e.g., \"450\" or \"32.5\") rather than raw numbers in the metafields array"
},
{
"name": "custom namespace used",
"max_score": 8,
"description": "All metafield definitions and write operations use the \"custom\" namespace, not an arbitrary or app-specific namespace"
},
{
"name": "visibleToStorefrontApi set",
"max_score": 10,
"description": "Metafield definitions include visibleToStorefrontApi: true so they are accessible from themes and the Storefront API"
},
{
"name": "Correct type for weight",
"max_score": 8,
"description": "The weight field definition uses the type \"number_integer\" (not number_decimal or string types)"
},
{
"name": "Correct type for dimensions",
"max_score": 8,
"description": "The dimension fields (length, width, height) use a numeric type — either number_decimal or number_integer"
},
{
"name": "PRODUCT ownerType",
"max_score": 7,
"description": "Metafield definitions specify ownerType: \"PRODUCT\""
},
{
"name": "Batch count correct",
"max_score": 8,
"description": "With 30 products × 4 metafields = 120 total metafields, the dry-run output shows at least 5 batch calls (ceil(120/25) = 5) to metafieldsSet"
},
{
"name": "Dry-run output produced",
"max_score": 7,
"description": "dry-run-output.txt exists and shows at least one example variable payload including ownerId, namespace, key, value, and type fields"
}
]
}
Outdoor Gear Product Data Enrichment
Problem/Feature Description
OutdoorPeak is an outdoor equipment retailer running a Shopify store with 150+ products. To comply with new EU product regulations, they need to surface structured technical data — weight, dimensions (length, width, height in cm), and primary material — for every product on both the product pages and via the storefront API for a companion mobile app.
Their developer team has been storing this data in product descriptions as unstructured text, which means it can't be queried programmatically or displayed consistently. The solution is to introduce properly typed custom fields that can be read in themes and through the API. The team has heard that fields not set up correctly won't show up in the storefront, and that bulk operations are needed to set data across all products without hitting rate limits.
You've been provided with a sample dataset of 30 products and asked to write a TypeScript script that sets up the required fields and populates them from the provided data. The script should run standalone without requiring a running Shopify store — it should output a dry-run log showing exactly what Admin API mutations it would execute (the mutation names, variables, and batch structure), but should NOT actually call any live API.
Output Specification
Produce the following files:
setup-metafields.ts— A TypeScript script that:
1. Defines and would create the necessary field type definitions (one per attribute) on the PRODUCT owner type 2. Bulk-sets metafield values for all 30 products using the provided data 3. Logs to stdout the mutations and batched variable payloads it would send (dry-run mode, no live API calls)
dry-run-output.txt— The actual stdout output produced by running the script (capture it withnpx ts-node setup-metafields.ts > dry-run-output.txtor equivalent). This must include the mutation names called and at least one example of the variable payload for each mutation type, showing the batch structure.
schema-notes.md— A short document listing each field's namespace, key, and type, plus a brief explanation of any batching strategy used.
Input Files
The following product data is provided as input. Extract it before beginning.
=============== FILE: inputs/products.json =============== [ { "id": "gid://shopify/Product/1001", "weight_grams": 450, "length_cm": 32.5, "width_cm": 18.0, "height_cm": 12.0, "material": "Aluminum" }, { "id": "gid://shopify/Product/1002", "weight_grams": 1200, "length_cm": 55.0, "width_cm": 30.0, "height_cm": 25.0, "material": "Nylon" }, { "id": "gid://shopify/Product/1003", "weight_grams": 85, "length_cm": 15.0, "width_cm": 8.0, "height_cm": 3.0, "material": "Stainless Steel" }, { "id": "gid://shopify/Product/1004", "weight_grams": 600, "length_cm": 40.0, "width_cm": 22.0, "height_cm": 15.0, "material": "Polycarbonate" }, { "id": "gid://shopify/Product/1005", "weight_grams": 320, "length_cm": 28.0, "width_cm": 14.0, "height_cm": 9.5, "material": "Carbon Fiber" }, { "id": "gid://shopify/Product/1006", "weight_grams": 2400, "length_cm": 70.0, "width_cm": 45.0, "height_cm": 30.0, "material": "Canvas" }, { "id": "gid://shopify/Product/1007", "weight_grams": 150, "length_cm": 20.0, "width_cm": 10.0, "height_cm": 5.0, "material": "Titanium" }, { "id": "gid://shopify/Product/1008", "weight_grams": 980, "length_cm": 48.0, "width_cm": 28.0, "height_cm": 20.0, "material": "Polyester" }, { "id": "gid://shopify/Product/1009", "weight_grams": 75, "length_cm": 12.0, "width_cm": 6.0, "height_cm": 2.5, "material": "Rubber" }, { "id": "gid://shopify/Product/1010", "weight_grams": 1800, "length_cm": 60.0, "width_cm": 35.0, "height_cm": 28.0, "material": "Gore-Tex" }, { "id": "gid://shopify/Product/1011", "weight_grams": 560, "length_cm": 38.0, "width_cm": 20.0, "height_cm": 14.0, "material": "Aluminum" }, { "id": "gid://shopify/Product/1012", "weight_grams": 3200, "length_cm": 80.0, "width_cm": 50.0, "height_cm": 35.0, "material": "Nylon" }, { "id": "gid://shopify/Product/1013", "weight_grams": 95, "length_cm": 16.0, "width_cm": 9.0, "height_cm": 4.0, "material": "Stainless Steel" }, { "id": "gid://shopify/Product/1014", "weight_grams": 720, "length_cm": 42.0, "width_cm": 24.0, "height_cm": 16.0, "material": "Polycarbonate" }, { "id": "gid://shopify/Product/1015", "weight_grams": 410, "length_cm": 30.0, "width_cm": 16.0, "height_cm": 11.0, "material": "Carbon Fiber" }, { "id": "gid://shopify/Product/1016", "weight_grams": 2800, "length_cm": 75.0, "width_cm": 48.0, "height_cm": 32.0, "material": "Canvas" }, { "id": "gid://shopify/Product/1017", "weight_grams": 180, "length_cm": 22.0, "width_cm": 12.0, "height_cm": 6.0, "material": "Titanium" }, { "id": "gid://shopify/Product/1018", "weight_grams": 1100, "length_cm": 52.0, "width_cm": 30.0, "height_cm": 22.0, "material": "Polyester" }, { "id": "gid://shopify/Product/1019", "weight_grams": 60, "length_cm": 10.0, "width_cm": 5.0, "height_cm": 2.0, "material": "Rubber" }, { "id": "gid://shopify/Product/1020", "weight_grams": 2100, "length_cm": 65.0, "width_cm": 38.0, "height_cm": 30.0, "material": "Gore-Tex" }, { "id": "gid://shopify/Product/1021", "weight_grams": 500, "length_cm": 35.0, "width_cm": 19.0, "height_cm": 13.0, "material": "Aluminum" }, { "id": "gid://shopify/Product/1022", "weight_grams": 1400, "length_cm": 58.0, "width_cm": 32.0, "height_cm": 26.0, "material": "Nylon" }, { "id": "gid://shopify/Product/1023", "weight_grams": 90, "length_cm": 14.0, "width_cm": 7.5, "height_cm": 3.5, "material": "Stainless Steel" }, { "id": "gid://shopify/Product/1024", "weight_grams": 680, "length_cm": 41.0, "width_cm": 23.0, "height_cm": 15.5, "material": "Polycarbonate" }, { "id": "gid://shopify/Product/1025", "weight_grams": 370, "length_cm": 29.0, "width_cm": 15.0, "height_cm": 10.0, "material": "Carbon Fiber" }, { "id": "gid://shopify/Product/1026", "weight_grams": 2600, "length_cm": 72.0, "width_cm": 46.0, "height_cm": 31.0, "material": "Canvas" }, { "id": "gid://shopify/Product/1027", "weight_grams": 160, "length_cm": 21.0, "width_cm": 11.0, "height_cm": 5.5, "material": "Titanium" }, { "id": "gid://shopify/Product/1028", "weight_grams": 1050, "length_cm": 50.0, "width_cm": 29.0, "height_cm": 21.0, "material": "Polyester" }, { "id": "gid://shopify/Product/1029", "weight_grams": 70, "length_cm": 11.0, "width_cm": 5.5, "height_cm": 2.2, "material": "Rubber" }, { "id": "gid://shopify/Product/1030", "weight_grams": 1950, "length_cm": 62.0, "width_cm": 36.0, "height_cm": 29.0, "material": "Gore-Tex" } ]
{
"context": "Tests whether the agent uses Metaobjects (not JSON metafields) for structured multi-field content, correctly uses metaobjectDefinitionCreate followed by metaobjectCreate, applies metaobjectUpdate for updates, and renders content correctly in Liquid using the appropriate access patterns.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Metaobjects used, not JSON",
"max_score": 15,
"description": "The FAQ and testimonial data is stored using Metaobject types (metaobjectDefinitionCreate + metaobjectCreate), NOT as a json-type metafield on the shop or product"
},
{
"name": "metaobjectDefinitionCreate for FAQs",
"max_score": 8,
"description": "A metaobjectDefinitionCreate mutation is called to define the FAQ type with fieldDefinitions (at minimum question and answer fields)"
},
{
"name": "metaobjectDefinitionCreate for testimonials",
"max_score": 8,
"description": "A separate metaobjectDefinitionCreate mutation is called to define the testimonial type with fieldDefinitions (at minimum customer_name and quote fields)"
},
{
"name": "fieldDefinitions have required flags",
"max_score": 7,
"description": "At least the core fields (question/answer for FAQs, customer_name/quote for testimonials) have required: true in their fieldDefinitions"
},
{
"name": "metaobjectCreate for entries",
"max_score": 10,
"description": "Each FAQ and testimonial entry is created using metaobjectCreate with a fields array — not via a bulk JSON import or metafieldsSet"
},
{
"name": "Liquid .value iteration",
"max_score": 10,
"description": "Liquid snippets access the list of entries using .value on the shop metafield (e.g., shop.metafields.custom.faq_entries.value) and iterate with a for loop"
},
{
"name": "Individual field access in Liquid",
"max_score": 10,
"description": "Liquid snippets access individual fields on each entry using .field_name.value pattern (e.g., faq.question.value) rather than treating the object as a plain hash"
},
{
"name": "metaobjectUpdate referenced",
"max_score": 8,
"description": "The script or schema notes include a metaobjectUpdate call or code example for updating an existing entry (not recreating it), or at minimum the dry-run log references metaobjectUpdate"
},
{
"name": "Rating field type correct",
"max_score": 7,
"description": "The testimonial rating field uses the type \"rating\" or \"number_integer\" (not plain text) in its field definition"
},
{
"name": "Type names in schema doc",
"max_score": 8,
"description": "content-schema.md documents the metaobject type names (e.g., faq_entry, testimonial) and the key+type for each field in both structures"
},
{
"name": "Dry-run log produced",
"max_score": 9,
"description": "dry-run-log.txt exists and shows at least one example of metaobjectDefinitionCreate payload and one metaobjectCreate payload"
}
]
}
FAQ and Testimonials Content System for Shopify Theme
Problem/Feature Description
StyleHouse is a Shopify fashion brand that manages their FAQ and customer testimonials content through their development team. Both types of content have multiple distinct fields: FAQs have a question and a detailed answer; testimonials have a customer name, quote, rating (1-5), and an optional product reference. Currently both are stored as hard-coded HTML in theme sections, which means content editors cannot update them without a developer.
The product manager wants both content types to be editable in the Shopify Admin UI by non-technical staff, and for the data to be rendered in two theme sections via Liquid. They specifically want to avoid building a separate CMS or storing everything as a single blob of JSON in one metafield, which would make the individual fields un-editable in the admin.
You've been asked to write: 1. A TypeScript setup script that creates the data structures and populates initial content entries using the provided seed data 2. A Liquid snippet for rendering FAQs on a page 3. A Liquid snippet for rendering testimonials
The script should output a dry-run log to stdout showing what mutations it would call (no live API calls needed). Use the provided seed data to populate the initial entries.
Output Specification
setup-content.ts— TypeScript script that creates the type definitions and entries for both FAQs and testimonials from the seed data, with dry-run logging to stdoutdry-run-log.txt— The stdout output from running the script, showing the mutation names and at least one example payload for each mutation typesections/faq.liquid— Liquid snippet rendering FAQ entries from shop metafieldssections/testimonials.liquid— Liquid snippet rendering testimonial entries from shop metafieldscontent-schema.md— Documents the type names, field keys and types for both content structures
Input Files
The following seed data is provided. Extract it before beginning.
=============== FILE: inputs/faqs.json =============== [ { "question": "What is your return policy?", "answer": "We accept returns within 30 days of purchase. Items must be unworn with original tags attached. Sale items are final sale.", "sort_order": 1 }, { "question": "Do you offer international shipping?", "answer": "Yes, we ship to over 40 countries. International orders typically arrive within 7-14 business days. Import duties are the responsibility of the customer.", "sort_order": 2 }, { "question": "How do I find my size?", "answer": "Use our size guide available on every product page. We recommend measuring your chest, waist, and hips and comparing to our size chart. When in doubt, size up.", "sort_order": 3 }, { "question": "Are your products ethically made?", "answer": "All our manufacturing partners are certified by the Fair Trade Foundation. We publish our full supplier list annually on our sustainability page.", "sort_order": 4 }, { "question": "Can I modify or cancel my order?", "answer": "Orders can be modified or cancelled within 2 hours of placement by contacting our support team. After that, orders enter fulfillment and cannot be changed.", "sort_order": 5 } ]
=============== FILE: inputs/testimonials.json =============== [ { "customer_name": "Sarah M.", "quote": "The quality is outstanding. I've been a customer for three years and every piece has lasted beautifully.", "rating": 5 }, { "customer_name": "James T.", "quote": "Sizing is true to chart and the fabric feels premium. Delivery was faster than expected.", "rating": 5 }, { "customer_name": "Priya K.", "quote": "Love the sustainable packaging. The coat I ordered is exactly as pictured and incredibly warm.", "rating": 4 }, { "customer_name": "Marcus B.", "quote": "Customer service resolved my exchange quickly and without hassle. Will definitely order again.", "rating": 5 } ]
{
"context": "Tests whether the agent correctly uses Shopify Storefront API metafield query patterns — explicitly requesting metafields by namespace and key, using reference vs references for single vs list references, applying inline fragments for typed references, and using the correct Liquid access patterns including .value and parse_json for list types.",
"type": "weighted_checklist",
"checklist": [
{
"name": "Explicit namespace+key query",
"max_score": 10,
"description": "Storefront API query requests each metafield using metafield(namespace: \"custom\", key: \"...\") syntax — metafields are not assumed to be returned automatically"
},
{
"name": "reference field for single ref",
"max_score": 10,
"description": "The sustainability_cert metaobject_reference is queried using the reference { ... } field (singular), not references"
},
{
"name": "references field for list ref",
"max_score": 10,
"description": "The compatible_accessories list.product_reference is queried using references(first: N) { edges { node { ... } } } (plural), not reference"
},
{
"name": "Metaobject inline fragment",
"max_score": 10,
"description": "The sustainability_cert reference uses an inline fragment \"... on Metaobject\" (or equivalent typed fragment) to access its fields"
},
{
"name": "Product inline fragment for accessories",
"max_score": 10,
"description": "The compatible_accessories list items use an inline fragment \"... on Product\" to access id, title, handle, or similar product fields"
},
{
"name": "Liquid .value for list metafield",
"max_score": 12,
"description": "In the Liquid snippet, compatible_accessories is accessed via .value (e.g., product.metafields.custom.compatible_accessories.value) and iterated with a for loop — raw value is NOT used directly as an array"
},
{
"name": "Liquid namespace.key pattern",
"max_score": 8,
"description": "Liquid snippet accesses metafields using the product.metafields.custom.key pattern for all three fields"
},
{
"name": "Plain value .value access",
"max_score": 10,
"description": "In the Storefront API response destructuring, the plain text field (tech_spec) is accessed via .value property on the metafield result"
},
{
"name": "Notes cover both contexts",
"max_score": 10,
"description": "notes.md explains the different access patterns for at least two of the three field types, covering both Storefront API and Liquid contexts"
},
{
"name": "references uses first: N",
"max_score": 10,
"description": "The references query for list.product_reference includes a first: parameter (e.g., first: 8) to limit results"
}
]
}
Headless Storefront Product Detail Module
Problem/Feature Description
TrekGear is migrating their Shopify store to a headless architecture using a custom TypeScript frontend. Their product pages need to display several types of enriched content that they have stored as custom data on products: a single sustainability certification (a reference to a custom object), a list of compatible accessory products (a list of product references), and a plain-text technical spec field. They also need the Storefront API queries to work alongside a Liquid fallback for their legacy theme.
The data is already stored in Shopify using the custom namespace. The developer team knows that the raw Storefront API returns custom data differently depending on whether it's a plain value, a single reference, or a list of references — and that getting this wrong means the fields silently return null. They need both a working TypeScript query module and a Liquid snippet that reads the same data correctly.
Produce a TypeScript module and a Liquid snippet that correctly query and render these three fields for a product. You do not need a live Shopify store — the TypeScript file should contain the complete query strings and show how the response would be destructured, but actual API calls are not required.
Output Specification
Produce the following files:
storefront-query.ts— A TypeScript module that exports a GraphQL query string (or query function) for fetching a product by handle with all three metafield types. Include code showing how the response data would be accessed for each field type (plain value, single reference, list of references).
product-metafields.liquid— A Liquid snippet that reads and renders the same three fields from aproductobject:- The sustainability certification reference
- The list of compatible accessories (as links)
- The technical spec text
notes.md— A brief explanation of the different access patterns used for each field type in both the Storefront API and Liquid contexts, and why these patterns differ.
The metafields to support are:
custom.sustainability_cert— points to a single certification entry (a custom object) with fieldsnameandbodycustom.compatible_accessories— a collection of up to 8 related products stored as referencescustom.tech_spec— a short plain-text technical specification string
{
"name": "finsi/shopify-metafields",
"version": "0.1.0",
"summary": "Custom data with metafield definitions, validation, and storefront access",
"skills": {
"shopify-metafields": {
"path": "SKILL.md"
}
}
}