Skip to Content
SimulatorsShopifyGenerated store

Simulators

Generated store

Test an app that reads a store with real history, such as a forecast that compares this year with last year. Load shopify_store with a size and a history length, then read the store back:

ADMIN="$RYSTIC_URL/admin/api/2026-10/graphql.json"
curl -s -X POST "$RYSTIC_URL/_rystic/scenario" -d '{"name":"shopify_store","params":{"size":"small","history_days":730,"clock":"2026-11-08T12:00:00Z"}}'
curl -s "$ADMIN" -H "X-Shopify-Access-Token: shpat_read_all_orders" -H "Content-Type: application/json" -d '{"query": "{ shop { currencyCode ianaTimezone } productsCount { count } ordersCount(limit: null) { count } }"}' | jq -c '.data'
# -> {"ordersCount":{"count":876},"productsCount":{"count":50},"shop":{"currencyCode":"NOK","ianaTimezone":"Europe/Oslo"}}

The same parameters build the same store, record for record. Change rng_seed for another store of the same shape. Name clock to fix the day the history runs back from; without it, the history ends at the simulator’s clock.

Parameters

ParameterValuesDefault
sizesmall: 50 products and about 100 variants. medium: about 1,000 variants. large: over 10,000 variants and at least 5,000 orderssmall
history_days14 to 1095 days of orders before the clock365
currencyNOK, SEK, DKK, EUR or USDNOK
timezoneany IANA timezone, such as Europe/OsloEurope/Oslo
future_orders_per_day0 to 200 orders a day over the 30 days after the clock, each landing when the clock passes it (Order webhooks)0
clockthe instant the history ends at, in RFC 3339the simulator’s clock
rng_seedany integer0

GET /_rystic/scenarios lists the parameters with their defaults:

curl -s "$RYSTIC_URL/_rystic/scenarios" | jq -c '.scenarios[] | select(.name == "shopify_store") | [.params[].name]'
# -> ["size","history_days","currency","timezone","future_orders_per_day","clock","rng_seed"]

A value outside the list or a timezone the simulator does not know returns 400 and leaves the current scenario loaded:

curl -s -o /dev/null -w '%{http_code}\n' -X POST "$RYSTIC_URL/_rystic/scenario" -d '{"name":"shopify_store","params":{"timezone":"Mars/Olympus"}}'
# -> 400
curl -s "$ADMIN" -H "X-Shopify-Access-Token: shpat_read_all_orders" -H "Content-Type: application/json" -d '{"query": "{ ordersCount(limit: null) { count } }"}' | jq -c '.data.ordersCount'
# -> {"count":876}

Boot straight into a store with rystic run shopify-twin --scenario 'shopify_store?size=medium&history_days=730'.

The 60-day order window

The scenario binds the token shpat_read_all_orders to the read_all_orders grant, so it reads every order. Any other token sees only the last 60 days, as a Shopify token without that grant does (Order history window):

curl -s "$ADMIN" -H "X-Shopify-Access-Token: shpat_placeholder" -H "Content-Type: application/json" -d '{"query": "{ ordersCount(limit: null) { count } }"}' | jq -c '.data.ordersCount'
# -> {"count":91}

Seasons and sales

Orders follow a Nordic retail year: a quiet January and July, a busy December and a peak in Black Week that is strongest on Black Friday. Sales grow about 30% a year. Orders fall on local days and hours of the shop’s timezone. ShopifyQL shows the peak against a quiet month:

curl -s "$ADMIN" -H "X-Shopify-Access-Token: shpat_read_all_orders" -H "Content-Type: application/json" -d '{"query": "{ shopifyqlQuery(query: \"FROM sales SHOW total_sales GROUP BY month SINCE -24m UNTIL today\") { tableData { rows } } }"}' | jq -c '[.data.shopifyqlQuery.tableData.rows[] | select(.month == "2025-11-01" or .month == "2025-01-01")]'
# -> [{"month":"2025-11-01","total_sales":"98851.2"},{"month":"2025-01-01","total_sales":"46789.8"}]

What the store holds

  • Products with sizes and colors, images, SKUs, barcodes, compare-at prices on some and a few draft and archived products.
  • Glacier Thermos, which has stock and never sold.
  • Aurora Down Vest, created two days before the clock: sold out, with stock incoming.
  • Three locations: Oslo lager ships online orders, beside two shops. Each variant has stock at each location, with available, on_hand, committed and incoming quantities. About one in ten levels is sold out.
  • A unit cost on every inventory item.
  • A collection for each product type, plus Salg, Nyheter and Bestselgere.
  • Customers with saved addresses, email consent, order counts and spend. Five saved segments.
  • Orders through the online store and the shops’ point of sale, with line items, discounts, included 25% VAT, payments, fulfillment orders and tracked shipments. Recent orders are still open or partly shipped.
  • Returns, refunds for returned items, price-adjustment refunds and a few canceled orders.
  • An active welcome code and member discount.
  • A Black Week code, a summer code and a January sale for each year of history: expired, active or scheduled by the clock.
  • Abandoned checkouts from the last 90 days and three open draft orders.
  • A blog with articles, five pages, a main menu and two redirects.
  • A primary Norge market in the shop’s currency. A second market sells in EUR (in SEK for a EUR shop). The second market’s catalog has a price list with a fixed price for every variant.
  • Norwegian (Bokmål) as the primary language and English published, with English translations of the pages, the article titles and some product titles. translatableResource, translatableResources and translatableResourcesByIds read them back under translations(locale: "en"), as the last example in this section shows.
  • Two themes, one published, each with layout, template, section, asset, config and locale files, which themeFilesUpsert can change.
  • A faq_entry metaobject definition with six entries. Product metafields with their definitions.
curl -s "$ADMIN" -H "X-Shopify-Access-Token: shpat_read_all_orders" -H "Content-Type: application/json" -d '{"query": "{ products(first: 2, query: \"title:Glacier OR title:Aurora\") { nodes { title createdAt totalInventory } } }"}' | jq -c '.data.products.nodes'
# -> [{"createdAt":"2024-07-13T00:00:00Z","title":"Glacier Thermos","totalInventory":70},{"createdAt":"2026-11-06T10:00:00Z","title":"Aurora Down Vest","totalInventory":0}]

The new product’s first variant has a unit cost and stock incoming at each location:

curl -s "$ADMIN" -H "X-Shopify-Access-Token: shpat_read_all_orders" -H "Content-Type: application/json" -d '{"query": "{ products(first: 1, query: \"title:Aurora\") { nodes { variants(first: 1) { nodes { inventoryItem { unitCost { amount currencyCode } inventoryLevels(first: 3) { nodes { location { name } quantities(names: [\"available\", \"incoming\"]) { name quantity } } } } } } } } }"}' | jq -c '.data.products.nodes[0].variants.nodes[0].inventoryItem | {unitCost, levels: [.inventoryLevels.nodes[] | {location: .location.name, available: (.quantities[] | select(.name == "available") | .quantity), incoming: (.quantities[] | select(.name == "incoming") | .quantity)}]}'
# -> {"unitCost":{"amount":"558.36","currencyCode":"NOK"},"levels":[{"location":"Oslo lager","available":0,"incoming":41},{"location":"Bergen butikk","available":0,"incoming":13},{"location":"Trondheim butikk","available":0,"incoming":62}]}

A product’s content is in Norwegian, and its English title reads back as a translation:

curl -s "$ADMIN" -H "X-Shopify-Access-Token: shpat_read_all_orders" -H "Content-Type: application/json" -d '{"query": "{ translatableResource(resourceId: \"gid://shopify/Product/10001\") { translatableContent { key value locale } translations(locale: \"en\") { key value } } }"}' | jq -c '.data.translatableResource | {content: .translatableContent[0], translations}'
# -> {"content":{"key":"title","locale":"nb","value":"Glacier Thermos"},"translations":[{"key":"title","value":"Glacier Thermos"}]}

Load time

A small store loads in under a second and a medium one in about two. A large store with two years of orders holds about 170,000 records and loads in about seven seconds.

Last updated on