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
| Parameter | Values | Default |
|---|---|---|
size | small: 50 products and about 100 variants. medium: about 1,000 variants. large: over 10,000 variants and at least 5,000 orders | small |
history_days | 14 to 1095 days of orders before the clock | 365 |
currency | NOK, SEK, DKK, EUR or USD | NOK |
timezone | any IANA timezone, such as Europe/Oslo | Europe/Oslo |
future_orders_per_day | 0 to 200 orders a day over the 30 days after the clock, each landing when the clock passes it (Order webhooks) | 0 |
clock | the instant the history ends at, in RFC 3339 | the simulator’s clock |
rng_seed | any integer | 0 |
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 lagerships online orders, beside two shops. Each variant has stock at each location, withavailable,on_hand,committedandincomingquantities. About one in ten levels is sold out. - A unit cost on every inventory item.
- A collection for each product type, plus
Salg,NyheterandBestselgere. - 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
Norgemarket 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,translatableResourcesandtranslatableResourcesByIdsread them back undertranslations(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
themeFilesUpsertcan change. - A
faq_entrymetaobject 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.