DEV Community

monday.com's API now returns 200 users instead of your whole account - the 2026-07 version flip that fails silently

If you sync users out of monday.com - an SSO deprovisioning job, an HR-to-monday roster sync, a SCIM-ish reconciliation script, a dashboard that counts seats, anything that runs query { users { id email } } - there's a change that went live on July 1, 2026 and fails in the worst way: it returns a 200, valid JSON, and the wrong number of users.

monday.com versions its API by date. Version 2026-07 entered release-candidate on April 1, 2026 and became the Current (default stable) version on July 1, 2026. The detail that makes this silent is monday's own documented default: If you don't pass an API-Version header, your app will always get the Current version. So every integration that doesn't pin an explicit version - and a lot of them don't, because the SDKs make it optional - rolled onto 2026-07 on July 1 with no deploy, no changelog read, no error.

Integrations pinned to an older version aren't safe either: when monday deprecates a version, requests to it fall forward to the Maintenance version (that's exactly what the February 15, 2026 deprecation did - it routed 2025-01 and 2024-10 to 2025-04). Either path lands you on the new Query.users behavior eventually.

And the 2026-07 release is a full overhaul of the User entity. Most of it is documented as a breaking change. The parts that bite are the ones that don't break loudly.

What changed in Query.users

Surface Before 2026-07 2026-07 (Current since Jul 1)
No limit argument all matching users returned 200 users returned
Maximum limit unbounded 1000 (over that β†’ error)
created_at Date (2024-01-15) ISO8601DateTime! (2024-01-15T09:30:00Z, non-null)
birthday Date String
utc_hours_diff Int Float
emails argument [String] [String!] (nulls in the array now rejected)
is_admin, is_guest, is_view_only, is_pending, enabled, is_verified present deprecated (removed in 2026-10)
kind, newest_first, non_active arguments present deprecated β†’ user_kind, sort, status (removed in 2026-10)

The headline is the first row. The rest stack on top of it.

1. Query.users without a limit silently caps at 200

This is the query that's in everyone's codebase:

query {
  users {
    id
    email
    name
  }
}

Before 2026-07, that returned every user in the account. As of July 1 it returns the first 200 and stops - no nextPageToken-style signal in the response telling you there's more, no error, no warning header. A 700-seat account syncs 200 users and silently drops 500.

Think about what runs that query:

  • SSO / SCIM deprovisioning. A nightly job pulls the monday user list, diffs it against the IdP, and disables anyone who left. After July 1 it only ever sees the first 200 users - so offboarded people in the tail of the list never get deprovisioned. That's not a cosmetic bug; it's dangling access to a system of record.
  • Seat / license reconciliation. Finance counts users to true-up billing. The count quietly drops to 200 and the numbers look better, so nobody investigates.
  • HR roster sync. New hires past the 200th user never appear downstream.

The fix is explicit pagination - you now have to page with limit and page:

query($page: Int!) {
  users(limit: 1000, page: $page) {
    id
    email
  }
}

…and loop until a page comes back with fewer than limit rows. Note limit itself is now capped at 1000; asking for more is one of the few things here that does error, which is at least honest.

2. Three scalar type changes that parse fine and store wrong

created_at went from a bare Date to a non-null ISO8601DateTime!. The field name didn't change, it's still a JSON string, the query still returns 200 - but the value went from 2024-01-15 to 2024-01-15T09:30:00Z. Anything that:

  • writes it into a DATE column (now you're truncating or erroring on the time component, depending on the driver),
  • does string equality against a stored YYYY-MM-DD,
  • or parses with a format string that doesn't expect a T/Z,

now silently mismatches. Date-range filters that compare created_at >= '2024-01-15' as strings can flip results because '2024-01-15T09:30:00Z' > '2024-01-15' lexically.

utc_hours_diff went from Int to Float. The whole point is half-hour and 45-minute offsets - India is 5.5, Nepal 5.75. Code that did parseInt, integer math, or unmarshalled into a Go int / Java int either drops the fraction silently or, in strict-typed clients, throws on the decimal. A "schedule this user's reminder in their timezone" feature is now off by 30 minutes for a chunk of the planet.

birthday went Date β†’ String. Looser, but the same shape of trap: code expecting a parseable date object gets a free-form string.

3. The second shoe: 2026-10 removes the fields you're still reading

In 2026-07 the old User fields are deprecated but still work. That's the trap - your code keeps reading them, your tests stay green, and the migration looks done. On the 2026-10 version they're removed outright:

  • Permission booleans is_admin, is_guest, is_view_only, is_pending, enabled, is_verified β†’ replaced by a single status (a UserStatus enum) plus user_kind. An access-control check like if (user.is_admin) either starts erroring on an unknown field (loud, if your client validates the schema) or - with permissive clients and fragment spreads - reads null/undefined and treats an admin as a non-admin, or vice-versa. Either direction is a security-relevant silent failure.
  • Photo fields photo_original, photo_thumb, photo_thumb_small, photo_tiny, photo_small β†’ replaced by a nested photo_url { original thumb tiny ... } object. Avatars silently 404 / go blank.
  • Argument renames kind β†’ user_kind, newest_first β†’ sort, non_active β†’ status. A query filtering users(non_active: true) to find deactivated accounts keeps working through the 2026-07 window, then loses the argument in 2026-10 - and depending on how your client handles an unknown argument, you either error or silently get the unfiltered set (active users where you expected inactive).

If you only fix the 200-cap and stop, you've bought three months. The field removals are the same migration; do them now.

What to grep for

# monday user queries with no limit (the 200-cap)
grep -rEi 'users\s*(\([^)]*\))?\s*\{' src/ | grep -vi 'limit'

# Deprecated permission booleans removed in 2026-10
grep -rE 'is_admin|is_guest|is_view_only|is_pending|is_verified|\.enabled' src/

# Deprecated photo fields -> photo_url { ... }
grep -rE 'photo_(original|thumb|thumb_small|tiny|small)' src/

# Deprecated query arguments
grep -rEw 'kind|newest_first|non_active' src/

# created_at parsed/stored as a bare date
grep -rE 'created_at' src/ | grep -iE 'date|split|substr|strftime|DATE'

# And the root cause: requests that never pin a version
grep -rEi 'api-version|monday.*version' src/

The non-grep audit is the connector check: any Make, Zapier, n8n, Workato, or homegrown job hitting api.monday.com/v2 that doesn't set an explicit API-Version header is, by monday's own rule, running on 2026-07 right now.

Why this one lands silently

Pin an explicit API-Version header and none of this touches you until you decide to move - that's the entire point of date-based versioning, and monday tells you to do it. But the default for an unversioned request isn't "stay where you are," it's "always get Current." So the integrations most exposed are the ones nobody's looked at in a year: the quiet nightly sync that's worked since 2024 and has no version header because it predates versioning.

Every failure here returns 200 OK with syntactically perfect, semantically incomplete data. The user list parses. The count is a plausible number. The deprovisioning job exits zero. The healthcheck that asserts "last call was 2xx" stays green. The only loud signal - asking for limit over 1000, or hitting a removed field on a strict client - is the least likely path. The likely path is a roster that quietly got shorter on July 1 and a deprovisioning gap nobody will notice until an audit.

FlareCanary watches the response shapes of the APIs you depend on and tells you when a default limit changes, when a scalar like created_at starts carrying a time component, or when a field you read every night quietly disappears. monday's 2026-07 version flip is exactly the kind of 200-OK-but-wrong transition that slips past healthchecks and Sentry alike. flarecanary.com

Comments

No comments yet. Start the discussion.