If you’re extracting data from a scrum board then at some point, you’ll need to extract sprint data, which is stored in two different places, inconsistently.

Your first contact with details on sprints is likely to be in the issue history. In here, there is information about what sprints this issue is in.

What’s that you say? “How is it possible for an issue to be in multiple sprints?” If it doesn’t finish in one sprint and that sprint ends, its still considered in that one forever. Then you can add it to another sprint and now it’s in two. Repeat as often as you like for a never-ending set of sprints for a single issue.

I’ve seen issues that have been in over a dozen sprints because they never get finished and just keep carrying over from one sprint to the next. Yes, that’s completely dysfunctional and should never happen. Yet, it happens all the time.

The entry in the history might look like this.

{
  "field": "Sprint",
  "fieldtype": "custom",
  "fieldId": "customfield_10020",
  "from": "76, 79",
  "fromString": "Sprint 12, Sprint 13",
  "to": "76, 80",
  "toString": "Sprint 12, Sprint 14"
}

Field ID

The first thing you might notice is that there is a fieldId, which most history items don’t have. This is a reference to the custom field (in the same document) that contains more information about the sprints that are referenced. That custom field may or may not exist. If it does exist, it will list information about one or more sprints and the sprint that you care about may or not be in that list.

Got it so far?

If it does all exist, it will look something like this, and you’ll see that it contains some useful information about the sprint such as start date, end date (which isn’t what you think it is), and completed date.

You might think that endDate is when the sprint actually completed but no, that’s completedDate. endDate is the date that you anticipated closing the sprint at the time you started it. Why are there two? I assume it made sense at one point.

"customfield_10020": [
  {
    "id": 10,
    "name": "Scrum Sprint 10",
    "state": "closed",
    "boardId": 2,
    "goal": "",
    "startDate": "2025-03-12T01:36:46.600Z",
    "endDate": "2025-03-23T17:32:42.000Z",
    "completeDate": "2025-07-13T16:58:25.335Z"
  },
  {
    "id": 43,
    "name": "Scrum Sprint 11",
    "state": "closed",
    "boardId": 2,
    "goal": "",
    "startDate": "2025-07-13T17:37:23.922Z",
    "endDate": "2025-07-25T09:33:21.000Z",
    "completeDate": "2026-01-22T16:02:01.486Z"
  },

Multiple sprints at once

The next thing you might notice is that unlike every other history type, the from and to values allow for multiple values.

{
  "field": "Sprint",
  "fieldtype": "custom",
  "fieldId": "customfield_10020",
  "from": "76, 79",
  "fromString": "Sprint 12, Sprint 13",
  "to": "76, 80",
  "toString": "Sprint 12, Sprint 14"
}

You might think that this history entry is telling you which sprints this item has been added to or removed from and in a roundabout way, it does.

You can see in this sample that this issue was in sprints 76 and 77 and is now in sprints 76 and 80. It’s always talking about the current state so we can infer from this that…

  • It used to be in 76 and still is, so nothing changed
  • It used to be in 79 and now it isn’t, so it’s been removed.
  • It used to not be in 80 and now it is, so it’s been added.

When you’re parsing those, it’s pretty easy to parse the from and to fields because they’re just numbers separated by commas.

The fromString and toString fields are harder though because it’s all concatenated text, and since commas are allowed in sprint names, you can end up with commas in the middle of a name.

“Surely they would escape the commas in the text so it can be easily parsed”, you say! They do not.

If you want unambiguous names, you need to parse the id and use that to index into the data in the custom field above. Of course, we’ve already learned that the sprint you want may or may not be there.

If it isn’t there then we have a separate API to get sprints, which is great. Except that it may not be there either. We’ll get there in a moment.

Remove before add

There’s a lovely gotcha where an issue can be removed from a sprint before it was ever added. This happens when the issue gets created within the sprint. Jira considers it to be in that sprint but there is no record of it being added in the history. Then if it’s removed from that sprint, you’ll see the removal all by itself.

The sprint API

This API will return all (mostly) of the sprints for the current board. /rest/agile/1.0/board/{boardId}/sprint

{
  "maxResults": 50,
  "startAt": 0,
  "total": 1,
  "isLast": true,
  "values": [
    {
      "id": 1,
      "self": "https://improvingflow.atlassian.net/rest/agile/1.0/sprint/1",
      "state": "closed",
      "name": "Scrum Sprint 1",
      "startDate": "2022-03-26T16:04:09.679Z",
      "endDate": "2022-04-09T16:04:00.000Z",
      "completeDate": "2022-04-10T22:17:29.972Z",
      "createdDate": "2022-03-26T16:03:49.814Z",
      "originBoardId": 2,
      "goal": ""
    }
  ]
}

I’m sure you’ve picked up by now that even this API doesn’t return all the sprints, although it often returns more here than we have in the custom field.

In my testing so far, it seems that it does return any sprint that actually has an issue in it. Any sprint that’s empty will not be returned, which is annoying.

Conclusion

So in conclusion, like all the other Jira API’s, there is useful data in here but its inconsistent, incomplete, and poorly thought out.


Other articles about the Jira API:

See also the JiraMetrics tool, which is the reason I’ve had to learn the idiosyncrasies of the Jira API. If you just want access to the data then let JiraMetrics do it for you.