{"title":"End-to-end store build sequence","slug":"agent-store-build-sequence","url":"https://support.storeconnect.com/articles/agent-store-build-sequence","url_markdown":"https://support.storeconnect.com/articles/agent-store-build-sequence.md","subtitle":null,"summary":"The order to build a StoreConnect store in, from design brief and store data through theme creation to preview and push for approval. Use it when standing up a whole store rather than changing part of one.","type":"AI","video_url":"","keywords":"store build, store setup, MCP tools, agent tools, design brief, fonts, brand colors, screenshot, theme creation, build order, media, products, categories, content blocks, menus, theme structure, preview, push for review, publish, StoreConnect CLI, agent workflow, end-to-end","last_modified":"2026-08-21T07:12:35+0000","body_markdown":"This article documents the complete sequence for building a StoreConnect store from scratch: connecting your tools, creating store data in the correct dependency order, creating a theme with the required file structure, and pushing the result for approval. Follow this sequence whether you are working interactively, running an automated build, or instructing an AI agent.\n\nNothing like this workflow exists as a single reference anywhere else — this article is the canonical guide.\n\n## Overview\n\nA StoreConnect store has two independently managed halves:\n\n1. **Store data** — products, categories, articles, pages, content blocks, menus, and media, all created and staged through the store's MCP tools (the same tools an AI agent uses once connected — see [Connect an AI agent to your store](connect-an-ai-agent-to-your-store)).\n2. **Theme** — Liquid templates, assets, and configuration that control how the data appears in the browser.\n\nBoth halves must be in place before a store is usable. The sections below walk through each in order.\n\n---\n\n## Phase 1: Connect your tools and plan the build\n\nThere is no bulk data importer and no `data/*.json` file format — every record in this article is created by calling an MCP tool (`create_product`, `create_category`, `create_page`, and so on) and staging it onto a content change. [Connect an AI agent to your store](connect-an-ai-agent-to-your-store) covers how to mint a token and connect an MCP client; once connected, the tools named in each phase below are what you call.\n\nThe StoreConnect CLI complements this rather than replacing it:\n\n- `sc theme pull` / `sc theme push` manage the theme half of the build (Phase 3 and Phase 4 below).\n- Read-only commands — `sc product list`, `sc category list`, `sc article list`, `sc page list`, `sc block list`, `sc media list`, `sc menu list` / `sc menu show` — let you inspect what already exists on the store before you build on top of it.\n\nThe CLI has no commands for creating or editing products, categories, articles, pages, content blocks, or menus — that is what the MCP tools are for.\n\nBefore you start calling tools, decide:\n\n- **What you're building** and the order in Phase 2 below — later records reference earlier ones.\n- **Where your images live.** Every media-referencing tool takes a public `url` — host each image somewhere reachable before you call `create_media`, `add_product_image`, `update_store` (`logo_url`), or `update_category` (`banner_url`).\n- **One content change or several.** Group everything for one build into a single content change by passing the same `content_change_id` returned by your first `create_*` call into every subsequent call, so the whole build previews and rolls back as a unit — see \"Same-draft cross-references\" later in this phase.\n\n---\n\n## Phase 2: Create store data in dependency order\n\nRecords are created in a specific order because later records reference earlier ones by the `id` each `create_*` tool returns. **Build in this exact sequence:**\n\n### Step 1 — Design brief\n\nBefore creating any content, call `set_design_brief` with the store's design intent — personality, palette, and type direction. Do this first: it is the single best defense against a store drifting into looking like several different sites, especially across sessions.\n\n`set_design_brief` takes one required parameter:\n- `brief` — a free-form JSON object. Suggested keys (none enforced — structure it however best captures the intent): `personality` (for example `\"warm, minimal, trustworthy\"`), `colors` (for example `{\"primary_intent\": \"earthy green, calming\"}`), `fonts` (for example `{\"heading\": \"serif, editorial\"}`), `layout_notes`, `imagery_style`, `do_not` (things to avoid).\n\n```json\n\n{\n  \"personality\": \"warm, minimal, trustworthy\",\n  \"colors\": {\"primary_intent\": \"earthy green, calming\"},\n  \"fonts\": {\"heading\": \"serif, editorial\"},\n  \"do_not\": \"no stock photos of laptops\"\n}\n```\n\nUnlike every other tool in this sequence, `set_design_brief` saves immediately — it is not staged on a content change and needs no review, because it is a memory aid for agents rather than storefront content. There is one brief per store; calling `set_design_brief` again replaces the whole thing.\n\nThe brief persists across sessions: `get_design_brief` reads it back, and it is appended to the start of every future session's server instructions automatically — yours or a different agent's. `push_content_change` also stamps a copy of it onto that push as a snapshot of the intent behind it; this stamp isn't a field on the Salesforce Content Change record today, so treat `get_design_brief` (or the server instructions) as the source of truth, not the Salesforce record.\n\n:::tip\nStarting a new session on a store that already has a brief? Call `get_design_brief` first (or just check your server instructions, which already include it) to pick up the design direction set earlier instead of guessing again.\n:::\n\n### Step 2 — Media\n\nCreate media first because most other content can reference it.\n\nCall `create_media` with:\n- `url` — required; a publicly accessible URL for the image (or other file). The platform downloads and processes it to Cloudinary, generating all nine named sizes (`thumb_url`, `medium_url`, `large_url`, `huge_url`, and so on)\n- `name` — required display name\n- `alt_text` — optional, for accessibility\n- `file_type` — `image` (default), `uploaded_video`, or `document`\n- `identifier` — optional stable handle for referencing this media from a theme\n\n```json\n\n{\n  \"url\": \"https://example.com/images/hero.jpg\",\n  \"name\": \"Hero image\",\n  \"alt_text\": \"Coffee farm at golden hour\"\n}\n```\n\nThe call returns an `id` you pass to other tools — `add_product_image`'s `media_id`, `create_content_block`'s `image_id` / `media_id`, and so on.\n\n:::tip\nSeveral tools also accept a public image `url` directly and stage the media record for you in the same call — `add_product_image`'s `url`, `update_category`'s `banner_url`, and `update_store`'s `logo_url`. You only need a standalone `create_media` call when you want to reuse one media record across several places, or need `alt_text`/`identifier` on it.\n:::\n\n### Step 3 — Categories\n\nCreate product categories after media, because a category's banner image references it.\n\n`create_category` takes:\n- `name` — required\n- `path` — required; URL slug (for example `single-origins`)\n- `display_name` — optional customer-facing name (defaults to `name`)\n\nOnce the category exists, call `update_category` to fill in `introduction_markdown` (supports Markdown), `meta_title`, `meta_description`, `meta_keywords`, and `banner_url` (a public image URL).\n\n:::note\nArticle categories are a separate concept from product categories in the platform, but there is no agent tool to create or assign them yet. If your build needs them, set them up in Salesforce directly.\n:::\n\n### Step 4 — Products\n\nCreate products after categories and media because products reference both.\n\n`create_product` stages the product and, in the same call, its price (`price`) and category memberships (`category_ids`). It automatically makes the product a visible, sellable master — it sets the master flag, marks it active, and sets the availability date to now — so you do not set those flags yourself.\n\nKey `create_product` fields:\n- `name` — required\n- `product_code` — SKU / product code\n- `slug` — URL slug for the product page; if you omit it, the product's URL falls back to its Salesforce id, so set one for a human-friendly URL\n- `description`, `summary_markdown` — Markdown supported on `summary_markdown`\n- `price` — a decimal in the store's currency (for example `24.00`, not cents); a product cannot be purchased without one\n- `category_ids` — array of category ids returned from your `create_category` calls; a product won't appear on a category page until this link exists\n- `meta_title`, `meta_description`, `meta_keywords` — SEO fields\n\n**What you must supply for storefront visibility:** because `create_product` already makes the product a sellable master, the only two things you need to provide are (a) a `price` and (b) at least one `category_ids` link. There are no separate tools for setting the master, active, or availability flags — they are handled for you.\n\n```json\n\n{\n  \"name\": \"Ethiopia Yirgacheffe\",\n  \"product_code\": \"ETH-YIRG-250\",\n  \"slug\": \"ethiopia-yirgacheffe\",\n  \"price\": 24.00,\n  \"category_ids\": [\"\u003cid-returned-from-create_category\u003e\"]\n}\n```\n\nAdd images afterward, one `add_product_image` call per image, each with the product's `id` plus either an existing `media_id` or a hosted `url`. Images render in the `position` order you give them; the first one you add is used as the product's primary/listing image.\n\nVariants (size, color, and other options) are a separate step: call `create_product_variant` with `master_product_id`, `name`, `variant_type` (for example `Size`), and `variant_value` (for example `Large`) — it stages the variant product, links it to its master, and can set its `price` in the same call.\n\n:::note\nFilterable product attributes (traits, such as Origin or Roast Level) are not yet exposed through agent or CLI tools — there is no `create_trait`-style tool. If your build needs them, configure trait types and assign values in Salesforce Setup directly.\n:::\n\n### Step 5 — Articles\n\nCreate articles after you're ready to link to them from pages or menus — articles don't currently depend on any other content type.\n\n`create_article` takes:\n- `title` and `path` — both required\n- `body_markdown` — the article body in Markdown\n- `author`, `subtitle`, `intro_markdown`, `summary_markdown`\n- `published` — whether the article is published once the change goes live\n- `meta_title`, `meta_description`, `meta_keywords`\n\n:::note\nArticle categories and hero images exist in the platform but there is no agent tool to set them yet. If your build needs them, assign them in Salesforce directly.\n:::\n\n### Step 6 — Pages\n\nCreate pages once you know what you're linking to from menus and content blocks.\n\n`create_page` takes:\n- `title` and `path` — both required; `path` is the page's URL path (for example `about-us`)\n- `body_markdown` — page content in Markdown\n- `subtitle`, `meta_title`, `meta_description`, `meta_keywords`\n- `visible`, `require_login`\n- `parent_id`, `position` — for nesting under another page\n\n:::tip\nThere's no special path value for the home page — create it like any other page (for example `path: \"home\"`), then use `update_store`'s `home_page_id` (Step 9 below) to link it as the store's root page.\n:::\n\n:::tip\nAgents can create a discovery file for AI crawlers and assistants at the well-known path `llms.txt` — call `create_page` with `path: \"llms.txt\"` and Markdown content describing the business. StoreConnect does not generate this file for you.\n:::\n\n### Step 7 — Content blocks\n\nCreate content blocks after pages and media because content blocks reference both.\n\nA content block is a structured section that appears on a page (for example, a hero banner, featured products grid, or text section). `create_content_block` takes:\n- `name` — required\n- `template` — block type (see [Theme snippets, blocks, and components](theme-snippets-blocks-components) for the full list of block types)\n- `title`, `subtitle`, `content_markdown`\n- `identifier` — optional stable handle for referencing the block from a theme\n- `image_id` — a media reference, used by `image`, `image_text_overlay`, and `image_beside_text` blocks\n- `media_id` — a downloadable-media reference, used by `media` blocks\n- `link_label`, `link_target` — call-to-action label and URL/path\n- `page_id` — places the block on that page\n- `position` — sort order on the page\n- `product_ids` — array of product references, used by `featured_products` blocks\n\n```json\n\n{\n  \"name\": \"Home hero\",\n  \"template\": \"image_text_overlay\",\n  \"title\": \"Welcome\",\n  \"subtitle\": \"Quality coffee, delivered.\",\n  \"image_id\": \"\u003cid-returned-from-create_media\u003e\",\n  \"link_label\": \"Shop Now\",\n  \"link_target\": \"/products\",\n  \"page_id\": \"\u003cid-returned-from-create_page\u003e\",\n  \"position\": 1\n}\n```\n\n### Step 8 — Menus\n\nCreate menus after categories and pages because menu items link to both.\n\nFirst call `create_menu` with a `name` (and optional `identifier` — the stable handle themes use, such as `header` or `footer`; defaults to a slug of the name). Then call `create_menu_item` once per link, with:\n- `menu_id` — the menu's returned id\n- `display_name` — required; the label shown in the navigation\n- exactly one target: `url` (external or relative), `page_id`, `article_id`, `product_id`, or `product_category_id`\n- `position`, `parent_id` — for nested navigation\n\n```json\n\n{\"name\": \"Header\", \"identifier\": \"header\"}\n```\n\n```json\n\n{\"menu_id\": \"\u003cid-returned-from-create_menu\u003e\", \"display_name\": \"Shop\", \"product_category_id\": \"\u003cid-returned-from-create_category\u003e\"}\n```\n\n```json\n\n{\"menu_id\": \"\u003cid-returned-from-create_menu\u003e\", \"display_name\": \"About\", \"page_id\": \"\u003cid-returned-from-create_page\u003e\"}\n```\n\n### Step 9 — Store linkage\n\nAfter your content exists, link the store to its home page and menus with the `update_store` agent tool:\n- `home_page_id` — the page id to use as the store's root page\n- `header_menu_id`, `footer_menu_id` — the menu ids for the header and footer navigation\n\nPass the `id` returned from the matching `create_page` / `create_menu` calls (or an existing live page/menu's id).\n\n### Step 10 — Set the brand palette, fonts, and logo\n\n**Colors** go through the store's global CSS, using the `custom_styles` parameter on `update_store`. Pass raw CSS that overrides the base theme's CSS custom properties on `:root`, as a flat hex value:\n\n```css\n\n:root {\n  --sc-color-primary: #2D4A2D;\n  --sc-color-secondary: #C4622D;\n}\n```\n\nHex alone is enough: the theme itself renders from `-h`/`-s`/`-l` HSL channel triplets (for example `--sc-color-primary-h`, `-s`, `-l`), and the platform derives those automatically from the hex value you set on `primary`, `secondary`, `error`, `sale`, and `bonus` at render time — you never need to compute or write the triplets yourself. This is idempotent: if a triplet is already declared anywhere in your `custom_styles` (hand-written or from an earlier compile), it's left alone.\n\nThe `custom_styles` value is the same field surfaced in Liquid as `current_store.global_css`, which the layout renders inside a `\u003cstyle\u003e` block — so the overrides apply on top of the base theme with no theme file required. (A custom CSS file uploaded with `set_theme_asset` is an alternative, theme-author route.)\n\nDo **not** use theme variables for branding: `set_theme_variables` keys such as `colors.primary` do **not** change the rendered palette. Theme variables are for behavioral settings (for example `products.per_page`).\n\n**Fonts** go through the dedicated `fonts` parameter on `update_store`, not `custom_styles`. Setting a bare `font-family` declaration in `custom_styles` sets the CSS variable but never loads the typeface, so nothing visibly changes — `fonts` is the way to both load and apply one:\n\n```json\n\n{\n  \"fonts\": {\n    \"heading\": {\"family\": \"Playfair Display\", \"weights\": [400, 700]},\n    \"body\": {\"family\": \"Inter\", \"weights\": [400, 600]},\n    \"source\": \"google\"\n  }\n}\n```\n\n- `heading.family` / `body.family` — required; letters, digits, and spaces only.\n- `heading.weights` / `body.weights` — optional arrays of integers; omit to use the default weight.\n- `source` — `\"google\"` loads the family from Google Fonts via `@import`; `\"system\"` assumes the family is already available (an OS/system font) and only sets the CSS variables.\n\nThis compiles a managed `/* sc:fonts */ … /* /sc:fonts */` section into `custom_styles` — for `source: \"google\"` it's an `@import url('https://fonts.googleapis.com/css2?family=...\u0026display=swap')` line plus a `:root` block setting `--sc-font-family` (body) and `--sc-font-family-heading` (headings), each with a generic fallback. Calling `update_store` with `fonts` again replaces just that managed section — the rest of your `custom_styles` is untouched, so switching fonts never leaves a stale `@import` behind.\n\nSet the store logo the same way, with `update_store`'s `logo_url` — a public image URL; StoreConnect stages the media for you.\n\n### Escape hatch: let the human take over visually\n\nAt any point in the build — not just at the end — the human may want to see or hand-tweak something visually rather than describe it to the agent. Call `get_editing_url` to get a one-click, pre-authenticated link:\n\n- `surface` — required; `\"builder\"` opens the visual editor on **this session's open draft** (creating one if none exists yet), so their edits land directly on it and your next `get_*`/`list_*` calls will see them. `\"console\"` opens the read-only operations console instead — no draft involved.\n- `path` — optional; a storefront path to land on inside the builder (ignored for console).\n\nIt returns `{url, expires_in_seconds, note}`. The sign-in window is short (`expires_in_seconds`) — call `get_editing_url` again for a fresh link if it goes stale before the human clicks it.\n\nThis requires a real user identity — the user-token or OAuth connection you set up in [Connect an AI agent to your store](connect-an-ai-agent-to-your-store) — because the link is signed for a specific Salesforce user. A store's 2-part system key has no associated Salesforce user and cannot mint this link.\n\n### Same-draft cross-references\n\nWhen building a store through agent tools, all the steps above can be grouped into a single content change. Within that change, every `create_*` call returns an `id` immediately — you can use that `id` in subsequent calls in the same change without waiting for the change to be committed. This means the full dependency-ordered build (media → categories → products → articles → pages → content blocks → menus → store linkage) fits inside one content change, which makes rollback straightforward and avoids partial-state windows.\n\n---\n\n## Phase 3: Create the theme\n\nWith store data in place, create the theme. A theme is a directory of Liquid templates and assets.\n\n:::tip\nIf you are using the `create_theme` agent tool, pass your existing `content_change_id` to fold the theme record creation into the same change as your store data. This keeps the entire build — data and theme — in a single atomic change that can be rolled back as a unit.\n:::\n\nThe required directory structure is:\n\n```\n\nmy-theme/\n├── layouts/\n│   └── theme.liquid          # Required — main layout\n├── pages/\n│   ├── home.liquid\n│   ├── product.liquid\n│   ├── products.liquid\n│   ├── cart.liquid\n│   ├── checkout.liquid\n│   ├── order.liquid\n│   ├── account.liquid\n│   ├── page.liquid\n│   ├── article.liquid\n│   ├── search.liquid\n│   ├── not_found.liquid\n│   ├── form_submission.liquid\n│   └── auth/\n│       ├── login.liquid\n│       ├── register.liquid\n│       └── password/\n│           └── forgot.liquid\n├── snippets/\n│   ├── header.liquid\n│   ├── footer.liquid\n│   ├── flash.liquid\n│   ├── form_errors.liquid\n│   └── products/\n│       └── card.liquid\n├── components/\n│   ├── cart.liquid\n│   └── cart-menu.liquid\n├── translations/\n│   └── en.default.json\n└── variables.json\n```\n\n### Step 11 — Build the layout\n\n`layouts/theme.liquid` is the most critical file. It must include all platform-required variables. Missing any of these causes visible failures:\n\n\n```liquid\n\n{%- default body_content: nil, csrf_meta_tags: nil, csp_meta_tag: nil -%}\n{%- default title: nil, meta_keywords: nil, meta_description: nil -%}\n{%- default sc_support: nil, theme_bar: nil, data: nil -%}\n{%- default controller: nil, action: nil, id: nil -%}\n{%- default theme_supplement_stylesheet: nil, theme_supplement_javascript: nil -%}\n\u003c!doctype html\u003e\n\u003chtml lang=\"{{ current_store.locale }}\"\u003e\n  \u003chead\u003e\n    \u003cmeta charset=\"utf-8\"\u003e\n    \u003cmeta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"\u003e\n    {{ csrf_meta_tags }}\n    {{ csp_meta_tag }}\n    {% render \"meta_data\" %}\n    {%- require \"styles/theme.css\" %}\n    {{ theme_supplement_stylesheet }}\n    {%- require \"scripts/theme.js\" %}\n    {{ theme_supplement_javascript }}\n    {{ sc_support }}\n  \u003c/head\u003e\n  \u003cbody id=\"{{ id }}\" {{ data }}\u003e\n    {{ theme_bar }}\n    \u003ca href=\"#SC-Main\" class=\"skip-link\"\u003e{{ \"accessibility.skip_to_content\" | t | default: \"Skip to main content\" }}\u003c/a\u003e\n    {% render \"header\" %}\n    \u003cmain id=\"SC-Main\"\u003e\n      {% render \"flash\" %}\n      {{ body_content }}\n    \u003c/main\u003e\n    {% render \"footer\" %}\n  \u003c/body\u003e\n\u003c/html\u003e\n```\n\n\n| Variable | Required | If missing |\n|----------|----------|------------|\n| `body_content` | Yes | Pages appear blank |\n| `csrf_meta_tags` | Yes | All form submissions fail |\n| `theme_bar` | Yes | Theme editor breaks |\n| `sc_support` | Yes | Platform features break |\n| `theme_supplement_stylesheet` | Yes | Custom CSS does not load |\n| `theme_supplement_javascript` | Yes | Custom JS does not load |\n\nSee [Theme layouts and pages](theme-layouts-and-pages) for the full variable reference.\n\n### Step 12 — Build page templates\n\nEach page template uses a specific context variable. Use the correct one for each route:\n\n| Page | Template | Context variable | Notes |\n|------|----------|-----------------|-------|\n| Home | `pages/home.liquid` | `current_page` | Output `current_page.body_content` for CMS blocks |\n| Product | `pages/product.liquid` | `current_product` | Show name, price, images, add-to-cart form |\n| Product listing | `pages/products.liquid` | `current_search` | Results, filters, pagination |\n| Cart | `pages/cart.liquid` | `current_cart` | Use `{% component \"cart\" %}` for live updates |\n| Checkout | `pages/checkout.liquid` | `current_checkout_step` | Four steps via `{% case %}` |\n| Order | `pages/order.liquid` | `current_order` | Order number, items, totals, addresses |\n| Content page | `pages/page.liquid` | `current_page` | Title, body_content |\n| Article | `pages/article.liquid` | `current_article` | Title, hero_image, body_content |\n| Search | `pages/search.liquid` | `current_search` | Tabs for products, articles, pages |\n| Account | `pages/account.liquid` | `current_customer` | Use `{% layout \"account\" %}`, route by `section` param |\n| Login | `pages/auth/login.liquid` | — | `{% form \"login\" %}` |\n| Register | `pages/auth/register.liquid` | — | `{% form \"register\" %}` |\n| 404 | `pages/not_found.liquid` | `error` | — |\n\n**Checkout requires all four steps:**\n\n\n```liquid\n\n{% case current_checkout_step %}\n{% when \"customer_information\" %}\n  {% form \"checkout-customer-information\" %}\n    {% render \"checkout/customer_information/form\", form: form %}\n  {% endform %}\n{% when \"shipping_information\" %}\n  {% form \"checkout-shipping-information\" %}\n    {% render \"checkout/shipping_information/form\", form: form %}\n  {% endform %}\n{% when \"accept_terms\" %}\n  {% form \"checkout-accept-terms\" %}\n    {% render \"checkout/accept_terms/form\", form: form %}\n  {% endform %}\n{% when \"payment_information\" %}\n  {% component \"checkout/payment_information/page\", reload: \"sc.voucher-applied sc.voucher-removed\" %}\n{% endcase %}\n```\n\n\n**Cart must use a reloadable component:**\n\n\n```liquid\n\n{% component \"cart\", reload: \"sc.cart-updated\" %}\n```\n\n\n### Step 13 — Build key snippets\n\nAt minimum, create:\n\n- `snippets/header.liquid` — logo, navigation from `all_menus.main-menu`, account link, cart badge component\n- `snippets/footer.liquid` — store info from `current_store`, footer menu\n- `snippets/flash.liquid` — render `current_flash.notice`, `.alert`, and `.error` with appropriate ARIA roles\n- `snippets/form_errors.liquid` — accept `errors` parameter, render as a list with `role=\"alert\"`\n- `snippets/products/card.liquid` — product card with image, name, price, and link\n\n**Header snippet pattern:**\n\n\n```liquid\n\n\u003cheader\u003e\n  \u003cnav aria-label=\"{{ 'navigation.main' | t }}\"\u003e\n    \u003ca href=\"/\"\u003e{{ current_store.name }}\u003c/a\u003e\n    \u003cul\u003e\n      {% for item in all_menus.main-menu.items %}\n        \u003cli\u003e\u003ca href=\"{{ item.url }}\"\u003e{{ item.title }}\u003c/a\u003e\u003c/li\u003e\n      {% endfor %}\n    \u003c/ul\u003e\n    {% component \"cart-menu\", reload: \"sc.cart-updated\" %}\n  \u003c/nav\u003e\n\u003c/header\u003e\n```\n\n\n### Step 14 — Add translations\n\nCreate `translations/en.default.json` with keys matching every `| t` filter usage in your templates:\n\n```json\n\n{\n  \"products\": {\n    \"show\": {\n      \"add_to_cart\": \"Add to Cart\"\n    },\n    \"index\": {\n      \"all_products\": \"All Products\"\n    }\n  },\n  \"cart\": {\n    \"header\": \"Shopping Cart\",\n    \"checkout\": \"Proceed to Checkout\"\n  },\n  \"checkout\": {\n    \"title\": \"Checkout\"\n  },\n  \"navigation\": {\n    \"main\": \"Main navigation\",\n    \"open_menu\": \"Open menu\"\n  },\n  \"accessibility\": {\n    \"skip_to_content\": \"Skip to main content\"\n  }\n}\n```\n\n### Step 15 — Add variables.json\n\nTheme configuration variables:\n\n```json\n\n{\n  \"products\": {\n    \"per_page\": 12,\n    \"comparisons\": false\n  },\n  \"images\": {\n    \"ratio\": {\n      \"width\": 4,\n      \"height\": 5\n    }\n  }\n}\n```\n\n### Step 16 — Verify the theme checklist\n\nBefore pushing the theme, confirm:\n\n- [ ] Layout outputs `{{ body_content }}`, `{{ csrf_meta_tags }}`, `{{ theme_bar }}`, `{{ sc_support }}`\n- [ ] Layout outputs `{{ theme_supplement_stylesheet }}` and `{{ theme_supplement_javascript }}`\n- [ ] Every `{% form %}` uses a valid form type string\n- [ ] Product page uses `current_product`, not `product`\n- [ ] Cart page uses `{% component \"cart\", reload: \"sc.cart-updated\" %}` (not static HTML)\n- [ ] Checkout handles all four steps\n- [ ] Payment step uses `{% component %}` (not `{% form %}`)\n- [ ] All `| t` keys exist in `translations/en.default.json`\n- [ ] `{% render \"meta_data\" %}` is in the layout `\u003chead\u003e`\n- [ ] All images have `alt` attributes\n\nSee [Theme SEO and accessibility](theme-seo-accessibility) for the full accessibility checklist.\n\n---\n\n## Design quality basics for agents\n\nBefore pushing, run through these four checks — they catch the mistakes that make an agent-built site look inconsistent even when every individual piece works:\n\n- **One H1 per page.** Every page and article body should have exactly one Markdown `# ` heading. The push lint report flags a body with none — and if the page missing it is the store's assigned home page, that's an error in the report rather than a warning.\n- **Load your fonts.** Set fonts through `update_store`'s `fonts` parameter (Step 10), never a bare `font-family` rule in `custom_styles`. A `font-family` declaration with nothing to load it (no `@import`, `@font-face`, or `\u003clink\u003e`) changes the CSS variable but not what actually renders — the push lint flags this too.\n- **Stick to real theme tokens.** Only the `--sc-*` custom properties the theme's own CSS actually declares or reads have any effect — for example `--sc-container-max-width`, `--sc-line-height-tight`/`-base`/`-loose`, `--sc-letter-spacing-heading`, and `--sc-font-family-heading`, alongside the color and behavioral variables covered elsewhere in this article and in [Theme assets and styling](theme-assets-and-styling). An invented `--sc-*` name, or a `custom_styles` class selector that matches nothing on the site, silently does nothing — the push lint report calls both out by name (with a nearest-match suggestion for a likely typo) so they don't ship unnoticed.\n- **Check both viewports.** After significant visual changes, call `get_screenshot` (see Step 18) and compare desktop and mobile against the design brief before moving on, rather than inferring correctness from markup alone.\n\n---\n\n## Phase 4: Push and preview\n\n### Step 17 — Push the theme\n\nUse the StoreConnect CLI to push your local theme files to the store as a draft:\n\n```bash\n\nsc theme push my-theme\n```\n\nThis syncs the local theme directory to the platform as a new content change — `sc theme push` always creates its own draft; it cannot join one already in progress. If you built the store's data with agent tools per Phase 2, that data is already staged (every `create_*`/`update_*` call staged it onto its content change) — there is no separate \"push\" step for that half of the build. To land data and theme in one atomic change, build the theme through agent tools too (`create_theme` with your existing `content_change_id`, per the tip in Phase 3) rather than pushing it from the CLI as a second, separate change.\n\n### Step 18 — Preview the theme\n\nGet a preview URL to see the draft on the live store without affecting visitors:\n\n```bash\n\nsc theme preview my-theme\n```\n\nIf you are working entirely through agent tools, `push_content_change` and `get_preview_url` return this same URL directly in their response — no separate CLI call is needed.\n\nVisit the preview URL in a browser. The theme preview bar appears at the top of every page. Check:\n- Home page renders with content blocks\n- Product pages show correct data (name, price, images, add-to-cart form)\n- Cart updates in real time when products are added\n- Checkout flows through all four steps\n- Navigation menus show the expected items\n- Mobile layout is correct\n\nUse the `{% debug %}` tag to inspect variables if anything appears wrong. See [Theme debugging](theme-debugging) for debugging techniques.\n\n**If you're an agent, you can't open a browser** — call `get_screenshot` instead:\n- `path` — optional storefront path; defaults to the home page (`/`).\n- `viewport` — `\"desktop\"`, `\"mobile\"`, or `\"both\"` (default).\n\nIt renders your session's open draft (falling back to the live storefront when there's no open draft) through a server-side headless browser and hands the pixels straight back as an image, plus a Cloudinary URL as a text fallback. Rendering happens server-side, so this can take a few seconds. It's not enabled on every server — if it returns a clear \"not enabled\" error, that's a server configuration matter, not something to retry.\n\nFor performance rather than visual issues, the same bearer token also authenticates a render-profiling endpoint, `GET /api/v1/debug/render_profile?path=/some/storefront/path`. It needs the `store:read` scope, renders the path in your store's context exactly as a real visit would, and reports parse time, filter timings, and errors. Only read-only content routes profile this way: pages, articles, categories, products, and search. For in-template timing instead, see [theme debugging](theme-debugging).\n\n### Step 19 — Push for review\n\nWhen the preview looks correct, push the draft for review:\n\n```bash\n\nsc theme publish my-theme\n```\n\nOr, if you're working through agent tools, call `push_content_change` with the `content_change_id`.\n\nEither path does the same thing: the draft is sent to Salesforce as a Content Change record with status `review` — **nothing goes live**. A human with an approver-level Store Role must open the record in Salesforce and set its Status to Published before it applies to the store; see [Review and approve a Content Change](review-and-approve-a-content-change). There is no environment where a push publishes automatically, and no dev/build-mode shortcut around the approval step.\n\nPushing also returns a lint report covering things like unpriced products, missing image alt text, pages or articles with no heading (an H1) or meta description, duplicate menu items pointing at the same target, unsafe HTML (`\u003cscript\u003e`, `\u003ciframe\u003e`, event handlers, `javascript:` URLs), a declared font with nothing to load it, and `custom_styles` issues — unused custom properties, class selectors matching nothing on the site, unrecognized `--sc-*` tokens, and too many distinct fonts or colors to look intentional. The report doesn't block the push — the human approver sees it too, alongside the field-level diff, so they know exactly what they're approving.\n\n`push_content_change` (and `get_content_change`) also return a `salesforce_url` — a direct link to the pushed record in Salesforce. It can be `null` right after this call: Heroku Connect assigns the record's Salesforce id asynchronously when it syncs down, not at push time. Call `get_content_change` again shortly after to pick up the link once it appears.\n\n---\n\n## Reference: build dependency diagram\n\n```\n\nMedia\n  └─▶ Categories (product)\n        └─▶ Products\n  └─▶ Pages\n        └─▶ Content Blocks (via pages + media)\n  └─▶ Menus (via categories + pages)\n        └─▶ Store Linkage (home page + menus)\n\nArticles — independent; agent tools do not yet support article categories or hero images\n```\n\nBuild in the order listed. Same-draft references only resolve to records already created earlier in the same content change — pass a `category_ids` value that doesn't exist yet (in this change or live) and the call fails; the same applies to any other id-referencing parameter."}