Skip to content
Log in

End-to-end store build sequence

On this page

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.

Nothing like this workflow exists as a single reference anywhere else — this article is the canonical guide.

Overview

A StoreConnect store has two independently managed halves:

  1. 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).
  2. Theme — Liquid templates, assets, and configuration that control how the data appears in the browser.

Both halves must be in place before a store is usable. The sections below walk through each in order.


Phase 1: Connect your tools and plan the build

There 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 covers how to mint a token and connect an MCP client; once connected, the tools named in each phase below are what you call.

The StoreConnect CLI complements this rather than replacing it:

  • sc theme pull / sc theme push manage the theme half of the build (Phase 3 and Phase 4 below).
  • 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.

The CLI has no commands for creating or editing products, categories, articles, pages, content blocks, or menus — that is what the MCP tools are for.

Before you start calling tools, decide:

  • What you’re building and the order in Phase 2 below — later records reference earlier ones.
  • 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).
  • 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.

Phase 2: Create store data in dependency order

Records are created in a specific order because later records reference earlier ones by the id each create_* tool returns. Build in this exact sequence:

Step 1 — Design brief

Before 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.

set_design_brief takes one required parameter: - 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).

```json

{ “personality”: “warm, minimal, trustworthy”, “colors”: {“primary_intent”: “earthy green, calming”}, “fonts”: {“heading”: “serif, editorial”}, “do_not”: “no stock photos of laptops” } ```

Unlike 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.

The 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.

:::tip Starting 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. :::

Step 2 — Media

Create media first because most other content can reference it.

Call create_media with: - 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) - name — required display name - alt_text — optional, for accessibility - file_typeimage (default), uploaded_video, or document - identifier — optional stable handle for referencing this media from a theme

```json

{ “url”: “https://example.com/images/hero.jpg”, “name”: “Hero image”, “alt_text”: “Coffee farm at golden hour” } ```

The 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.

:::tip Several 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. :::

Step 3 — Categories

Create product categories after media, because a category’s banner image references it.

create_category takes: - name — required - path — required; URL slug (for example single-origins) - display_name — optional customer-facing name (defaults to name)

Once 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).

:::note Article 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. :::

Step 4 — Products

Create products after categories and media because products reference both.

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.

Key create_product fields: - name — required - product_code — SKU / product code - 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 - description, summary_markdown — Markdown supported on summary_markdown - price — a decimal in the store’s currency (for example 24.00, not cents); a product cannot be purchased without one - 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 - meta_title, meta_description, meta_keywords — SEO fields

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.

```json

{ “name”: “Ethiopia Yirgacheffe”, “product_code”: “ETH-YIRG-250”, “slug”: “ethiopia-yirgacheffe”, “price”: 24.00, “category_ids”: [“"] } ```

Add 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.

Variants (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.

:::note Filterable 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. :::

Step 5 — Articles

Create articles after you’re ready to link to them from pages or menus — articles don’t currently depend on any other content type.

create_article takes: - title and path — both required - body_markdown — the article body in Markdown - author, subtitle, intro_markdown, summary_markdown - published — whether the article is published once the change goes live - meta_title, meta_description, meta_keywords

:::note Article 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. :::

Step 6 — Pages

Create pages once you know what you’re linking to from menus and content blocks.

create_page takes: - title and path — both required; path is the page’s URL path (for example about-us) - body_markdown — page content in Markdown - subtitle, meta_title, meta_description, meta_keywords - visible, require_login - parent_id, position — for nesting under another page

:::tip There’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. :::

:::tip Agents 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. :::

Step 7 — Content blocks

Create content blocks after pages and media because content blocks reference both.

A 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: - name — required - template — block type (see Theme snippets, blocks, and components for the full list of block types) - title, subtitle, content_markdown - identifier — optional stable handle for referencing the block from a theme - image_id — a media reference, used by image, image_text_overlay, and image_beside_text blocks - media_id — a downloadable-media reference, used by media blocks - link_label, link_target — call-to-action label and URL/path - page_id — places the block on that page - position — sort order on the page - product_ids — array of product references, used by featured_products blocks

```json

{ “name”: “Home hero”, “template”: “image_text_overlay”, “title”: “Welcome”, “subtitle”: “Quality coffee, delivered.”, “image_id”: “", "link_label": "Shop Now", "link_target": "/products", "page_id": "", "position": 1 } ```

Step 8 — Menus

Create menus after categories and pages because menu items link to both.

First 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: - menu_id — the menu’s returned id - display_name — required; the label shown in the navigation - exactly one target: url (external or relative), page_id, article_id, product_id, or product_category_id - position, parent_id — for nested navigation

```json

{“name”: “Header”, “identifier”: “header”} ```

```json

{“menu_id”: “", "display_name": "Shop", "product_category_id": ""} ```

```json

{“menu_id”: “", "display_name": "About", "page_id": ""} ```

Step 9 — Store linkage

After your content exists, link the store to its home page and menus with the update_store agent tool: - home_page_id — the page id to use as the store’s root page - header_menu_id, footer_menu_id — the menu ids for the header and footer navigation

Pass the id returned from the matching create_page / create_menu calls (or an existing live page/menu’s id).

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:

```css

:root { –sc-color-primary: #2D4A2D; –sc-color-secondary: #C4622D; } ```

Hex 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.

The custom_styles value is the same field surfaced in Liquid as current_store.global_css, which the layout renders inside a <style> 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.)

Do 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).

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:

```json

{ “fonts”: { “heading”: {“family”: “Playfair Display”, “weights”: [400, 700]}, “body”: {“family”: “Inter”, “weights”: [400, 600]}, “source”: “google” } } ```

  • heading.family / body.family — required; letters, digits, and spaces only.
  • heading.weights / body.weights — optional arrays of integers; omit to use the default weight.
  • 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.

This 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=...&display=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.

Set the store logo the same way, with update_store’s logo_url — a public image URL; StoreConnect stages the media for you.

Escape hatch: let the human take over visually

At 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:

  • 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.
  • path — optional; a storefront path to land on inside the builder (ignored for console).

It 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.

This requires a real user identity — the user-token or OAuth connection you set up in 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.

Same-draft cross-references

When 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.


Phase 3: Create the theme

With store data in place, create the theme. A theme is a directory of Liquid templates and assets.

:::tip If 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. :::

The required directory structure is:

```

my-theme/ ├── layouts/ │ └── theme.liquid # Required — main layout ├── pages/ │ ├── home.liquid │ ├── product.liquid │ ├── products.liquid │ ├── cart.liquid │ ├── checkout.liquid │ ├── order.liquid │ ├── account.liquid │ ├── page.liquid │ ├── article.liquid │ ├── search.liquid │ ├── not_found.liquid │ ├── form_submission.liquid │ └── auth/ │ ├── login.liquid │ ├── register.liquid │ └── password/ │ └── forgot.liquid ├── snippets/ │ ├── header.liquid │ ├── footer.liquid │ ├── flash.liquid │ ├── form_errors.liquid │ └── products/ │ └── card.liquid ├── components/ │ ├── cart.liquid │ └── cart-menu.liquid ├── translations/ │ └── en.default.json └── variables.json ```

Step 11 — Build the layout

layouts/theme.liquid is the most critical file. It must include all platform-required variables. Missing any of these causes visible failures:

```liquid

{%- default body_content: nil, csrf_meta_tags: nil, csp_meta_tag: nil -%} {%- default title: nil, meta_keywords: nil, meta_description: nil -%} {%- default sc_support: nil, theme_bar: nil, data: nil -%} {%- default controller: nil, action: nil, id: nil -%} {%- default theme_supplement_stylesheet: nil, theme_supplement_javascript: nil -%} <!doctype html>

{{ csrf_meta_tags }} {{ csp_meta_tag }} {% render "meta_data" %} {%- require "styles/theme.css" %} {{ theme_supplement_stylesheet }} {%- require "scripts/theme.js" %} {{ theme_supplement_javascript }} {{ sc_support }} <body id="{{ id }}" {{ data }}> {{ theme_bar }} {% render "header" %}
{% render "flash" %} {{ body_content }}
{% render "footer" %} </body>

```

Variable Required If missing
body_content Yes Pages appear blank
csrf_meta_tags Yes All form submissions fail
theme_bar Yes Theme editor breaks
sc_support Yes Platform features break
theme_supplement_stylesheet Yes Custom CSS does not load
theme_supplement_javascript Yes Custom JS does not load

See Theme layouts and pages for the full variable reference.

Step 12 — Build page templates

Each page template uses a specific context variable. Use the correct one for each route:

Page Template Context variable Notes
Home pages/home.liquid current_page Output current_page.body_content for CMS blocks
Product pages/product.liquid current_product Show name, price, images, add-to-cart form
Product listing pages/products.liquid current_search Results, filters, pagination
Cart pages/cart.liquid current_cart Use {% component "cart" %} for live updates
Checkout pages/checkout.liquid current_checkout_step Four steps via {% case %}
Order pages/order.liquid current_order Order number, items, totals, addresses
Content page pages/page.liquid current_page Title, body_content
Article pages/article.liquid current_article Title, hero_image, body_content
Search pages/search.liquid current_search Tabs for products, articles, pages
Account pages/account.liquid current_customer Use {% layout "account" %}, route by section param
Login pages/auth/login.liquid {% form "login" %}
Register pages/auth/register.liquid {% form "register" %}
404 pages/not_found.liquid error

Checkout requires all four steps:

```liquid

{% case current_checkout_step %} {% when “customer_information” %} {% form “checkout-customer-information” %} {% render “checkout/customer_information/form”, form: form %} {% endform %} {% when “shipping_information” %} {% form “checkout-shipping-information” %} {% render “checkout/shipping_information/form”, form: form %} {% endform %} {% when “accept_terms” %} {% form “checkout-accept-terms” %} {% render “checkout/accept_terms/form”, form: form %} {% endform %} {% when “payment_information” %} {% component “checkout/payment_information/page”, reload: “sc.voucher-applied sc.voucher-removed” %} {% endcase %} ```

Cart must use a reloadable component:

```liquid

{% component “cart”, reload: “sc.cart-updated” %} ```

Step 13 — Build key snippets

At minimum, create:

  • snippets/header.liquid — logo, navigation from all_menus.main-menu, account link, cart badge component
  • snippets/footer.liquid — store info from current_store, footer menu
  • snippets/flash.liquid — render current_flash.notice, .alert, and .error with appropriate ARIA roles
  • snippets/form_errors.liquid — accept errors parameter, render as a list with role="alert"
  • snippets/products/card.liquid — product card with image, name, price, and link

Header snippet pattern:

```liquid

```

Step 14 — Add translations

Create translations/en.default.json with keys matching every | t filter usage in your templates:

```json

{ “products”: { “show”: { “add_to_cart”: “Add to Cart” }, “index”: { “all_products”: “All Products” } }, “cart”: { “header”: “Shopping Cart”, “checkout”: “Proceed to Checkout” }, “checkout”: { “title”: “Checkout” }, “navigation”: { “main”: “Main navigation”, “open_menu”: “Open menu” }, “accessibility”: { “skip_to_content”: “Skip to main content” } } ```

Step 15 — Add variables.json

Theme configuration variables:

```json

{ “products”: { “per_page”: 12, “comparisons”: false }, “images”: { “ratio”: { “width”: 4, “height”: 5 } } } ```

Step 16 — Verify the theme checklist

Before pushing the theme, confirm:

  • [ ] Layout outputs {{ body_content }}, {{ csrf_meta_tags }}, {{ theme_bar }}, {{ sc_support }}
  • [ ] Layout outputs {{ theme_supplement_stylesheet }} and {{ theme_supplement_javascript }}
  • [ ] Every {% form %} uses a valid form type string
  • [ ] Product page uses current_product, not product
  • [ ] Cart page uses {% component "cart", reload: "sc.cart-updated" %} (not static HTML)
  • [ ] Checkout handles all four steps
  • [ ] Payment step uses {% component %} (not {% form %})
  • [ ] All | t keys exist in translations/en.default.json
  • [ ] {% render "meta_data" %} is in the layout <head>
  • [ ] All images have alt attributes

See Theme SEO and accessibility for the full accessibility checklist.


Design quality basics for agents

Before pushing, run through these four checks — they catch the mistakes that make an agent-built site look inconsistent even when every individual piece works:

  • 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.
  • 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 <link>) changes the CSS variable but not what actually renders — the push lint flags this too.
  • 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. 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.
  • 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.

Phase 4: Push and preview

Step 17 — Push the theme

Use the StoreConnect CLI to push your local theme files to the store as a draft:

```bash

sc theme push my-theme ```

This 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.

Step 18 — Preview the theme

Get a preview URL to see the draft on the live store without affecting visitors:

```bash

sc theme preview my-theme ```

If 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.

Visit the preview URL in a browser. The theme preview bar appears at the top of every page. Check: - Home page renders with content blocks - Product pages show correct data (name, price, images, add-to-cart form) - Cart updates in real time when products are added - Checkout flows through all four steps - Navigation menus show the expected items - Mobile layout is correct

Use the {% debug %} tag to inspect variables if anything appears wrong. See Theme debugging for debugging techniques.

If you’re an agent, you can’t open a browser — call get_screenshot instead: - path — optional storefront path; defaults to the home page (/). - viewport"desktop", "mobile", or "both" (default).

It 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.

For 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.

Step 19 — Push for review

When the preview looks correct, push the draft for review:

```bash

sc theme publish my-theme ```

Or, if you’re working through agent tools, call push_content_change with the content_change_id.

Either path does the same thing: the draft is sent to Salesforce as a Content Change record with status reviewnothing 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. There is no environment where a push publishes automatically, and no dev/build-mode shortcut around the approval step.

Pushing 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 (<script>, <iframe>, 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.

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.


Reference: build dependency diagram

```

Media └─▶ Categories (product) └─▶ Products └─▶ Pages └─▶ Content Blocks (via pages + media) └─▶ Menus (via categories + pages) └─▶ Store Linkage (home page + menus)

Articles — independent; agent tools do not yet support article categories or hero images ```

Build 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.

Was this article helpful?

Was this article helpful?