"Not all operations have an unique name" — and why your Shopify types quietly stop updating
You edit a GraphQL query in your embedded app. You run codegen. You go back to your route and the new field isn't on the type — TypeScript still knows the old shape, and it still typechecks, so nothing is red. You edit the query again. Nothing changes again.
This is for you if that has happened and you assumed you were holding codegen wrong. If your graphql-codegen run has always been green, skip this one.
Before you start
- An embedded Shopify app with
@shopify/api-codegen-presetwired up — a.graphqlrc.tswithshopifyApiProject({ apiType: ApiType.Admin, ... })and generated types inapp/types. → Shopify: generate types for GraphQL
The learning
Two things are hard errors in graphql-codegen, and Shopify's own docs walk you into both:
- Two operations anywhere in your documents glob that share a name.
- An anonymous operation in a file that holds any other operation.
Both fail the run. Neither deletes or blanks the generated files. app/types/admin.generated.d.ts keeps whatever it last held, so your editor stays confidently, silently wrong.
Shopify does not care about the operation name — it never reaches the server as anything meaningful. Naming is a codegen concern only, which is exactly why nobody bothers, and why the docs examples are full of query { ... }.
Symptom 1 — the duplicate
Three routes each declared a query called ProbeSetup, because each was pasted from the last one:
const themesRes = await admin.graphql(`#graphql
query ProbeSetup($themeId: ID!, $filenames: [String!]) {
theme(id: $themeId) { files(first: 5, filenames: $filenames) { nodes { filename } } }
}
`);Run codegen and the failure names the query and every file it found it in:
[STARTED] Generate
[FAILED] Not all operations have an unique name: ProbeSetup:
[FAILED]
[FAILED] * ProbeSetup found in:
[FAILED]
[FAILED] - /…/app/routes/app._index.tsx
[FAILED] - /…/app/routes/app.dev-tools.tsx
[FAILED] - /…/app/routes/app.theme-setup.tsx
[FAILED]
[SUCCESS] Generate outputsThat "an unique" is theirs, not a typo of mine — if you're searching for this error, search it exactly as printed.
Unique means unique across the whole documents glob, not per file. Every route you own is in one namespace.
Symptom 2 — the anonymous one, which only appears once you fix the duplicates
Rename the three, run again, and a second wall shows up that was hidden behind the first:
[FAILED] GraphQL Document Validation failed with 5 errors;
[FAILED] Error 0: This anonymous operation must be the only defined operation.
[FAILED] at /…/app/routes/app._index.tsx:2:9
[FAILED]
[FAILED] Error 1: This anonymous operation must be the only defined operation.
[FAILED] at /…/app/routes/app.dev-tools.tsx:2:13This one is plain GraphQL validation, not a codegen rule. An anonymous operation is legal only when it is the only operation in its file. Files like this are the trap:
// one route, two operations, one of them anonymous → invalid
await admin.graphql(`#graphql query { themes(first: 5, roles: [MAIN]) { edges { node { id } } } }`);
await admin.graphql(`#graphql query ProbeSetupIndex($themeId: ID!) { theme(id: $themeId) { id } }`);The line:column points inside the tagged string, not at the file — 2:9 is not line 2 of your route. Don't go looking there.
The part that cost me the most: it fails loudly and then says SUCCESS
Look at the last line of that first failure again:
[SUCCESS] Generate outputsThe listr renderer prints per-task status, and the parent task reports success while its children failed. Scroll-back in a busy terminal and this is what your eye lands on.
The honest signal is the exit code:
$ npx graphql-codegen --config .graphqlrc.ts > /dev/null 2>&1; echo "EXIT=$?"
EXIT=1
$ git status --porcelain -- app/types
# ← nothing. The stale types are still there.That empty second line is the whole bug. A failed run and a run with nothing to change look identical in your working tree.
And because codegen isn't part of shopify app dev, nothing re-runs it for you. The types drift for as long as you don't look.
The fix
Name every operation, uniquely, and suffix it with where it lives so the next paste doesn't collide:
- query { themes(first: 5, roles: [MAIN]) { edges { node { id } } } }
+ query ListMainThemesIndex { themes(first: 5, roles: [MAIN]) { edges { node { id } } } }
- query ProbeSetup($themeId: ID!, $filenames: [String!]) {
+ query ProbeSetupDevTools($themeId: ID!, $filenames: [String!]) {No runtime change. Shopify ignores the name.
Where you can see it worked
Run it and watch two things, in this order:
$ npx graphql-codegen --config .graphqlrc.ts; echo "EXIT=$?"
…
[SUCCESS] Generate outputs
EXIT=0
$ git diff --stat -- app/types
app/types/admin.generated.d.ts | 319 ++++++++++++++++++------EXIT=0 and a diff in app/types. If the exit code is 0 and the diff is empty after you changed a query, you are still reading stale types.
Stop it coming back
Codegen is only useful if something fails when it fails. Put it in front of the build, where it exits non-zero on the machine that blocks the merge:
{
"scripts": {
"prebuild": "graphql-codegen --config .graphqlrc.ts",
"graphql-codegen": "graphql-codegen"
}
}Committing the generated types matters too — a CI job that regenerates them and finds a diff is how "someone edited a query and never ran codegen" gets caught.
Docs
- graphql-codegen configuration → https://the-guild.dev/graphql/codegen/docs/config-reference/codegen-config
- Shopify Admin API type generation → https://shopify.dev/docs/api/shopify-app-remix/latest/guide-graphql-types
Implementation prompt
I have an embedded Shopify admin app (Remix + TypeScript) using @shopify/api-codegen-preset
with a .graphqlrc.ts and generated types in app/types. Before anything else, check that
this setup exists and set it up if it does not.
Task: make graphql-codegen pass and keep it passing.
1. Find every GraphQL operation in the codegen `documents` glob — inline template literals
tagged `#graphql` passed to admin.graphql(), plus any .graphql files.
2. Give every operation a unique name across the ENTIRE glob, not per file. Suffix each
with the module it lives in (ListMainThemesIndex, ProbeSetupDevTools) so future copies
collide loudly instead of silently.
3. Add a `prebuild` script that runs graphql-codegen, so a failing run fails the build.
4. Run codegen and report the exit code and `git diff --stat -- app/types`.
Do not change any query or mutation body, variables, or the selected fields. Names are a
codegen concern only — Shopify ignores them, so this must be a zero-runtime-change edit.
Constraints:
- Operation names must be unique across the whole documents glob, not just within a file.
- An anonymous operation is valid ONLY if it is the only operation in its file. Name them all.
- graphql-codegen prints "[SUCCESS] Generate outputs" even on a failing run. Trust the exit
code, not the log.
- A failed run leaves the previously generated files untouched; it does not blank them. Do
not assume an unchanged app/types means success.
Acceptance check: `npx graphql-codegen --config .graphqlrc.ts` exits 0, and after editing
any query's selection set, re-running it produces a diff in app/types. Tell me if either fails.Verified against @graphql-codegen/cli 5.0.7, @shopify/api-codegen-preset 1.1.9, graphql 16.11 — both failures reproduced on a checkout of the broken commit, 2026-07-31.