shopify
The calculated-cost bucket is state you can set
Shopify meters the GraphQL Admin API by calculated query cost rather than by request count, and every answer carriesextensions.cost with a throttleStatus a
well-written client paces itself off. A twin that omitted that block would break
every such client; one that answered 429 instead of HTTP 200 with a THROTTLED
entry would send every Shopify backoff down the wrong branch.
So the bucket is container state. drain_cost_bucket puts it where you want it, and
advancing the sandbox restores it a step at a time — which is the only way backoff
code runs on the first query instead of never.
The cost model is not invented. Shopify publishes the shape of the rule and not
its sizing function, so the function was measured against Shopify’s own endpoint at
every boundary. It reproduces Shopify’s numbers exactly, including the 1729 that
Shopify’s own MAX_COST_EXCEEDED message quotes for the deepest legal query.
Three error channels, each carrying what Shopify puts in it
The Admin API has three, they carry different failures, and callers routinely check one:- A
401with a string-valued{"errors": …}for authentication. Verified against a live shop, where a missing token and an unrecognised one turn out to be the same answer — and where authentication happens before the version in the path is looked at. - An
errors[]array atHTTP 200for everything the GraphQL layer refuses: a field the schema lacks, a malformed cursor, a connection argument out of range, a global id of the wrong type, a query over the cost limit, a missing access scope. userErrors[]inside a200with no top-level errors for a business-rule rejection: a handle that collides, a refund over the maximum, a fulfillment for a line item already shipped.
The schema is a pinned artefact
ShopifyAPI, Shopify’s own Python client, is a thin transport: it returns the raw
response body as a string and parses nothing, so it can tell a caller nothing
about shape. A twin conformed only against it could answer with any field name at
all and pass.
So Shopify’s Admin GraphQL schema is pinned beside the client, the twin’s modelled
surface is written as SDL, and a test walks all 245 types — every field, every
argument, every nullability, every argument default, every enum value — against
Shopify’s own. Narrowing is
incompleteness and says so through the not-modelled refusal; widening is invention
and fails the build.
What it models
Advancing the sandbox steps two things and no more, because a Shopify store has
no other lifecycle: the cost bucket restores, and a bulk operation moves along
CREATED → RUNNING → COMPLETED with its result URL fetchable only at the end.
Orders do not self-fulfil and drafts do not expire.
Scenarios
stocked-store, partially-fulfilled, refund-window-closed, out-of-stock,
throttled, narrow-scopes.
Not modelled
The REST Admin API, refused whole rather than half-built — a layer that worked for two paths and 404’d on the third would be worse than none. The OAuth install flow, app billing, session tokens and App Bridge. Webhook delivery: subscriptions are recorded and nothing is sent. The Storefront and Customer Account APIs, Checkout extensibility and Shopify Payments. Collections, markets, B2B companies, subscription contracts, gift cards, shipping profiles, carrier services, translations and Shopify Functions. Order editing, multi-location fulfillment routing, andproductSet
against a product that already exists. Introspection, and @skip and @include —
which are the language’s own rather than Shopify’s, so the document they are written
in parses here and the refusal names the directive instead of the @.
The refusal is settled deliberately, and against Shopify’s own schema rather than by
guesswork. A field Shopify does not have gets Shopify’s validator verbatim,
undefinedField included, because that claim is true. A field Shopify has and
this twin has not built gets a code Shopify never sends, because telling a caller the
schema lacks a field that is right there would send them off to write a workaround
for a problem that does not exist. Never a null node, and never a routing 404.
The twin’s own README explains every such decision and where each number came from.