The Shopify Admin API search query syntax is the string you pass as query: on connections such as products, orders and customers: terms like field:value, comparators :<, :>, :<= and :>=, upper-case AND/OR, - or NOT to exclude, and * for prefixes. Its failures are mostly silent, so read the extensions.search warnings on every response.
That last part matters, because a malformed filter does not raise a GraphQL error. You get a 200, a page of data and, at best, a warning under extensions, which your client only sees if it reads that key on purpose.
The queries below are written against Admin GraphQL 2026-07, which shopify.dev labels latest as of November 2026 (2026-10 is listed as the release candidate). We validated both operations against the 2026-07 schema and ran the JavaScript helper’s unit tests on Node 24.12. We did not run any of it against a live store, so everything about how the search parser behaves comes from Shopify’s documentation, not from our own requests.
How does the Shopify Admin API search query syntax work?
Shopify publishes a single search query syntax that it says applies across the Admin API, the Storefront API, the Customer Account API, REST search endpoints and webhook delivery filters. If you have written webhook subscription filters, you already know the grammar.
A query is a list of terms. Whitespace between two terms means AND. A term is an optional modifier, then either a bracketed subquery or a value with an optional name and comparator in front of it. OR binds tighter than AND, so bob OR norman Shopify means “Shopify, plus bob or norman”.
Which fields you can use is defined per connection, not by the grammar. The products query reference lists status, tag, tag_not, vendor, product_type, sku, updated_at, inventory_total, published_status and many more. Some take a comma-separated list (status:active,draft), and the dates accept relative values such as updated_at:<now. Check the list for the version you are calling before you guess a field name.
Why does a Shopify GraphQL query filter return every product?
Shopify’s docs describe four ways to end up there, and none of them throws.
An invalid field name
The grammar page says it plainly: “If you specify an invalid field, then the query is ignored and all results are returned.” The same page’s debugging example tells a different story. It sends name:blue jeans (products have title, not name) and shows an empty nodes array next to this warning:
{
"data": { "products": { "nodes": [] } },
"extensions": {
"search": [
{
"path": ["products"],
"query": "name:blue jeans",
"parsed": {
"and": [
{ "field": "default", "match_all": "jeans" },
{ "field": "name", "match_all": "blue" }
]
},
"warnings": [{ "field": "name", "message": "Invalid search field for this query." }]
}
]
}
}
Every result or none. We cannot tell you which one your store returns, because we did not test it. Both are wrong, and the docs show this case producing that warning, which is why the warning is the thing to check.
A space after the colon
first_name: Bob is not a field search. Per the docs, it searches for the terms “first_name” and “Bob” anywhere in the document. The value has to follow the colon with no whitespace.
Lower-case connectives and :=
The grammar defines the connectives as "AND" | "OR", and the docs say NOT “must be capitalized”. By the grammar, a lower-case or is just another search term. Equality is :, never :=; the docs spell out that orders_count:=16 searches for the literal term “=16”.
A metafield definition without filtering enabled
You can filter on metafields.{namespace}.{key}, but only once the definition has the adminFilterable capability switched on. The query-by-metafield guide says that without it, “queries return unfiltered results”. That guide does not say whether a warning comes back in that case, so do not count on one.
Check the definitions before you trust the filter:
query FilterableDefinitions {
metafieldDefinitions(first: 50, ownerType: PRODUCT, namespace: "custom") {
nodes {
namespace
key
type { name }
capabilities { adminFilterable { enabled eligible status } }
}
}
}
eligible says whether the definition can have the capability at all. If enabled is false, the guide shows how to switch it on with metafieldDefinitionUpdate. Then watch status: the schema’s MetafieldDefinitionAdminFilterStatus enum includes IN_PROGRESS (existing metafields “are currently being processed”) and FAILED, and only FILTERABLE means the filter is ready. A cheap second check is a canary: query a value you know no product has, such as metafields.custom.color:"zz-no-match". Any result means the filter is not being applied.
The same guide adds that text metafield matches are case-sensitive, booleans go unquoted (metafields.custom.eco_friendly:true), and reference fields need the full GID in quotes.
How should you quote and escape values?
The grammar lets a value be a bare name or a quoted string, single or double quotes, and says date values must be quoted (some field examples in the reference do show bare years, such as published_at:<=2024). The characters :, \, ( and ) are special and need a backslash.
The docs also contradict each other on multi-word values. The default-search row says title:Bob Norman means title:bob AND “norman” anywhere in the document. The title example further down says title:Caramel Apple is equivalent to the phrase query title:"Caramel Apple". Quote anything with a space in it and you avoid the question.
Quoting inside a GraphQL string literal gets ugly fast. The docs’ example for a value containing a double quote is "metafields.custom.description:\"24\\\" monitor\"". Pass the search string as a GraphQL variable instead, so there is only one layer of escaping, and build it in code:
// search.mjs - build Shopify search strings and fail loudly on parser warnings.
const COMPARATORS = new Set([':', ':<', ':>', ':<=', ':>=']);
/** Quote a string value, escaping backslashes and double quotes. */
export function quote(value) {
return `"${String(value).replace(/["\\]/g, (c) => `\\${c}`)}"`;
}
/** One field term. Numbers and booleans stay bare; strings and dates are quoted. */
export function term(field, value, comparator = ':') {
if (!COMPARATORS.has(comparator)) {
throw new Error(`Unknown comparator ${comparator}; there is no := in Shopify search`);
}
let v;
if (typeof value === 'number' || typeof value === 'boolean') v = String(value);
else if (value instanceof Date) v = quote(value.toISOString().replace(/\.\d{3}Z$/, 'Z'));
else v = quote(value);
return `${field}${comparator}${v}`;
}
export const and = (...terms) => terms.join(' AND ');
export const or = (...terms) => `(${terms.join(' OR ')})`;
export const not = (t) => `-${t}`;
/** Collect every warning from extensions.search in an Admin GraphQL response body. */
export function searchWarnings(body) {
const entries = body?.extensions?.search ?? [];
return entries.flatMap((entry) =>
(entry.warnings ?? []).map((w) => ({ path: entry.path, query: entry.query, ...w })),
);
}
/** Run an Admin GraphQL request and throw if the search parser complained. */
export async function adminQuery({ shop, token, version, query, variables, debug = false, fetchImpl = fetch }) {
const headers = { 'Content-Type': 'application/json', 'X-Shopify-Access-Token': token };
if (debug) headers['Shopify-Search-Query-Debug'] = '1';
const res = await fetchImpl(`https://${shop}/admin/api/${version}/graphql.json`, {
method: 'POST',
headers,
body: JSON.stringify({ query, variables }),
});
const body = await res.json();
if (body.errors) throw new Error(JSON.stringify(body.errors));
const warnings = searchWarnings(body);
if (warnings.length > 0) {
const detail = warnings.map((w) => `${w.field}: ${w.message} (in "${w.query}")`).join('; ');
throw new Error(`Search query was not applied as written: ${detail}`);
}
return body;
}
and(term('status', 'active'), or(term('tag', 'sale'), term('tag', 'clearance')), not(term('vendor', 'Acme'))) produces status:"active" AND (tag:"sale" OR tag:"clearance") AND -vendor:"Acme". Our tests check the strings the builder emits, and they feed adminQuery the documented warning payload above through a stubbed fetch. They do not prove Shopify parses those strings the way the grammar describes. Quoting a single-word value such as "active" is allowed by the grammar, but if a particular field misbehaves with quotes, drop them for that field.
How do you see how Shopify parsed your query?
Requests with malformed queries already get warnings in extensions.search. Send the header Shopify-Search-Query-Debug: 1 and, per the docs, you get the extension on every query, warnings or not, including the parsed tree. That tree is the fastest way to spot a term that landed in default when you meant it for a field. Turn it on in development and in a test that runs your real filters against a dev store. In production, the warning check is enough.
Do range searches need a matching sort key?
They can. The docs warn that a range query “might be slow or timeout” on a large collection when the search field differs from the sort key, and suggest matching them. A sync job that pulls recently changed products should pair updated_at:>= with sortKey: UPDATED_AT:
query ProductsSearch($query: String!, $cursor: String) {
products(first: 50, after: $cursor, query: $query, sortKey: UPDATED_AT) {
nodes { id title updatedAt }
pageInfo { hasNextPage endCursor }
}
}
For a full export rather than a delta, a bulk operation is the better tool than paging this.
What we would do
Treat every search string as code. Build it with a helper like the one above, pass it as a variable, and make any extensions.search warning a hard failure in your client. If the documented “all results are returned” behaviour is what your store does, a cleanup job scoped to status:draft with one mistyped field becomes a job that walks the whole catalogue. For metafield filters, check adminFilterable at deploy time and keep a canary query in your test suite, since that failure may not produce a warning at all. The one case where we would not bother is a one-off query in GraphiQL, where you can read the results yourself.
We build and maintain Shopify apps and integrations that lean on the Admin API, including the sync jobs where a bad filter does the most damage. If you have one that is drifting, our Shopify development team can look at it.