Jira exports worklogs to CSV in two shapes, and most of the pain in this topic comes from getting the shape you didn’t want. The two-click route, the Export menu on an issue search, produces one row per issue with Time Spent as a single rolled-up number of seconds. That answers budget questions and nothing else: who logged the hours and on which days is not in the file in any usable form. To get actual worklog rows out of Jira you have three options. The Log Work columns hidden in the all-fields export, which carry a date bug serious enough to disqualify them for billing. A short script against the worklog REST API. Or an app that keeps its own copy of the worklog data and exports it cleanly.
I co-founded Planim Time, a desktop Jira tracker, and the Export CSV button in its report view exists because I got tired of running the script version by hand every month. This post walks each route, what the file actually contains, and the two open Jira bugs plus one endpoint shutdown to know about before trusting any of them.
Decide first: totals or rows
Every worklog export question reduces to grain. Per-issue totals tell you whether a project is over budget. Per-entry rows are what an invoice, a timesheet, or a payroll reconciliation needs: one line per act of logged work, with an author and a date. Jira’s built-in export produces the first natively and the second only in a broken form, so pick your route by the question you’re answering.
| What you’re producing | Grain | Route |
|---|---|---|
| Budget check on a project or version | Totals | Issue search export |
| Client invoice with dates | Rows | API script, or an app |
| Per-person timesheet for a month | Rows | API script, or an app |
| Quick look at where time went | Totals | Issue search export |
If the totals grain is enough, you may not need an export at all; the built-in time tracking reports cover some of those questions in place.
Route 1: the issue search export
Run a search in the issue navigator, usually narrowed with the worklog JQL fields:
worklogAuthor = currentUser() AND worklogDate >= "2026-06-01" AND worklogDate <= "2026-06-30"
Then open the Export menu and pick a CSV variant: current fields exports the columns you see, all fields exports everything. Two facts about what lands in the file.
First, the Time Spent column is one aggregate number per issue, in raw seconds, summed over every worklog the issue has ever had. Your June JQL narrowed which issues qualify, but the total still includes January’s hours. There is no way to make this column respect a date range, because the JQL worklog fields filter issues, not entries.
Second, the all-fields export does contain per-entry data, in a set of Log Work columns: one column per worklog, each cell packing the comment, a timestamp, the author, and the seconds into one semicolon-separated string. It looks parseable, and people build invoicing spreadsheets on top of it. Don’t. The timestamp in that cell is when the entry was created, not the started date the user entered. Atlassian tracks this as JRACLOUD-77926 and JRASERVER-64025, both open for years. Anyone who backfills worklogs after the fact, which in my experience is most people in the week before an invoice goes out, gets every backfilled entry stamped with the day they typed it in rather than the day they worked. The file looks right, sums right, and bills the wrong days.
The columns also multiply: one column per worklog means an issue with eighty entries adds eighty columns to the sheet, and the widest issue in your search sets the width for everyone. And the export itself is capped at 10,000 issues per file on Cloud (raised from 1,000 in March 2025); past that you batch the JQL or move to the API.
Verdict: fine for totals, structurally unfit for timesheets.
Route 2: a script against the worklog API
The REST API is the only plugin-free way to real worklog rows, and the data it returns is correct: the started field is the date the user entered, exactly what the CSV export loses.
One 2025 change first. Atlassian shut down the old /rest/api/3/search endpoint in late 2025, so most pre-2025 export scripts you’ll find on Stack Overflow now fail with 410 Gone. The replacement is GET /rest/api/3/search/jql, which paginates with a nextPageToken instead of startAt and returns only IDs unless you ask for fields explicitly. Server and Data Center keep the old /rest/api/2/search.
The whole job is two loops: find the issues, then pull and filter their worklogs.
import csv, requests
BASE = "https://your-domain.atlassian.net/rest/api/3"
AUTH = ("[email protected]", JIRA_TOKEN)
SINCE, UNTIL = "2026-06-01", "2026-06-30"
def issue_keys():
token, keys = None, []
while True:
params = {"jql": f'worklogDate >= "{SINCE}" AND worklogDate <= "{UNTIL}"',
"fields": "key", "maxResults": 100}
if token:
params["nextPageToken"] = token
page = requests.get(f"{BASE}/search/jql", params=params, auth=AUTH).json()
keys += [i["key"] for i in page["issues"]]
token = page.get("nextPageToken")
if not token:
return keys
def rows(key):
start = 0
while True:
page = requests.get(f"{BASE}/issue/{key}/worklog",
params={"startAt": start, "maxResults": 100},
auth=AUTH).json()
for w in page["worklogs"]:
day = w["started"][:10] # the date the logger entered
if SINCE <= day <= UNTIL:
yield [key, w["author"]["displayName"], day,
w["timeSpentSeconds"]]
start += page["maxResults"]
if start >= page["total"]:
return
with open("worklogs.csv", "w", newline="") as f:
out = csv.writer(f)
out.writerow(["issue", "author", "date", "seconds"])
for key in issue_keys():
out.writerows(rows(key))
The per-entry filter in rows() is not optional. JQL matched issues with at least one June worklog, but those issues arrive with their whole history attached, and without the SINCE <= day <= UNTIL check your June file quietly includes May.
One subtlety we hit building the same logic into Planim Time: started comes back with the timezone offset it was logged in, like 2026-06-30T23:30:00.000+0200. Slicing the first ten characters keeps the date in the logger’s own timezone, which is almost always what an invoice wants. But if your team spans timezones and you normalise everything to UTC instead, entries logged late in the evening migrate across midnight onto the next day, and a month’s total shifts by a few hours at each end. Pick one convention and write it down before someone reconciles the export against payroll.
I left worklog comments out of the CSV on purpose: on Cloud they arrive as Atlassian Document Format, a nested JSON structure, and flattening it to text is its own chore. Auth setup, the 400s and 429s you’ll meet, and the bulk worklog/updated endpoints for whole-instance pulls are all in the worklog REST API guide; the script above is just the export-shaped corner of that API.
Route 3: a Marketplace app
Tempo and Clockwork both maintain a real timesheet surface inside Jira, and both export it to CSV or Excel from their report views, with correct work dates, because they read worklogs through the API rather than through Jira’s export pipeline. If your team already pays for one, its export button is the shortest path in this post and you can stop reading.
If it doesn’t, installing a Marketplace app only to get exports is a hard sell: they need an admin, and they bill per Jira user whether or not that user tracks time. The arithmetic of that, and what teams use instead when the goal is billing clients out of Jira data, is its own topic.
Route 4: a tracker with its own worklog store
Desktop trackers sit in the same position as Marketplace apps, one step closer to the data: they already mirror worklogs through the API into a local store, so exporting is just writing that store to a file. In Planim Time the report view has an Export CSV button with selectable columns, and the dates are the started dates because the store is API-fed; it is, honestly, the script from Route 2 productised, with the sync loop keeping the data current instead of a monthly manual run.
This route makes sense when the export is recurring. If finance wants a worklog file every month, a tracker that’s already syncing beats re-running a script and re-explaining its timezone convention.
Opening the file in Excel
Three small things go wrong between a correct CSV and a correct spreadsheet.
Durations arrive in seconds, both in the native export’s Time Spent column and in the script above. Convert with =D2/3600 for decimal hours, or =D2/86400 with the cell formatted as [h]:mm for a duration display. Resist exporting pre-formatted strings like 1h 30m; they read nicely and sum as zero.
The native all-fields export’s Log Work cells contain semicolons inside quoted fields. Excel locales that use semicolon as the list separator (most of Europe) split those cells into fragments on a plain double-click open. Import via Data, From Text/CSV, and set the delimiter to comma explicitly.
Keep dates as ISO 2026-06-09 strings. Excel’s locale guessing turns ambiguous formats like 06/09/2026 into June or September depending on the machine that opens the file, and a billing dispute over which is not hypothetical.
Which route, in one table
| Scenario | Route |
|---|---|
| One-off budget check | Issue search export, current fields |
| Invoice or timesheet, occasionally | The API script above |
| Invoice or timesheet, every month | An app or tracker with its own export |
| Whole-instance reporting pipeline | API, the bulk endpoints from the REST guide |
The built-in export isn’t broken, it’s answering a different question: how much time each issue has absorbed, ever. The moment your CSV needs a true date on every row, the native pipeline has nothing safe to offer, and the API stops being the advanced option and becomes the only one.