The GraphQL node in n8n: queries, variables and cursor-based pagination
Published 23 August 2026 · 6 min read
Shopify made its GraphQL API the reference for all new development, Linear exposes nothing else, GitHub and Monday.com depend on it for anything beyond their most common operations — and yet most n8n guides that cover them treat GraphQL as an implementation detail to work around rather than a tool to master. A study by Gleison Brito and Marco Tulio Valente, published at the IEEE International Conference on Software Architecture in 2020 ("REST vs GraphQL: A Controlled Experiment", see it on Google Scholar), measures what that choice actually changes: across the tested scenarios, GraphQL clients transferred noticeably less unnecessary data than their REST equivalents, at the cost of queries that are harder to write correctly. That trade-off plays out directly inside n8n's GraphQL node. This guide covers its configuration, a common trap around error handling, parametrizing with variables, and cursor-based pagination — the point that trips up the most people, since it has no equivalent to the HTTP Request node's Pagination option.
n8n's native GraphQL node
n8n ships a dedicated GraphQL node, separate from HTTP Request, with four fields worth knowing:
- Endpoint — a single URL, unlike REST where every resource has its own; it's the query that determines what comes back, not the URL.
- Query — the text of the GraphQL query or mutation, in standard format.
- Variables — a JSON object that feeds the parameters declared in the query (more on this below).
- Authentication — None, Basic Auth, Header Auth, Query Auth, or Predefined Credential Type when n8n offers a native credential for the target service (GitHub, Shopify, Linear...), which saves you rebuilding the header by hand.
It's this same single-entry-point principle that our guides to Linear and Monday.com document — two services where the GraphQL API isn't an advanced option but the only way in behind the dedicated node.
The trap: a 200 OK hiding an error
This is the most common surprise for anyone discovering GraphQL coming from REST: a malformed query, a nonexistent field, or a denied authorization almost never return an HTTP error status. The server responds with 200, with a body that contains an errors field (message, path of the offending field, optional code) alongside a data field that's partially or entirely null. n8n's GraphQL node faithfully follows this specification: as long as the HTTP response is a success, the node does not fail, even if the query completely failed server-side.
The fix is simple but never sets itself up: an IF node right after the GraphQL node, testing {{ $json.errors && $json.errors.length > 0 }}. The true branch routes to your usual error-handling logic — see our guide to error handling with Error Workflow — instead of letting a null data.user silently propagate through the rest of the workflow and crash a node much further down, with an error message that no longer has anything to do with the real cause.
Variables and parametrized queries
Hardcoding the ID or filter directly into the query text works once, then forces you to duplicate the node for every case. GraphQL's best practice — declaring typed variables in the query and supplying them separately — applies as-is in n8n:
query GetIssue($id: String!) {
issue(id: $id) {
title
state { name }
}
}
The node's Variables field then receives a JSON object built with a standard n8n expression:
{ "id": "{{ $json.issueId }}" }
This split between the query's fixed shape and its dynamic parameters avoids accidentally injecting quotes or special characters into the query text — the same risk, and the same fix, as parametrized queries on the SQL side, covered in our Postgres node guide. For building the JSON object itself from more complex input data, our guide to n8n expressions and syntax covers string and object manipulation inside node fields.
Native GraphQL node or HTTP Request: when to switch
The GraphQL node covers most needs, but three situations push toward a plain HTTP Request node in POST, with a {"query": "...", "variables": {...}} body:
- A predefined credential exists for the service, but only on the HTTP Request side — a case documented in our guide to connecting Shopify to n8n, where the GraphQL Admin API is driven by HTTP Request using the same token as the native Shopify node.
- Very specific headers that the GraphQL node's form doesn't expose.
- A need for fine control over pagination and retries, covered next — the HTTP Request node fits more naturally into a manual loop and into a retry-with-backoff pattern.
In both cases, the GraphQL query itself doesn't change a single line: only the node sending it differs.
Cursor-based pagination: the real friction point
The HTTP Request node's Pagination option (incrementing an offset, following a "next page" URL) was designed for REST — see our full guide to API pagination. It doesn't map to anything on the GraphQL side, where nearly every API (Shopify, GitHub, and Linear included) uses the Relay connection pattern: each query returns a pageInfo field with hasNextPage (boolean) and endCursor (an opaque token, not a page number), to be fed back into the next call via an after variable.
Neither the HTTP Request node's native pagination nor the Loop Over Items node fits here: Loop Over Items iterates over data already sitting in memory, while the actual need is to call the API again as many times as necessary, without knowing in advance how many pages exist. The pattern that works is a hand-built loop:
- A Set node initializes the cursor to
nullfor the first call. - The GraphQL (or HTTP Request) node runs the query with
after: {{ $json.cursor }}. - An IF node tests
{{ $json.data.products.pageInfo.hasNextPage }}: if true, the branch loops back to step 2 with the cursor updated toendCursor; if false, the flow moves on to aggregating the accumulated results.
This is a loop built by wiring nodes backward on the canvas, not a Loop Over Items — the same incremental-resumption principle described in our guide to loops in n8n. For a large scheduled import rather than an on-demand one, keeping the last processed cursor between runs (with $getWorkflowStaticData or a Supabase table) avoids re-walking the whole dataset from scratch on every run.
Rate limits: by complexity rather than by call count
Another difference from REST: several GraphQL APIs (Linear, GitHub) don't cap a raw request count but a "cost" budget computed from each query's complexity — number of fields, nesting depth, size of the requested lists. One broad query that pulls a lot of nested fields in a single call can consume as much budget as ten simple REST requests. The reflex stays the same as for any external API: catch the rate-limit error code, back off progressively, and bound your retries — see our guide to retries and timeouts with the HTTP Request node for the mechanics.
Three concrete cases where the switch pays off
- Shopify — the native node covers orders and products; switching to the GraphQL Admin API avoids several REST calls to fetch variants, metafields, and multi-location inventory in a single query (see our Shopify guide).
- GitHub — the native node covers issues and pull requests; checks, projects, and discussions go through the GraphQL API, documented in our GitHub automation guide.
- Linear — the API is exclusively GraphQL behind the native node; custom labels, cycles, and advanced sub-issues are written directly as queries, as detailed in our Linear connection guide.
Summary
n8n's GraphQL node covers the standard setup — single endpoint, query, variables, authentication — but leaves three responsibilities to the workflow itself: checking the errors field despite a 200 status, building cursor-based pagination with a manual loop rather than a built-in option, and watching rate limits measured in complexity rather than call count. Once these three reflexes are in place, they apply identically across Shopify, GitHub, Linear, and any other GraphQL API your workflows go on to query.
FAQ
Frequently asked questions
Is n8n's native GraphQL node enough for every case?
For most queries and mutations, yes. It falls short on three specific points: authenticating with a third-party service's predefined credential (Shopify, Linear...), a handful of very specific headers, and above all automatic pagination, which the node doesn't have. In those cases, an HTTP Request node in POST with a {query, variables} body reproduces the exact same call, with more control.
Why doesn't my GraphQL node return an error even though the query failed?
Because the GraphQL specification responds with an HTTP 200 status almost systematically, even on error: the detail sits in an errors field in the response body, not in the HTTP status code. n8n's GraphQL node doesn't automatically fail the execution in that case — you need to check for that field yourself with an IF node right after.
How do you paginate a GraphQL API in n8n without a dedicated pagination node?
The HTTP Request node's Pagination option (offset, next URL) doesn't apply to the cursor-based pagination most GraphQL APIs use (the pageInfo pattern with hasNextPage and endCursor). The pattern that works is a manual loop: a node that calls the query again, feeding back the cursor received on the previous iteration, until hasNextPage turns false.
Can n8n's GraphQL node be used as a tool for an AI agent?
Yes, exactly like an HTTP Request node: added as a Tool on an AI Agent node, with a clear description of what the query returns, it lets the agent query the GraphQL API autonomously based on the conversation — for example to look up a Linear ticket or a Shopify product on request.
Bundle FlowKit Complet
€269