DEV Community

FlareCanary
FlareCanary

Posted on

Jira's agile API drops offset pagination on November 1, 2026 — three ways your board and sprint syncs silently truncate

If you sync data out of Jira — a sprint-report dashboard, a Snowflake warehouse pulling issue history, a Slack bot that posts backlog counts, a custom burndown chart, anything reading the agile REST API — you have a pagination change coming that fails in the worst possible way: quietly.

On November 1, 2026, Jira Software Cloud removes random page access from a set of nine agile endpoints. The Jira Software Cloud changelog gives a six-month deprecation window and is explicit: the endpoints that page with startAt will be removed, and integrations must move to token-based pagination using nextPageToken.

The endpoints in scope are the ones every agile integration leans on:

  • GET /rest/agile/1.0/board/{boardId}/issue — issues for a board
  • GET /rest/agile/1.0/board/{boardId}/backlog — issues in the backlog
  • GET /rest/agile/1.0/board/{boardId}/sprint/{sprintId}/issue — issues in a sprint
  • GET /rest/agile/1.0/board/{boardId}/epic/{epicId}/issue — issues under an epic
  • GET /rest/agile/1.0/sprint/{sprintId}/issue
  • plus the board/epic/sprint listing endpoints

The loud failure is the one teams plan around: after November 1 the deprecated startAt path is gone, you get an error, you redeploy. The failures worth thinking about happen before that — during the migration window, while both shapes work and the integration looks "done." Three of them swallow issues without raising anything.

What actually changed in the response

Two things, and the second is the one that bites:

Surface Old (offset) New (token)
Page position startAt query param nextPageToken query param
End-of-results signal startAt + maxResults >= total isLast: true / nextPageToken absent
total returned on every response no longer returned
Random access jump to any page; prefetch in parallel sequential only — you must walk the tokens

A response now looks like this:

{
  "maxResults": 50,
  "issues": [ ... ],
  "nextPageToken": "CAEaAggB",
  "isLast": false
}
Enter fullscreen mode Exit fullscreen mode

No startAt. No total. You paginate by passing nextPageToken back on the next request, and you stop when isLast is true (or nextPageToken is absent). If you need a count, Atlassian points you at a separate approximate-count endpoint — the number is no longer free on the page you already fetched.

Every one of the failure modes below comes from old code that assumed total exists and that a short page means the end.

1. total is gone, so while (startAt < total) exits after page one

The single most common offset loop in the wild:

start_at, total = 0, None
issues = []
while total is None or start_at < total:
    resp = session.get(
        f"/rest/agile/1.0/board/{board_id}/issue",
        params={"startAt": start_at, "maxResults": 50},
    ).json()
    issues.extend(resp["issues"])
    total = resp["total"]          # <-- KeyError, or .get() -> None
    start_at += 50
Enter fullscreen mode Exit fullscreen mode

When total stops coming back, one of two things happens, and both fail open:

  • If the code reads resp["total"] directly, you get a KeyError — at least that's loud. But most production clients learned long ago to use resp.get("total") defensively.
  • With total = resp.get("total")None, the loop condition start_at < None raises in Python 3, but the very common guarded form while total is None or start_at < total: evaluates total is None as True forever — so this one actually loops. The nastier variant is while start_at < (total or 0):, which is 0 < 0False after the first page. One page of issues, loop exits clean, no error.

That last shape — (total or 0), total ?? 0, total || maxResults — is everywhere, because it's the idiom people reach for to "handle the case where total is missing." After November 1 (and on the new endpoints before then), it silently caps every sync at the first maxResults issues. A board with 800 issues reports 50. The burndown chart still renders. The sprint report still has rows. Nobody alerts on "the dashboard has data, just less of it."

2. The "short page means done" heuristic drops the tail

The other way teams avoid depending on total is to stop when a page comes back shorter than maxResults:

let issues = [], token = undefined;
do {
  const page = await getIssues({ maxResults: 50, nextPageToken: token });
  issues.push(...page.issues);
  token = page.nextPageToken;
} while (page.issues.length === 50);   // <-- the bug
Enter fullscreen mode Exit fullscreen mode

With offset pagination, a full page reliably meant "there's probably more," and a short page meant "you're at the end." Token pagination breaks that assumption. Token-based pages are not guaranteed to be full — the server can hand you 30 issues on a page with isLast: false and a valid nextPageToken, and the next page has the rest.

This is the exact failure Atlassian integrators have been hitting on the platform's matching JQL migration: any query returning more results than maxResults silently drops results, with no indication that more exist. The loop above sees a 30-issue page, decides 30 !== 50 means "done," throws away a perfectly good nextPageToken, and returns a truncated set. The only correct end-of-results signal is isLast === true or an absent nextPageTokennever the length of the page.

If you have any pagination loop whose exit condition references .length, len(...), count, or === maxResults, that's the first callsite to fix.

3. Parallel prefetch and persisted offsets quietly stop working

Two performance patterns that were safe with startAt and aren't with tokens:

Parallel page prefetch. Dashboards that need a whole board fast often fan out requests — startAt=0, startAt=50, startAt=100 — concurrently, because with offsets you can compute every page boundary up front from total. Token pagination is strictly sequential: you can't know page N's token until you've fetched page N-1. A migration that keeps the parallel fan-out has nothing to fan out — it either collapses to fetching page one several times, or guesses offsets the new endpoint ignores. You get the first page, in triplicate, and call it the board.

Persisted offset high-water marks. Long-running cron syncs frequently store "I got up to startAt=400, resume there tomorrow." There is no numeric offset to persist anymore, and the cursor token is opaque and not documented as stable across runs. A pipeline that resumes from a stored offset against the new endpoint resumes from nowhere — silently re-ingesting from the top or skipping the delta, depending on how the resume code degrades. The durable pattern is to re-paginate from the start each run and filter by updated date, not to persist position.

What to grep for before November 1

# Agile endpoints that page by offset
grep -rE 'rest/agile/1\.0/(board|sprint).*(issue|backlog)' src/

# Loops that depend on total
grep -rE "\.get\(['\"]total|\[['\"]total['\"]\]|total\s*\|\|\s*0|total\s*\?\?\s*0" src/

# End-of-results heuristics based on page length, not isLast
grep -rE "length\s*===?\s*\w*max|len\(.*\)\s*<\s*max|< maxResults" src/

# Parallel offset fan-out
grep -rE "startAt\s*[:=]\s*(i\s*\*|page\s*\*|\d+\s*\*)" src/
Enter fullscreen mode Exit fullscreen mode

The non-grep audit is the connector check: any Fivetran, Airbyte, Workato, n8n, or homegrown ETL job that reads /rest/agile/1.0/board/.../issue with startAt needs to move to nextPageToken and stop trusting total. And anything that persists a numeric pagination offset between runs needs to switch to a date high-water mark.

Why this one lands silently

Every failure here returns 200 OK with a syntactically perfect, semantically incomplete response. The first page is real issues. The JSON parses. The dashboard renders. The healthcheck — which almost always asserts "did the last call return 2xx" — stays green. The only loud signal, the removed startAt endpoint, doesn't fire until November 1, by which point the silently-truncated version may have been shipping wrong sprint metrics for weeks.

Token pagination is the right call on Atlassian's part — offset paging over a moving issue set was never consistent. But "more correct" and "drop-in compatible" are different things, and the gap between them is exactly where a passing test suite and a clean error feed stop meaning what you think they mean.


FlareCanary watches the response shapes of the APIs you depend on and tells you when a field like total stops appearing, when a pagination contract changes, or when row counts shift in ways that look normal but aren't. Jira's agile pagination migration is precisely the kind of 200-OK-but-wrong transition that slips past healthchecks and Sentry alike. flarecanary.com

Top comments (0)