Recover every Jira worklog when the API hands you only the oldest twenty
Gabriela Perdum
Author
12 min readAugust 29, 2026
Key takeaways
END STATE: a live worklog total that reconciles against the issue's own lifetime timespent field, plus the deleted worklogs no read endpoint will show you.
The failure is SILENT: worklog.total is correct, the entries are truncated to the oldest 20, and the sum just looks low.
The GA Rovo MCP tool list has no read-side worklog tool, but the PREVIEW server added getIssueWorklog in June 2026 — try that before building anything.
The worklog id lives in a sibling WorklogId changelog item, not on the timespent item. Pairing them is what makes the replay sound.
Reconciling against timespent certifies the LIVE TOTAL only — never worklog identity, dates, or the recovered deletions.
Somebody asked a question on the community that I want more people to see, because the failure it describes does not look like a failure.
Through the Rovo MCP server, getJiraIssue reports worklog.totalcorrectly — he had issues carrying more than four thousand entries and the count came back right. Then it returns the oldest twenty. There is no startAt, no maxResults, no startedAfter or startedBefore on that tool.
So you sum what you were given, and you get a number. It is clean, it is plausible, and it is wrong. He measured his month-level totals at roughly 21% under, with several issues reporting zero hours for a month in which time genuinely had been logged — because their whole month sat in the truncated tail.
By the end of this you will be able to detect that on your own data, recover the full set, and — the part that took me two rewrites to get honest — know exactly which half of your recovered number is proven and which half is not.
Note
Prerequisites
Node 18 or later, for the replay. Verified on v24.15.0.
An API token, or MCP access to an issue's changelog.
One issue you know has more than twenty worklogs. If you do not have one, step 1 will find you one.
About forty minutes.
The three files used below — replay.mjs, demo.mjs and fixture.json — are listed in full in this article. Put them in one directory.
Why summing the page is not a safe thing to do
Two facts have to sit together before this makes sense.
The GA tool list has no read-side worklog tool. Atlassian's supported tools page lists 55 unique tool names across 61 rows, fourteen of them Jira — eight read, five write, one search. The only tool name containing "worklog" is addWorklogToJiraIssue, which is a write. Nothing there wraps GET /rest/api/3/issue/{issueIdOrKey}/worklog, and that endpoint is precisely the one supporting every parameter the tools are missing. fetchAtlassian only accepts ARIs, so you cannot reach the REST endpoint that way either.
And the truncation is silent. A truncated list that also reported a truncated total would be obvious — you would see twenty of twenty and go looking. Reporting the true total and twenty entries is the shape that gets shipped, because every individual number looks right.
That combination is why this reaches a monthly report rather than a stack trace.
1
Detect the truncation by comparing the reported total against what you actually received.
2
Measure your own shortfall before you trust any report built on the page.
3
Read the live set properly
there is now a tool for this, and most people can stop here.
4
Replay the changelog for the one thing step 3 cannot give you
deleted worklogs.
5
Reconcile, and be precise about what that proves.
6
Push the ticket, with the volume story that actually moves it.
Step 1 — Detect the truncation
The check is two numbers that should agree and do not.
If shown equals total on every issue you check, you do not have this problem yet. Run the check across a whole project before concluding that, because it only takes one busy issue to skew a monthly figure.
Step 2 — Measure your own shortfall
Do not take 21% from me or from anyone. Measure it, because the number depends entirely on how your logging is distributed.
Sum the visible page and compare against the issue's own lifetime timespent field, which Jira maintains independently:
javascript
1exportfunctiontruncatedSum(worklogPage){2return(worklogPage.worklogs||[]).reduce((n, w)=> n +Number(w.timeSpentSeconds||0),0);3}45exportfunctionshortfallPct(pageSeconds, lifetimeSeconds){6if(!lifetimeSeconds)return0;// issues with no worklogs report timespent: null7return100*(1- pageSeconds / lifetimeSeconds);8}
That guard is not decoration. Jira returns timespent: null for an issue with no worklogs, and without it the untruncated case you are using as a control prints NaN%.
How you know it worked:node demo.mjs prints a non-zero shortfall on a truncated issue, and 0.0% on any issue with twenty or fewer worklogs. A non-zero shortfall on a small issue means something else is wrong and this procedure is not your answer.
Step 3 — Read the live set properly
Before building anything: check the preview server first. When the question was asked, the GA tool list had no read-side worklog tool. It still does not — but the preview tool list has carried one since June 2026:
getIssueWorklog — "List worklogs for a Jira issue to read entries and discover worklog IDs for edits or deletes."
The write tool is renamed addOrEditJiraIssueWorklog on the same surface. If you can point your client at https://mcp.atlassian.com/v1/mcp/preview, that is your answer and you can stop reading at step 5.
With a token, the equivalent is the REST endpoint the MCP server never wrapped:
This has no twenty-entry cap, and it returns each worklog's started date and author — which the changelog does not. Two things to know: maxResults is silently clamped at 5000 (ask for 99999 and the response echoes 5000 with HTTP 200), so page on startAt and trust total; and a worklog with visibility restrictions is omitted for anyone outside the group or role it is restricted to.
How you know it worked: the number of rows you collect equals the total the endpoint reports, and equals worklog.total from step 1.
Step 4 — Replay the changelog for the deletions
Step 3 gives you every worklog that still exists. It cannot give you the ones that were deleted — those appear in no read endpoint at all. If you are reconciling a disputed month rather than totalling one, that is the population you actually care about, and the changelog is the only place it survives.
This is the part the asker, Todd Joyce, worked out himself, and his observation is the load-bearing one: "Changelog items include WorklogId." Read that literally, because I did not at first and it cost me a rewrite.
A worklog write leaves one changelog entry holding several sibling items. The id is not a property of the timespent item — it is its own item, WorklogId, with a capital W and a capital I. Here is a real entry from a live issue, a deletion:
timespent carries the issue's cumulative total before and after. WorklogId carries the identity: the id is in to for an add or an edit, and in from for a delete. Pair the two.
javascript
1exportfunctionreplayWorklogs(changelog){2const entries = changelog.values|| changelog.histories||[];3const seen =newMap();4for(const entry of entries){5const items = entry.items||[];6const ts = items.find((i)=> i.field==="timespent");7const wl = items.find((i)=> i.field==="WorklogId");8if(!ts ||!wl)continue;// not a worklog write9const delta =Number(ts.to||0)-Number(ts.from||0);10const isDelete = wl.to==null;// structural — never the sign of delta11const id = isDelete ? wl.from: wl.to;12const prior = seen.get(id);13if(isDelete){14 seen.set(id,{...(prior ||{seconds:-delta }),deleted:true,deletedAt: entry.created});15}elseif(prior){16// Same id again = an EDIT. timespent deltas are cumulative, so accrue.17 seen.set(id,{...prior,seconds: prior.seconds+ delta,editedAt: entry.created});18}else{19 seen.set(id,{seconds: delta,deleted:false,recordedAt: entry.created});20}21}22const worklogs =[...seen.entries()].map(([id, w])=>({ id,...w }));23const live = worklogs.filter((w)=>!w.deleted);24return{25 worklogs,26 live,27deleted: worklogs.filter((w)=> w.deleted),28liveCount: live.length,29liveSeconds: live.reduce((n, w)=> n + w.seconds,0),30};31}
Three details in there are the whole difference between this working and quietly lying to you.
Deletion is detected structurally, not arithmetically.wl.to == null means deleted. An earlier version of this article matched a negative timespent delta against a live worklog of the same size — and everybody logs "1h", so with three one-hour worklogs it marks an arbitrary one deleted. The total still reconciles. The identities are wrong. Never guess by amount when the id is sitting on the next line.
An edit re-emits the same id, with to set and no from, exactly like an add. Accruing the delta onto the existing entry is what keeps a worklog edited from 2h to 3h recorded as 3h and not as a second worklog of 1h.
Fetch the changelog from the dedicated endpoint, and page it.?expand=changelog returns its entries under histories, not values — a replay written for one shape against the other returns a silent, confident zero. It is also capped at 100 entries with no way to page past it (JRACLOUD-59998, Closed/Fixed: "Limit changelog to 100 entries, and add /changelog endpoint for full history"). For a four-thousand-worklog issue that is the same silent shortfall you started with.
What the changelog will not give you is dates.entry.created is when the worklog was recorded, not the date it covers — somebody logging Friday's work on Monday produces a Monday entry. For live worklogs, take started from step 3. For deleted ones the worked date is simply gone, and the honest thing is to report them as an amount and an id, not as a date.
How you know it worked. Run it:
bash
1node demo.mjs
text
1worklog.total reported : 32
2entries actually returned: 20
34sum of the visible page : 21.0h
5replayed from changelog : 36.5h (32 live, 2 deleted)
6lifetime timespent field : 36.5h
7reconciles : yes
89shortfall if you trust the page: 42.5%
10worklog ids recovered : 34 (13408, 13420 deleted)
Be clear about what that run is.fixture.json is synthetic — 34 worklogs, two edited and two later deleted — but it is built in the exact changelog shape Jira Cloud emits, checked against a live issue. It demonstrates the mechanism; it proves nothing about your tenant. Step 5 is what you run on real data.
Step 5 — Reconcile, and be precise about what that proves
fields.timespent is maintained by Jira from the same writes by a different path, so agreement means two independent mechanisms landed on the same figure.
Here is the part I got wrong for a whole draft, and it is the most important sentence in this article: reconciliation certifies the live total and nothing else. It cannot certify worklog identity, it cannot certify dates, and it cannot certify the deletions — timespent tracks live worklogs only, so the recovered deleted set is validated by nothing at all. A replay that marks the wrong worklogs deleted still reconciles perfectly. I know, because mine did.
So: quote the live total as proven. Quote the deletions as recovered, with their ids, and let whoever is disputing the month check them individually.
How you know it worked:reconciles : yes. A non-zero delta means one of four things:
Your changelog is paginated and you have only part of it — the most common cause by far.
You are reading histories as values or vice versa, and getting zero.
The issue has restricted worklogs. timespent is an ordinary field with no field-level security, so it includes worklogs your token cannot read (JRASERVER-13880). Your replay will legitimately come in under it.
You are reconciling against aggregatetimespent, which rolls up sub-tasks. Note it does not roll up Epic to Story on company-managed projects (JSWCLOUD-18871).
Investigate rather than rounding — the whole point is a number nobody can wave away.
One route that does not work
The obvious way to recover worked dates without a token is JQL, probing per day:
text
1worklogDate = "2026-08-01" AND worklogAuthor = <account-id>
Do not do this. It fails three separate ways, all silent.
JQL returns issues, not worklogs. An issue with two worklogs on the same day counts once, and the same issue matches on every day it carries any worklog. Atlassian's own wording on the advanced search reference is "Search for work items with work logged on a specific date."
The clauses do not bind to the same worklog. These are issue-indexed fields, so worklogDate = D AND worklogAuthor = A returns issues where somebody logged on D and A logged at some point — not issues where A logged on D.
And JQL cannot see past 1,000 worklogs. From that same page, verbatim: "If a work item has more than 1,000 worklogs, JQL will only search the most recent ones." On the four-thousand-entry issues this whole article exists for, the invisible ones are the oldest — precisely the truncated tail you just recovered. It fails hardest exactly where you need it, in the same silent way as the original bug.
Worked dates come from started on the worklog endpoint. There is no shortcut. (worklogAuthor, incidentally, is not on that Cloud page at all — it is documented only for Data Center, though it does work in Cloud.)
Step 6 — Push the ticket, with the thing that moves it
The fix belongs in Atlassian's tooling, and there is a specific place to put your weight.
The ticket is ECO-1431 — "Support pagination for getJiraIssue and searchJiraIssuesUsingJql tools in MCP", a Suggestion under the API - Model Context Protocol component, status Gathering Interest. It was filed on 10 April 2026 and sat untouched for four months; as I write it has three votes, three watchers, and one comment — Todd's own, from 20 August, cross-linking the community thread. Gathering Interest's status description says it "needs more unique domain votes and comments before being reviewed by our team", so a concrete volume story is worth more than a vote.
Push the ticket's second ask, not its first. ECO-1431 carries both "Support pagination with the Jira tools for all fields that they return" and "Alternatively build a specific tool to return work logs that support pagination". The first has a long refusal behind it: JRACLOUD-34746, 39 votes, filed in 2013 and closed in 2017 with resolution Answered, Cezary Zawadka writing "We are sorry there was change in a behaviour. However as it was made due to performance reasons we want to keep it as it is" — and pointing at the dedicated worklog endpoint instead. That is a 2017 comment and I am quoting it, not narrating present intent. But the "alternatively" clause is the route Atlassian's own answer already sent people down — and getIssueWorklog on the preview server is that clause, shipped.
How you know it worked: open jira.atlassian.com/browse/ECO-1431 and check the summary matches what you are about to quote before you cite it.
There is corroboration outside the tracker if you want to show it is not a configuration problem on your side: GitHub issue 180 on atlassian/atlassian-mcp-server, open since June, titled "Worklogs beyond the first 20 per issue cannot be retrieved — no tool exposes GET /rest/api/3/issue/{issueIdOrKey}/worklog".
[[takeaways]] You can detect the truncation, you have a live worklog total that reconciles against Jira's own lifetime timespent, and you have the deleted worklogs that no read endpoint will show you — reported as what they are, recovered rather than proven.
The shortest version of all of this: try getIssueWorklog on the preview server first. If you can reach it, most of this article is unnecessary. The changelog replay earns its keep for deletions, for tenants pinned to the GA surface, and for the month somebody disputes.
And if you build the replay, pair timespent with its sibling WorklogId. Matching deltas by amount produces a total that reconciles beautifully and a set of worklogs that is quietly wrong — which is the same class of failure as the bug you started with, and harder to notice.
If you hit this, put your numbers on ECO-1431. A 21% shortfall and a four-thousand-worklog issue are the kind of specifics that move a suggestion out of Gathering Interest.