The {% schema %} Contract

How a Vein section declares itself: name, kind, settings, and blocks.

A Vein section is a single template file with one {% schema %} block. The schema is the authority for the section itself: its settings, its blocks, and the editor metadata used to render its controls. Where a section may be placed — which surfaces accept it, and which are required — is owned separately by the theme's surface rules, not by the section schema. This page is the exact section contract the parser enforces.

Anatomy of a section#

A section file is template markup followed by a {% schema %} block containing valid JSON. The markup renders the section; the schema declares what a merchant can configure. A file has exactly one {% schema %} block. It may appear anywhere in the file — top, bottom, or between markup — and it must not be nested inside another tag (an if, a for). Markup before and after the schema renders normally; the schema itself produces no output. Putting it at the end is a common convention, not a requirement.

vein
<section class="hero" data-tf-field="hero">
  <h1>{{ settings.heading }}</h1>
  {% if settings.show_button %}
    <a href="{{ settings.button_url | default: '/' }}">{{ settings.button_label }}</a>
  {% endif %}
</section>
{% schema %}
{
  "name": "hero",
  "kind": "section",
  "settings": [
    { "id": "heading", "type": "text", "default": "Welcome",
      "editor": { "label": "Heading", "group": "content" } },
    { "id": "show_button", "type": "checkbox", "default": true,
      "editor": { "label": "Show button", "group": "content" } },
    { "id": "button_label", "type": "text", "default": "Shop now",
      "editor": { "label": "Button label", "group": "content" } },
    { "id": "button_url", "type": "text",
      "editor": { "label": "Button link", "group": "content" } }
  ]
}
{% endschema %}

Top-level schema fields#

Field

Required

Meaning

name

Yes

The section id (unique). Referenced by a theme's surface to place this section.

kind

No

section (default) | layout | scene | snippet. Most sections are kind "section".

target

No

The surface slot this section is built for (e.g. global_body_start).

ctx_needs

No

Data the section reads at render — declared so the host loads exactly what's needed. Supported roots: shop, menus, localization, localization_prompt, restock_notifications, cart, wishlist, reviews, product, products, collection, category, page, policy, blog, blog_recent, blog_post, blog_category, vendor, brand, list, customer, customer_settings, csrf_token, form_error, auth_notice. Sub-keys select cheap tiers (cart.item_count, wishlist.item_count, reviews.stats). Shapes: see the CTX Runtime context reference.

settings

No

Section-level controls: an array of setting definitions.

blocks

No

Repeatable content units the merchant can add/reorder/remove.

Settings#

A setting is one configurable value. A setting is editor-visible only if it carries an "editor" block (label + optional group). A setting without "editor" is runtime-injected by the host and hidden from the merchant — absence of "editor" never means editable.

Key

Required

Meaning

id

Yes

Unique within the section (and within a block). Read in markup as settings..

type

Yes

The control type (see below).

default

No

Default value. The section should look designed on add — give real defaults.

options

For select

Array of { value, label } for select controls.

fields

For object

Array of { key, type, label } for an object (grouped) setting.

editor

To be editable

{ label, group }. Present ⟹ shown in the editor; absent ⟹ hidden/runtime-only.

Setting control types#

type

Renders as

Value stored

text

Single-line text input

string

textarea

Multi-line text input

string

richtext

Rich text editor (bold, italic, lists, links, headings)

HTML string — the platform sanitizes it to a safe allowlist at render and emits it as trusted markup; you just write {{ settings. }}

vendor

Search-and-pick a vendor

stable id — resolved to the live vendor object at render (see Resource pickers)

category

Search-and-pick a category

stable id — resolved to the live category object at render

brand

Search-and-pick a brand

stable id — resolved to the live brand object at render

product

Search-and-pick a product

stable id — resolved to the FULL listing card at render (title, url, vendor, featured_image, price / price_range in the visitor's market currency, available_for_sale, purchase_state); nil when hidden or deleted

collection

Search-and-pick a collection

stable id — resolved to the live collection (name, slug, url, image_url, product_count) at render

blog_post

Search-and-pick a blog post

stable id — resolved to the live post (title, slug, url, excerpt, image_url) at render

blog_category

Search-and-pick a blog category

stable id — resolved to the live category (name, slug, url, post_count) at render

page

Search-and-pick a page

stable id — resolved to the live page (title, slug, url) at render

policy

Pick a policy

the policy's fixed type (privacy, terms...) — resolved to the live policy (title, slug, url) at render

number

Number input

number

checkbox

Toggle

boolean

select

Dropdown (needs options)

one option value

color

Color picker

hex string

image

Image picker

file id → resolved to /assets/ at render

url

URL input

string

menu

Menu picker

menu handle (host-resolved to nav items)

object

Grouped sub-fields

object of { field key → value }, fields declared in "fields"

Resource picker settings#

A resource picker (vendor, category, brand, product, collection, blog_post, blog_category, page, policy) stores the picked entity's STABLE id — never its name or slug, which go stale. At render the host resolves the id to the live entity IN PLACE: {{ settings.vendor }} IS the vendor object. A deleted entity resolves to nil — guard and render a placeholder. List pickers (product_list, collection_list) store an ordered id list and resolve to a dense, ordered list of the same card objects. Menus, pickers, list pickers, and @dynamic sources are covered in depth — including their identical behavior on block settings — in Bound Settings.

vein
{% if settings.vendor %}
  <a href="{{ settings.vendor.url }}">{{ settings.vendor.name }}</a>
{% else %}
  {{ 'logo' | placeholder_svg }}
{% endif %}
{% schema %}
{
  "name": "featured-vendor",
  "settings": [
    { "id": "vendor", "type": "vendor",
      "editor": { "label": "Vendor", "group": "content" } }
  ]
}
{% endschema %}

Blocks#

Blocks are the repeatable content units inside a section — the merchant adds, reorders, hides, and removes them. A section declares which block types it allows; each block type has its own settings, validated exactly like section settings.

json
"blocks": {
  "max": 8,
  "default": ["slide", "slide"],
  "types": [
    { "type": "slide",
      "editor": { "label": "Slide" },
      "settings": [
        { "id": "image", "type": "image",
          "editor": { "label": "Image", "group": "content" } },
        { "id": "caption", "type": "text",
          "editor": { "label": "Caption", "group": "content" } }
      ] }
  ]
}

Field

Meaning

max

Maximum number of blocks (0 / omitted = unbounded).

default

Ordered list of block types created when the section is added. Every type listed must exist in types.

types

The allowed block types. Each: { type, editor, settings }.

Rendering blocks#

In the markup, iterate settings.blocks and branch on b.type. Read a block's own values from b.settings. Skip a hidden block with b.disabled. Wrap each block's output in the editor markers so it is selectable in the editor (these are no-ops on the live storefront).

vein
{% for b in settings.blocks %}
  {% unless b.disabled == true %}
    {% if b.type == 'slide' %}
      {{ b._editor_block_open }}
      <figure class="slide">
        {% if b.settings.image %}<img src="{{ b.settings.image }}" alt="{{ b.settings.caption }}">{% endif %}
        {% if b.settings.caption %}<figcaption>{{ b.settings.caption }}</figcaption>{% endif %}
      </figure>
      {{ b._editor_block_close }}
    {% endif %}
  {% endunless %}
{% endfor %}

Rules the parser enforces#

Rule

1

name is required; ids are unique within the section and within each block.

2

Every editable setting declares a supported control type (text, textarea, richtext, number, checkbox, select, color, image, object, menu, url, vendor, category, brand); an unknown type is rejected.

3

A select setting must declare options; an object setting must declare fields with unique non-empty keys.

4

Every exposed block type must declare at least one editable setting.

5

Every type in blocks.default must be declared in blocks.types.

6

There is exactly one {% schema %} block; it may appear anywhere in the file but must not be nested inside another tag.

7

These rules are enforced by the section parser when the file is loaded. An invalid schema fails at save/review time, not during storefront render.

8

Separately, the save/validation contract enforces the data shape: if a section declares blocks, its stored settings.blocks is required; and stored settings may contain only schema-declared keys — section settings at the section level, block settings per block type. Unknown keys are rejected on save.