Tutorial · Blog Post
Using the Monday.com GraphQL API with n8n
The built-in Monday.com node in n8n covers the basics. Two-way syncs, large boards, and migrations need the GraphQL API directly. This is how I call it from n8n: authentication, pagination, column value formats, rate limits, webhooks, and the echo loops that catch everyone building their first sync.
By Mehadi Hasan 10 min read
When the built-in node is enough
n8n's monday.com node handles boards, columns, groups, and items: create an item, get items, get items by column value, change one or several column values, move an item to a group, add an update. For a form that creates an item or a notification when something changes, use it and move on.
Switch to the HTTP Request node with raw GraphQL when you need any of these:
- Cursor pagination through boards too large to fetch in one call.
- Exactly the fields you need and nothing more, to stay inside the complexity budget.
- Several column types written in one mutation, with control over the JSON.
- Queries or mutations the node does not expose, such as linked items in connect boards columns or creating webhooks.
You do not need a second credential. The node's documentation notes you can reuse the monday.com credential in the HTTP Request node by choosing Authentication → Predefined Credential Type.
Authentication and the version header
Every request is a POST to https://api.monday.com/v2 with the token in the Authorization header (authentication docs). Personal tokens carry the permissions of the user who owns them, so create the token from a dedicated integration user rather than a person who might leave. That user also needs access to every board the workflow touches.
Send an API-Version header too (API versioning). Monday.com releases dated versions and retires old ones on a published schedule. Pinning a version means a platform change does not quietly alter your responses mid-sync; you upgrade deliberately instead.
HTTP Request node setup (placeholders in angle brackets)
POST https://api.monday.com/v2
Authorization: <YOUR_API_TOKEN>
API-Version: <VERSION, e.g. 2026-10>
Content-Type: application/json
{ "query": "...", "variables": { ... } }In the HTTP Request node, set the body to JSON and pass GraphQL through variables rather than string concatenation. It avoids escaping bugs, and an item name with a quote in it will not break your query.
Reading items: items_page and cursors
Large boards cannot be returned in one response. items_page returns a page of items plus a cursor; the limit defaults to 25 and goes up to 500. Ask only for the columns you use.
First page
query ($boardId: [ID!]) {
boards(ids: $boardId) {
items_page(limit: 100) {
cursor
items {
id
name
column_values(ids: ["status", "date4", "person"]) {
id
text
value
}
}
}
}
}Then call next_items_page (docs) with the cursor until it comes back null. Cursors expire 60 minutes after the first request, so a long run that pauses for rate limits must finish paging inside that window or start over. Using next_items_page at the root avoids the extra complexity cost of nesting items_page inside boards on every page.
Following pages
query ($cursor: String!) {
next_items_page(cursor: $cursor, limit: 100) {
cursor
items {
id
name
}
}
}In n8n, this is a small loop: an HTTP Request node, an IF node checking whether the cursor is empty, and a branch back to the request with the new cursor. Monday.com's docs also note that query_params (filters) and cursor can't be used in the same request: filter on the first call, then page with the cursor.
Writing values: one mutation, JSON-encoded columns
The column mutations are change_simple_column_value (a plain string), change_column_value (one column, JSON value), and change_multiple_column_values (several columns at once). Prefer the last one for syncs: one call per item instead of one per column, and fewer chances to leave an item half-updated.
The catch is that column_values is a JSON string keyed by column ID, so it is JSON inside JSON. Build the object in a Code or Set node, then stringify it once.
Mutation
mutation ($boardId: ID!, $itemId: ID!, $values: JSON!) {
change_multiple_column_values(
board_id: $boardId
item_id: $itemId
column_values: $values
) {
id
}
}Variables (placeholders in angle brackets)
{
"boardId": "<BOARD_ID>",
"itemId": "<ITEM_ID>",
"values": "{\"status\":{\"index\":1},\"date4\":{\"date\":\"2026-11-02\"},\"person\":{\"personsAndTeams\":[{\"id\":<USER_ID>,\"kind\":\"person\"}]}}"
}Column type gotchas
- Status: you can write by label (
{"label":"Done"}) or by index ({"index":1}). The status column docs recommend index as the stable choice across boards; labels break when someone renames them, and numeric-looking labels should always go by index. In a two-way sync, keep the mapping between the two boards' labels in configuration, not hard-coded in nodes. - People: the value is a
personsAndTeamsarray of IDs with akindof person or team (people column docs). You need Monday.com user IDs, not emails, so resolve and cache users first. - Dates: dates use
YYYY-MM-DD, and the raw value stores times in UTC while the text field converts to the calling user's time zone (date column docs). Compare raw values, not the text, or the same date can look different depending on whose token made the call. - Connect boards: write linked items as
{"item_ids":[...]}and read them back with an inline fragment onBoardRelationValue(connect boards docs). This is how client-facing and internal boards stay linked item to item. - Reading values:
textis for humans,valueis the raw JSON. Base sync decisions on raw values.
Complexity budget and rate limits
Monday.com limits API use in several ways at once: query complexity, per-minute budgets, daily calls, concurrency, and more (rate limits docs). Complexity is the one you feel first: every query has a cost based on what it asks for, and per-minute budgets follow a sliding window. The exact numbers depend on token type and plan and are subject to change, so read the current table rather than hard-coding a figure.
Practical rules that keep a sync inside the budget:
- Request only the fields and columns you use.
- Avoid deep nesting; paginate instead.
- Process writes in batches with Loop Over Items, with a Wait between batches.
- When you hit a limit, the error includes
retry_in_seconds. Wait that long before retrying, rather than retrying immediately. - Add a
complexityfield to a request to see the cost and remaining budget.
Checking complexity alongside a mutation (placeholders in angle brackets)
mutation {
complexity { query before after }
change_simple_column_value(board_id: <BOARD_ID>, item_id: <ITEM_ID>, column_id: "status", value: "Done") { id }
}Webhooks into n8n, including the challenge
Polling a board on a schedule works, but webhooks are what make a sync feel live. Monday.com can send events such as column changes, status changes, name changes, item creation, moves between groups, archives, and deletions (webhooks docs). You can create them from the board's integrations or with the create_webhook mutation.
When a webhook is registered, Monday.com first POSTs a JSON body containing a challenge token to your URL and expects the same token back as challenge in the response body. In n8n: a Webhook node set to respond using a Respond to Webhook node, an IF node that checks for a challenge field, and a branch that returns {"challenge": <the token>}. Every other request carries an event object and continues into the sync logic.
For webhooks created with an app integration token, requests can carry a JWT in the Authorization header that you verify against the app's signing secret. If you create webhooks another way, protect the n8n webhook URL itself and validate the payload shape before acting on it.
Two-way syncs: stopping echo loops
This is the trap in every two-way sync. A change on board A is written to board B. Board B fires its own webhook, and the workflow writes the same change back to board A, which fires again. Left alone, a single status change bounces between boards.
Two guards together handle it:
- Ignore your own writes. The webhook
eventincludes theuserIdthat made the change. If writes come from a dedicated integration user, drop events from that user. - Only write real differences. Before writing, read the target value and skip the mutation when it already matches the source. This also makes retries and replays safe.
On the Monday.com board sync I built, this design keeps client-facing and internal boards in step: bi-directional status sync through configurable mappings, cascading renames and deletions for linked items, and scheduled daily and 6-hour syncs that catch anything a webhook missed, with failures alerting to Slack in real time. The Trello to Monday.com migration platform drives the same GraphQL API at a larger scale, with a DataMapper translating between the two data models and an audit log of every step.
The takeaway
Use the built-in node for simple jobs. For syncs and anything at volume, call GraphQL from the HTTP Request node, pin an API version, page with cursors, write all columns in one mutation, respect the complexity budget, answer the webhook challenge, and design against echo loops from the first day. For the reliability side (error workflows, retries, alerts), see making n8n workflows production-ready.
Need boards that stay in sync?
If you would rather have it designed, built, and monitored for you, see my Monday.com automation service, or describe your boards and I will reply within 24 hours with an honest take on the approach.
Get a free 30-min automation auditFrequently asked questions
Do I need the HTTP Request node, or is the n8n Monday.com node enough?
The built-in node covers common board, group, column, and item operations, so use it for simple jobs. Switch to the HTTP Request node with GraphQL when you need cursor pagination on large boards, precise field selection to save complexity, several column types in one mutation, or operations the node does not expose. You can reuse the same monday.com credential in the HTTP Request node.
How do I paginate through a large Monday.com board?
Query items_page with a limit (up to 500) and request the cursor. Then call next_items_page at the root with that cursor until it returns null. Cursors expire 60 minutes after the first request, and filters (query_params) go on the first call only, since they cannot be combined with a cursor.
Why does my Monday.com webhook not register with n8n?
Monday.com verifies the URL by POSTing a challenge token and expects the same token back as a challenge field in the JSON response. Use a Webhook node that responds through a Respond to Webhook node, detect the challenge field, and return it.
How do I stop a two-way Monday.com sync from looping?
Make all writes from a dedicated integration user and ignore webhook events whose userId is that user, and compare the target value before writing so nothing is written when it already matches. Together those stop a change from bouncing between boards.
API details checked October 30, 2026 against the Monday.com and n8n documentation linked above. Limits and versions change; confirm current values before building.