06 · Metafields
Tipos, namespaces, y cómo los lees desde Liquid y GraphQL.
Los metafields son campos clave-valor tipados que puedes adjuntar a entidades existentes de Shopify: productos, variantes, pedidos, clientes, colecciones, páginas, blogs, artículos, y la tienda misma. Son la forma oficial de extender el modelo de datos de Shopify sin montar tu propia base de datos.
Qué es y por qué existe
Shopify gestiona entidades de negocio — Product, Order, Customer, Collection — con un esquema fijo. Durante años, los desarrolladores que necesitaban agregar datos adicionales recurrían a workarounds: meter información en el campo de descripción, abusar de los tags, o mantener una base de datos externa sincronizada con la tienda. Los metafields existen para eliminar esos workarounds.
Un metafield es un campo adicional que se adjunta a una entidad Shopify existente. Tiene tres partes obligatorias: un namespace (espacio de nombres para evitar colisiones), un key (nombre del campo), y un value tipado. Esa tipificación es la diferencia entre un metafield de 2026 y el sistema ad-hoc de hace cinco años — Shopify valida el valor antes de guardarlo, lo que significa que no puedes guardar texto donde se espera un entero, ni un color inválido donde se espera un color.
El resultado práctico es que puedes modelar datos de dominio específico directamente en Shopify. Para Brew Atlas, el catálogo de cafés tiene atributos que no existen en el modelo estándar de Shopify: origen geográfico, nivel de tueste, notas de cata, altitud de la finca. Esos datos viven como metafields en el producto — y Shopify los almacena, los valida, y los expone vía Liquid, GraphQL, y la Admin UI.
Definitions vs metafields ad-hoc
Antes de crear un metafield, tienes que decidir si vas a usar una definición o dejarlo como ad-hoc.
Una definición de metafield (metafield definition) es un esquema que declaras una vez para un namespace/key determinado. Cuando la definición existe, Shopify sabe el tipo esperado del valor, puede validarlo, puede mostrarlo en la Admin UI como un campo editable por el comerciante, y lo hace elegible como dynamic source en el Theme Editor.
Un metafield ad-hoc — creado directamente vía API sin definición previa — funciona técnicamente, pero tiene restricciones importantes: no aparece en la Admin UI (el comerciante no puede editarlo desde el panel), no puede ser referenciado como dynamic source en un schema de section, y es más difícil de documentar porque no existe un registro central de qué campos ad-hoc tiene tu aplicación.
La regla práctica: si el metafield es para datos que el comerciante puede necesitar editar, siempre crea una definición. Si es para datos que solo tu aplicación escribe y lee (flags internos, datos de sincronización), un metafield ad-hoc puede ser aceptable — pero documéntalo.
El catálogo de tipos
El tipo de un metafield determina cómo Shopify valida y serializa el valor. La lista completa al 2026 incluye:
Texto:
single_line_text_field— texto de una línea, sin HTMLmulti_line_text_field— texto de múltiples líneasrich_text_field— texto enriquecido (HTML limitado, serializado como JSON)
Numéricos:
number_integer— entero con signonumber_decimal— número decimal
Booleano y fechas:
boolean— true o falsedate— fecha ISO 8601 (YYYY-MM-DD)date_time— fecha y hora ISO 8601
Referencias y media:
url— URL absolutacolor— color en formato hexadecimal (#RRGGBB)file_reference— referencia a un archivo en la biblioteca de medios de Shopifypage_reference,product_reference,variant_reference,collection_reference— referencias a entidades de Shopifymetaobject_reference— referencia a una entrada de metaobjeto (verás esto en la sección 07)
Medidas:
weight,volume,dimension— con valor y unidadrating— número decimal con escala mínima y máxima
Dinero:
money— monto en la moneda de la tienda
Listas:
list.single_line_text_field,list.number_integer,list.product_reference, etc. — la mayoría de los tipos tienen una variantelist.*que almacena un array de valores del mismo tipo.
Namespaces
El namespace agrupa metafields y previene colisiones entre diferentes aplicaciones. Hay tres categorías:
custom — el namespace por defecto para datos que crea el comerciante directamente desde la Admin UI o con tu app. Los metafields en custom persisten cuando una app se desinstala.
$app: — prefijo especial para apps. El CLI de Shopify transforma $app:tasting_notes en un namespace interno que es privado a tu app. Cuando la app se desinstala, estos metafields se eliminan automáticamente. La declaración en shopify.app.toml te permite desplegar las definiciones con shopify app deploy.
shopify: — reservado para uso interno de Shopify. No puedes crear metafields en este namespace.
Para Brew Atlas, los datos del catálogo de café van en custom porque el comerciante tiene que poder editarlos desde la Admin UI. Si tu app maneja datos internos de sincronización o flags de estado, $app: es la elección correcta.
Acceso desde Liquid
Leer un metafield desde Liquid es directo:
{%- comment -%} ~/proyectos/shopify/brew-atlas/brew-atlas-theme/snippets/product-card.liquid {%- endcomment -%}
{%- liquid
assign roast = product.metafields.custom.roast_level.value
assign origin = product.metafields.custom.origin.value
-%}
<article class="card">
{%- comment -%} ... resto del card ... {%- endcomment -%}
{%- if roast != blank -%}
<span class="badge badge--roast badge--roast-{{ roast }}">
{{ roast | replace: '-', ' ' | capitalize }}
</span>
{%- endif -%}
{%- if origin != blank -%}
<p class="card__origin">{{ origin }}</p>
{%- endif -%}
</article>El path product.metafields.custom.roast_level devuelve el objeto metafield completo (con propiedades value, type, namespace, key). Para acceder solo al valor, agrega .value. El filtro | metafield_tag existe para renderizar ciertos tipos (como file_reference o rating) con el HTML apropiado automáticamente.
Para metafields de tipo list.*, el valor es un array que puedes iterar:
{%- comment -%} ~/proyectos/shopify/brew-atlas/brew-atlas-theme/snippets/tasting-notes.liquid {%- endcomment -%}
{%- assign notes = product.metafields.custom.tasting_notes.value -%}
{%- if notes != blank -%}
<ul class="tasting-notes">
{%- for note in notes -%}
<li class="tasting-notes__item">{{ note }}</li>
{%- endfor -%}
</ul>
{%- endif -%}Acceso desde GraphQL — Storefront API
El Storefront API acepta un array de identifiers para traer múltiples metafields en una sola query:
# ~/proyectos/shopify/brew-atlas/brew-atlas-app/src/queries/product-metafields.graphql
query ProductMetafields($handle: String!) {
product(handle: $handle) {
title
metafields(identifiers: [
{ namespace: "custom", key: "tasting_notes" }
{ namespace: "custom", key: "roast_level" }
{ namespace: "custom", key: "origin" }
{ namespace: "custom", key: "altitude_masl" }
]) {
key
value
type
}
}
}El campo metafields en el Storefront API devuelve un array con un elemento por identifier solicitado. Si un metafield no existe para ese producto, la posición correspondiente en el array es null. Siempre verifica null antes de usar el valor.
Acceso desde GraphQL — Admin API
Desde el Admin API puedes leer metafields con la sintaxis de selector directo:
# ~/proyectos/shopify/brew-atlas/brew-atlas-app/src/queries/admin-products-origin.graphql
query {
products(first: 10) {
edges {
node {
id
title
metafield(namespace: "custom", key: "origin") {
value
type
}
}
}
}
}El Admin API también expone metafields(first: N) como conexión paginada si necesitas leer todos los metafields de un recurso. El selector metafield(namespace, key) es más preciso cuando sabes exactamente qué campo necesitas.
Dynamic sources en el Theme Editor
Cuando tienes una definición de metafield creada, puedes conectar los settings de una section a ese metafield directamente desde el Theme Editor. En el schema de una section, un setting de tipo text puede recibir su valor desde product.metafields.custom.origin sin que el comerciante tenga que escribir el valor manualmente — lo toma del metafield del producto.
Esto es poderoso porque separa la configuración del tema de los datos del catálogo. El theme editor solo conecta la fuente; los datos viven en el producto y son editables desde el panel de Admin, no desde el Editor.
Puentes mentales
Los metafields son a Shopify lo que las columnas custom serían a una tabla de base de datos que no controlas. Piensa en Shopify como un SaaS: la tabla products es de ellos — tú no puedes hacer ALTER TABLE products ADD COLUMN roast_level VARCHAR(20). Lo que sí puedes hacer es agregar metafields, que son equivalentes a esas columnas extra, pero con validación de tipos integrada y sin migraciones.
La distinción entre definición y ad-hoc es análoga a tener un esquema declarado vs guardar datos sin esquema en un campo JSON genérico. El sistema funciona en ambos casos, pero el esquema declarado te da validación automática, visibilidad en la UI, y descubrimiento para otros desarrolladores del equipo.
El namespace $app: con cleanup automático en desinstalación es el equivalente de namespacing de módulos — tu app tiene un espacio de nombres aislado, y cuando el módulo se elimina, sus datos también desaparecen.
Estado 2026
Los metafields tienen API estable. Lo relevante al 2026:
- El tipo
rich_text_fieldserializa su contenido como JSON (no como HTML puro) usando el formato de Shopify. Cuando lo imprimes en Liquid, usa{{ metafield.value }}directamente — Shopify lo convierte a HTML automáticamente en ese contexto. - El tipo
metaobject_referencepermite referenciar una entrada de metaobjeto, creando relaciones entre entidades. Esto es lo que conecta un Product con una Finca en Brew Atlas — lo verás en detalle en la sección 07. - Las validaciones en definiciones permiten reglas como lista de valores permitidos, rangos numéricos, y patrones de regex para texto. Las validaciones se declaran en la definición y Shopify las aplica antes de guardar cualquier valor.
Sintaxis y anatomía
Ejemplo completo: definición de metafield creada vía Admin API y lectura completa desde Liquid y GraphQL.
Crear una definición (Admin API, mutation):
# Ejecutada una sola vez para configurar el namespace/key en la tienda
mutation CreateMetafieldDefinition($definition: MetafieldDefinitionInput!) {
metafieldDefinitionCreate(definition: $definition) {
createdDefinition {
id
name
namespace
key
type { name }
}
userErrors { field message }
}
}Con variables:
{
"definition": {
"name": "Nivel de tueste",
"namespace": "custom",
"key": "roast_level",
"type": "single_line_text_field",
"ownerType": "PRODUCT",
"validations": [
{
"name": "choices",
"value": "[\"light\",\"medium-light\",\"medium\",\"medium-dark\",\"dark\"]"
}
]
}
}Una vez que la definición existe, Shopify valida cualquier valor que intentes guardar en ese campo contra la lista de choices. Si envías "extra-dark", la mutación devuelve un userError — no un error HTTP.
Proyecto · Brew Atlas
Esta sección no requiere comandos de terminal. Vas a crear las definiciones de metafield desde el Admin de tu dev store (Settings → Custom data → Products) y editar un snippet en el scaffold local del theme.
Brew Atlas necesita cinco metafields en el recurso product para representar las características del café.
Qué vas a crear/tocar (archivos):
~/proyectos/shopify/brew-atlas/brew-atlas-theme/snippets/product-card.liquid— agregar lectura deroast_levelyorigincomo badge y texto de procedencia.
Dónde crear los metafields: Las definiciones de metafield se crean en el Admin de tu dev store, no en archivos del proyecto. Navega a: Settings → Custom data → Products → Add definition y crea los cinco campos:
custom.origin—single_line_text_field— país y región de origen (ej: “Colombia, Huila”)custom.roast_level—single_line_text_fieldcon validación dechoices: light, medium-light, medium, medium-dark, darkcustom.tasting_notes—list.single_line_text_field— notas de cata del cafécustom.altitude_masl—number_integer— altitud de la finca en metros sobre el nivel del marcustom.farm—metaobject_reference— referencia a la Finca (metaobjeto que se define en la sección 07)
Comandos a ejecutar: ninguno. Todo el trabajo de esta sección es en el Admin UI y en el snippet del theme.
Edita ~/proyectos/shopify/brew-atlas/brew-atlas-theme/snippets/product-card.liquid con el siguiente contenido actualizado que incluye el badge de tueste:
{%- comment -%} ~/proyectos/shopify/brew-atlas/brew-atlas-theme/snippets/product-card.liquid {%- endcomment -%}
{%- liquid
assign price = product.price | money
assign sold_out = false
unless product.available
assign sold_out = true
endunless
if product.compare_at_price > product.price
assign on_sale = true
assign savings = product.compare_at_price | minus: product.price | money
else
assign on_sale = false
endif
assign roast = product.metafields.custom.roast_level.value
assign origin = product.metafields.custom.origin.value
-%}
<article class="card {%- if sold_out %} card--sold-out{%- endif -%} {%- if on_sale %} card--on-sale{%- endif -%}">
<a href="{{ product.url }}" class="card__link">
{%- if product.featured_image -%}
<div class="card__media">
<img
src="{{ product.featured_image | image_url: width: 600 }}"
srcset="{{ product.featured_image | image_url: width: 300 }} 300w,
{{ product.featured_image | image_url: width: 600 }} 600w,
{{ product.featured_image | image_url: width: 900 }} 900w"
sizes="(min-width: 768px) 300px, 100vw"
width="600"
height="600"
alt="{{ product.featured_image.alt | escape }}"
loading="lazy"
/>
</div>
{%- endif -%}
<div class="card__info">
{%- if show_vendor and product.vendor != blank -%}
<p class="card__vendor">{{ product.vendor }}</p>
{%- endif -%}
<h3 class="card__title">{{ product.title }}</h3>
{%- if origin != blank -%}
<p class="card__origin">{{ origin }}</p>
{%- endif -%}
<div class="card__pricing">
<span class="price {%- if on_sale %} price--sale{%- endif -%}">
{{ price }}
</span>
{%- if on_sale -%}
<span class="price price--compare">
{{ product.compare_at_price | money }}
</span>
<span class="badge badge--sale">Ahorra {{ savings }}</span>
{%- endif -%}
{%- if sold_out -%}
<span class="badge badge--sold-out">Agotado</span>
{%- endif -%}
</div>
{%- if roast != blank -%}
<span class="badge badge--roast badge--roast-{{ roast }}">
{{ roast | replace: '-', ' ' | capitalize }}
</span>
{%- endif -%}
</div>
</a>
</article>Si usas la Storefront API desde la app, guarda la query en ~/proyectos/shopify/brew-atlas/brew-atlas-app/src/queries/product-with-metafields.graphql:
# ~/proyectos/shopify/brew-atlas/brew-atlas-app/src/queries/product-with-metafields.graphql
query ProductWithMetafields($handle: String!) {
product(handle: $handle) {
id
title
handle
metafields(identifiers: [
{ namespace: "custom", key: "origin" }
{ namespace: "custom", key: "roast_level" }
{ namespace: "custom", key: "tasting_notes" }
{ namespace: "custom", key: "altitude_masl" }
]) {
key
value
type
}
}
}Errores comunes
Los metafields con namespace $app: se eliminan automáticamente cuando la app se desinstala. Los metafields en custom persisten. Esto no es configurable — es por diseño. Si usas $app: para datos que el comerciante necesita conservar (por ejemplo, notas del editor sobre cada producto), esos datos desaparecen en la desinstalación. Para datos de larga vida que pertenecen al comerciante, usa custom.
No uses el namespace shopify:. Está reservado para uso interno de Shopify. Las mutaciones que intentan escribir en ese namespace fallan con un error de permisos. Si ves metafields en shopify: en la API, son datos de Shopify — solo lectura.
Los metafields ad-hoc (sin definición previa) no aparecen en la Admin UI. Si creas un metafield directamente vía API sin crear la definición correspondiente, el comerciante no va a poder verlo ni editarlo desde el panel. No es un bug — es que la Admin UI solo muestra los campos para los que existe una definición registrada. Para debug, puedes leerlos desde la API; para uso en producción, crea siempre la definición.
Para leer todos los metafields de un recurso y descubrir qué campos existen (útil al trabajar con una tienda que tiene metafields ad-hoc heredados), usa metafields(first: 20) sin filtrar por namespace/key. Desde Liquid, {{ product.metafields | json }} imprime el objeto completo de metafields del producto para depuración.
Checklist senior
- Puedes explicar la diferencia entre una definición de metafield y un metafield ad-hoc, y cuándo usar cada uno
- Conoces el catálogo de tipos principales y puedes elegir el correcto para un caso de negocio dado
- Entiendes la diferencia entre los namespaces
custom,$app:yshopify:y el comportamiento de cleanup de cada uno - Puedes leer un metafield en Liquid con
product.metafields.namespace.key.valuey manejar el caso en que el valor esblank - Sabes la sintaxis del Storefront API para traer múltiples metafields con
identifiers: [...] - Puedes escribir una mutation de Admin API para crear una definición de metafield con validaciones
- Entiendes que
userErrorsen las mutations de Admin API son errores de validación, no errores HTTP - Sabes qué es un dynamic source y cómo los metafields con definición lo habilitan en el Theme Editor
Quiz · ¿Lo tenés claro?
-
1. ¿Qué ventaja concreta tiene una definición de metafield sobre un metafield ad-hoc?
Definición = validación + UI en Admin + dynamic source en Theme Editor. Los ad-hoc se guardan pero sin validación y sin UI amigable. Ambos son accesibles desde Liquid/API.
-
2. ¿Qué comportamiento tiene el namespace `$app:` respecto a cleanup?
`$app:<namespace>` es privado de la app y se limpia en desinstalación. `custom` y `shopify:` persisten. Usar `$app:` evita basura en la tienda del merchant cuando te desinstalen.
-
3. En Liquid quieres leer un metafield y mostrar un fallback si está vacío. ¿Cómo?
Tanto el `if != blank` como el `| default` con verificación funcionan. El patrón importante es acceder al `.value` (el metafield es un objeto wrapper) y manejar el caso `blank` cuando el producto no tiene ese metafield asignado.
-
4. Desde el Storefront API quieres traer 3 metafields específicos de un producto. ¿Cuál es el patrón?
El Storefront API usa `metafields(identifiers: [...])` — devuelve un array en el mismo orden del input, con `null` para metafields inexistentes. Hay que filtrar nulls antes de mapear.
-
5. Una mutation del Admin API para crear un metafield devolvió status 200 pero el metafield no aparece. ¿Primera cosa que revisas?
Patrón crítico de Admin API: `userErrors: [{ field, message }]`. Un 200 con `userErrors` no vacío = error de validación. Hay que verificar explícitamente; no hacerlo es la causa #1 de 'parece que funciona pero no guarda nada'.
Siguiente
Con los metafields modelando los atributos del producto, el siguiente paso es modelar entidades completamente nuevas — aquellas que no son productos, variantes, ni ninguna de las entidades estándar de Shopify. Para Brew Atlas, la Finca (el lugar físico donde se produce el café) es una de esas entidades. Eso es el territorio de los metaobjetos.