DEV Community

Cover image for How to Query Jira Worklog Data with JQL And Where JQL Falls Short
Alina Chyzh
Alina Chyzh

Posted on

How to Query Jira Worklog Data with JQL And Where JQL Falls Short

Jira stores every worklog your team ever submits. Time spent, date, author, comment — it's all in there. The frustrating part is that most of this data is surprisingly hard to surface through the UI.

If you've ever tried to answer "how much time did the team log last week, grouped by user?" using native Jira tools, you've probably ended up in one of these places:

  • An Issue Navigator export that gives you issue-level time, not user-level totals
  • A spreadsheet with =SUMIF formulas pulling from a CSV dump
  • A dashboard gadget that kind of shows what you need but not quite

This post covers what JQL can and can't do with worklog data, some queries worth having in your toolkit, and where the Jira REST API becomes a better approach than JQL for anything more complex.


What JQL actually lets you filter on for worklogs

JQL has four worklog-related fields you can use in queries:

worklogAuthor
worklogComment
worklogDate
worklogDate >= "YYYY-MM-DD"
Enter fullscreen mode Exit fullscreen mode

That's it. No worklogHours, no totalTimeSpent filter, no grouping by user. JQL can find which issues a person logged time on, within a date range — but it can't sum that time, filter by amount, or aggregate across users.

Here are the queries I find myself writing most often:

All issues where a specific user logged time this month:

worklogAuthor = "user@company.com" AND worklogDate >= startOfMonth()
Enter fullscreen mode Exit fullscreen mode

All issues where anyone on the team logged time this sprint:

worklogAuthor in membersOf("team-name") AND worklogDate >= -14d AND sprint in openSprints()
Enter fullscreen mode Exit fullscreen mode

Issues logged by the current user that are still open:

worklogAuthor = currentUser() AND worklogDate >= -30d AND statusCategory != Done
Enter fullscreen mode Exit fullscreen mode

Issues with worklogs containing a specific keyword (useful for billing codes):

worklogComment ~ "billable" AND worklogDate >= "2026-04-01"
Enter fullscreen mode Exit fullscreen mode

All of these return issue lists, not time summaries. To get total hours from the results, you export to CSV and sum the Time Spent column yourself.


The gap: what JQL won't tell you

A few things you'd expect to be queryable that aren't:

You can't filter by amount of time logged. There's no worklogHours > 2 equivalent in JQL. You can find issues where a user logged work, but you can't filter to only issues where they logged more than a threshold.

You can't group by user. JQL returns a flat list of issues. If three people logged time on the same issue, you get that issue once — not three rows, one per author.

You can't query worklogs directly. JQL queries issues, not worklog entries. If a user logged two separate entries on the same issue on different days, you can't distinguish those entries via JQL — the issue just shows the total Time Spent field.

timespent is queryable but limited. You can write timespent > 2h to find issues where more than 2 hours have been logged total, but this is total across all users, not per-user.

timespent > 7200  // time is in seconds in JQL
project = MYPROJ AND timespent > 7200
Enter fullscreen mode Exit fullscreen mode

Where the REST API does what JQL can't

If you need per-user time summaries, the Jira REST API is the right tool. Two endpoints are worth knowing:

Get worklogs for a specific issue:

GET /rest/api/3/issue/{issueIdOrKey}/worklog
Enter fullscreen mode Exit fullscreen mode

Returns all worklog entries for that issue, including author, timeSpentSeconds, started date, and comment. This is the granular data JQL never surfaces.

Search issues and get their worklogs:
The practical pattern is two-step: use JQL via the search endpoint to get a list of issues, then fetch worklogs per issue.

// Step 1: get issues with worklogs in the date range
const searchResponse = await fetch(
  `${baseUrl}/rest/api/3/search?jql=worklogAuthor="${userEmail}" AND worklogDate>="${startDate}"&fields=summary,timespent`,
  { headers: { Authorization: `Basic ${btoa(`${email}:${apiToken}`)}` } }
);

const { issues } = await searchResponse.json();

// Step 2: get worklogs for each issue
const worklogs = await Promise.all(
  issues.map(issue =>
    fetch(`${baseUrl}/rest/api/3/issue/${issue.key}/worklog`, {
      headers: { Authorization: `Basic ${btoa(`${email}:${apiToken}`)}` }
    }).then(r => r.json())
  )
);

// Step 3: filter by author and date, sum time
const userWorklogs = worklogs
  .flatMap(w => w.worklogs)
  .filter(w => 
    w.author.emailAddress === userEmail &&
    w.started >= startDate
  );

const totalSeconds = userWorklogs.reduce((sum, w) => sum + w.timeSpentSeconds, 0);
const totalHours = (totalSeconds / 3600).toFixed(1);
Enter fullscreen mode Exit fullscreen mode

This gives you what JQL can't: actual per-user time totals, filterable by date range, groupable however you need.

One gotcha: the /worklog endpoint paginates at 20 entries by default. For issues with a lot of logged time, you'll need to handle the startAt parameter:

async function getAllWorklogs(issueKey, baseUrl, auth) {
  let allWorklogs = [];
  let startAt = 0;
  const maxResults = 100;

  while (true) {
    const response = await fetch(
      `${baseUrl}/rest/api/3/issue/${issueKey}/worklog?startAt=${startAt}&maxResults=${maxResults}`,
      { headers: { Authorization: auth } }
    );
    const data = await response.json();
    allWorklogs = [...allWorklogs, ...data.worklogs];

    if (startAt + maxResults >= data.total) break;
    startAt += maxResults;
  }

  return allWorklogs;
}
Enter fullscreen mode Exit fullscreen mode

The updated worklogs endpoint (often overlooked)

There's a less-known endpoint that's useful for syncing or auditing worklog data:

GET /rest/api/3/worklog/updated?since={timestamp}
Enter fullscreen mode Exit fullscreen mode

Returns all worklog IDs updated since a given Unix timestamp in milliseconds. Useful if you're building a sync process and don't want to re-fetch everything — just poll for updates since your last sync.

const since = Date.now() - (7 * 24 * 60 * 60 * 1000); // last 7 days
const response = await fetch(
  `${baseUrl}/rest/api/3/worklog/updated?since=${since}`,
  { headers: { Authorization: auth } }
);

const { values, lastPage, nextPage } = await response.json();
// values = array of { worklogId, updatedTime }
Enter fullscreen mode Exit fullscreen mode

Then fetch the actual worklog details in bulk:

POST /rest/api/3/worklog/list
body: { "ids": [worklogId1, worklogId2, ...] }
Enter fullscreen mode Exit fullscreen mode

This is significantly more efficient than querying per-issue if you're maintaining any kind of time tracking integration.


When to use what

Goal Use
Find issues where someone logged time JQL with worklogAuthor
Filter open issues by who's working on them JQL with worklogAuthor + status filter
Get total hours per user REST API /issue/{key}/worklog
Build a timesheet view REST API, aggregate by author + started date
Sync worklog data incrementally REST API /worklog/updated + /worklog/list
Show time trends across sprints Reporting add-on (JQL and API both get complex fast)

For the last row — if you need ongoing time reporting inside Jira without building and maintaining your own API integration, Report Hub handles the aggregation layer. It's a Forge app so the data stays inside Atlassian's infrastructure, which matters if you're in a regulated environment. The full breakdown of what's possible with Jira time tracking reports — including the JQL workarounds and their limits — is on the Grandia Solutions blog if you want more depth on the reporting side.


Tags

#jira #atlassian #javascript #productivity


Originally written by the team at Grandia Solutions — Atlassian Solution Partner specialising in Jira administration and Marketplace app development.

Top comments (0)