Flow Metrics in Python: The 3 Scripts Jira Won't Give You
Key Takeaways
- Your board shows current state, not elapsed state. The Jira changelog holds every status transition with a timestamp, and thirty lines of Python turns that into a queue diagnosis.
- Verification is the new bottleneck. Measuring the gap between pull request opened and first human review exposes the constraint that throughput metrics conceal.
- Everything here is read-only. No write scope, no automation acting on your board, no permissions your security team would refuse.
- The scripts start conversations rather than replacing them. Each one produces a question for the Retrospective, not a verdict.
- Some things should stay manual. Writing to the board, auto-generating stakeholder reports, and processing team members' words are all worse when automated.
There are three questions about your team's flow that your Jira board cannot answer. Not will not, cannot. The data is in the API but never rendered on the screen.
If you have read our central guide to agentic-era agile metrics, you already know that output volume has become the least reliable signal in modern delivery. This article is the practical companion: the actual scripts that produce the evidence those arguments depend on.
None of this requires you to become an engineer. It requires requests, pandas, and enough patience to read an API doc once.
Watch the Walkthrough
The full walkthrough, including the demo output for each script and the segment on what you should not automate.
Why Your Board Cannot Answer These Questions
A Scrum board is a state display. It tells you where every item is at this moment. It is very good at that, and it is the wrong instrument for almost every question worth asking in a Retrospective.
"Where does our work get stuck" is a question about elapsed time in past states. "Is review keeping up with development" is a question about the relationship between two different systems. "What changed this quarter" is a question about trend. The board holds none of these, and the reports built on top of it mostly summarise the same current-state data more attractively.
Permissions Note
Every script below uses read-only access. If someone tells you flow analysis requires write scope on a production Jira instance, they are describing automation, not measurement.
Script 1: Where Work Actually Waits
Jira's changelog records every status transition on an issue with a timestamp. Pull sixty completed items, reconstruct the transitions, and you get the median hours each item spent in each column.
| Status | Median hours | Share of elapsed time |
|---|---|---|
| To Do | 7.2 | 7% |
| In Progress | 12.7 | 12% |
| In Review | 32.4 | 30% |
| Ready for QA | 54.0 | 51% |
Look at the shape rather than the numbers. This team is not slow at building. It is slow at confirming that what was built is correct. Roughly four-fifths of an item's life is spent after the code exists, waiting for a human to look at it.
The script does not tell you why. Reviewers may be overloaded, items may be too large, QA may be a separate team with its own queue. Diagnosing that is your job, and it is the conversation this chart is meant to start.
import requests, pandas as pd
from datetime import datetime
AUTH = (EMAIL, API_TOKEN)
JQL = 'project = SDI AND statusCategory = Done ORDER BY resolved DESC'
r = requests.get(
f"{BASE_URL}/rest/api/3/search",
params={"jql": JQL, "maxResults": 60, "expand": "changelog"},
auth=AUTH,
)
rows = []
for issue in r.json()["issues"]:
# The changelog only records transitions, so the opening state has no
# start event. Seed it from the issue creation timestamp.
created = datetime.fromisoformat(issue["fields"]["created"][:19])
events = [(created, "To Do")] + sorted(
(datetime.fromisoformat(h["created"][:19]), item["toString"])
for h in issue["changelog"]["histories"]
for item in h["items"] if item["field"] == "status"
)
for (t1, status), (t2, _) in zip(events, events[1:]):
rows.append({"key": issue["key"], "status": status,
"hours": (t2 - t1).total_seconds() / 3600})
df = pd.DataFrame(rows)
df = df[df.status != "Done"] # Done is terminal, not a queue
print(df.groupby("status")["hours"].median().sort_values(ascending=False).round(1))Thirty lines including imports. Runs in about four seconds against a sixty-issue query.
Console output
status
Ready for QA 54.0
In Review 32.4
In Progress 12.7
To Do 7.2
Name: hours, dtype: float64Four lines that reorder your entire Retrospective agenda.
For the underlying calculation and its variations, our guide on how to calculate cycle time in Jira covers the definitional choices this script quietly makes for you.
Script 2: The Verification Bottleneck
This is the one that matters most right now. When your team adopted AI coding assistants, producing code got faster and confirming it did not. The constraint moved, and throughput metrics did not notice.
Three numbers from the same team's pull request history make the shift visible: median time to first human review, median time to merge, and average diff size across the quarter.
| Measure | Quarter start | Quarter end |
|---|---|---|
| Median hours to first review | 12 | 24 |
| 90th percentile hours to first review | 41 | 96 |
| Average diff size (lines added) | 140 | 410 |
The third row explains the first two. Diffs got larger, which is exactly what happens when generation becomes cheap, and larger diffs get picked up more slowly because nobody opens a four-hundred-line review at the end of the day.
This is a working agreements conversation, which is squarely your territory. Do you cap pull request size? Rotate a review-first hour? Pair on generated code instead of reviewing it asynchronously?
import requests, pandas as pd
from datetime import datetime
HEADERS = {"Authorization": f"Bearer {GITHUB_TOKEN}"}
REPO = "your-org/your-repo"
prs = requests.get(
f"https://api.github.com/repos/{REPO}/pulls",
params={"state": "closed", "per_page": 100},
headers=HEADERS,
).json()
rows = []
for pr in prs:
if not pr["merged_at"]:
continue
reviews = requests.get(pr["url"] + "/reviews", headers=HEADERS).json()
opened = datetime.fromisoformat(pr["created_at"][:19])
merged = datetime.fromisoformat(pr["merged_at"][:19])
first = min((datetime.fromisoformat(rv["submitted_at"][:19])
for rv in reviews if rv.get("submitted_at")), default=None)
rows.append({
"pr": pr["number"],
"hrs_to_review": (first - opened).total_seconds()/3600 if first else None,
"hrs_to_merge": (merged - opened).total_seconds()/3600,
"size": pr["additions"],
})
df = pd.DataFrame(rows)
print(df.describe(percentiles=[.5, .9]))A read-only token with repo:read scope is sufficient. Public repositories need no token at all.
Console output
hrs_to_review hrs_to_merge size
count 80.0 80.0 80.0
mean 35.0 51.1 293.2
std 40.6 43.5 186.2
min 1.6 8.5 74.0
50% 22.2 35.6 224.0
90% 75.5 99.1 590.0
max 301.0 313.8 974.0The gap between the 50th and 90th percentile is the story. Half of all pull requests get a first look inside a day; one in ten waits more than three. Slice the same frame by date to get the quarter-over-quarter trend above.
PMO Warning
If review latency is rising while velocity climbs, your delivery system is accumulating a queue, not accelerating. Read that alongside AI velocity inflation before you re-baseline any roadmap commitment.
Rising review latency and rising rework usually travel together. Pair this script with the counter-metric described in AI code churn and rework rate, and with the capacity model in measuring blended human and AI throughput.
Script 3: Retrospective Signals, Not Retrospective Summaries
Most Retrospectives open cold, and whoever is most confident sets the agenda. Arriving with three or four observations changes the first ten minutes entirely.
Note the word observations. These are questions the data noticed, not findings. You put them up and ask whether any of it matches what the team experienced. Sometimes the answer is no, which is also useful.
signals = []
reopened = df[df.status_sequence.apply(lambda s: "Done" in s and s[-1] != "Done")]
if len(reopened):
signals.append(f"{len(reopened)} items re-opened after Done: {list(reopened.key)}")
slow = pr_df[pr_df.hrs_to_review > pr_df.hrs_to_merge * 0.6]
if len(slow):
signals.append(f"{len(slow)} PRs spent over 60% of their life awaiting first review")
print("\n".join(f"- {s}" for s in signals))Deliberately simple threshold checks. No model, no clustering, no sentiment analysis.
That simplicity is the point, and it leads directly to the line worth drawing.
The Consent Line
Do not put team members' words through a model without their explicit knowledge and agreement. Not Retrospective comments, not Slack messages, not survey free-text. A Retrospective works because people say the true thing, and they stop saying it the moment they suspect their words are being processed and forwarded. Automate the system data. Leave the human data alone.
Running These Without a Jira Instance or a Repository
If you do not have API access yet, you can still see the output. The companion repository includes a generator that produces synthetic data shaped exactly like real Jira and GitHub API responses, with a fixed random seed so the results are reproducible.
The analysis code does not change. Only the line that fetches the data changes, which also makes this a safe way to demonstrate the idea to a security team before requesting a token.
Three Things You Should Not Automate
Writing to Jira. Auto-transitioning tickets and bulk-updating fields is mostly a native Jira automation rule, so scripting it reinvents a dropdown. More importantly, a board that updates itself stops being a shared team artifact. The friction of a human moving a card is sometimes doing real work.
Automated stakeholder reports. A stakeholder trusts your report because you looked at it. Generate the data automatically and write the interpretation yourself. For which measures belong in that interpretation, see the EBM AI-era KPIs.
Anything that replaces a conversation. If the output means a discussion does not happen, that is avoidance dressed as efficiency. Every script in this article is designed to start one.
So Do Scrum Masters Need to Learn Python?
No. You can be excellent in this accountability without writing a line of it, and anyone telling you otherwise is selling something.
But if you want to inspect flow with evidence instead of opinion, this is roughly two weekends of learning rather than a career pivot. The accountability has not changed. You are still responsible for the team's effectiveness, and no script does that for you. It just means you walk into the Retrospective knowing where the time went.
If the technical side of the role interests you more broadly, our guide to technical Scrum Master skills and responsibilities maps the wider territory.
Frequently Asked Questions (FAQ)
Do Scrum Masters need to learn Python?
No. You can be an excellent Scrum Master without writing a line of code. Python becomes useful in one narrow situation: when you want to inspect flow with evidence rather than opinion and your tooling does not expose the data. That is roughly two weekends of learning, not a career change.
What data does the Jira changelog contain?
The changelog records every field change on an issue with a timestamp, including every status transition. That means you can reconstruct exactly how long each item spent in each column, which the board itself never displays because it only shows current state.
Do I need write access to Jira to run these scripts?
No. Every script here is read-only. Most enterprise security teams will not issue a Scrum Master a token with write scope on a production instance, and they are right not to. Read access to the projects you already work on is sufficient.
Why measure time in each status instead of total cycle time?
Total cycle time tells you an item took nine days. The status breakdown tells you seven of those days were spent waiting for review and QA. The first number starts an argument about estimates. The second points at a specific queue you can actually do something about.
What is code review latency and why does it matter now?
Code review latency is the elapsed time between a pull request being opened and a human submitting the first review. It matters because AI assistants made producing code cheap without making verification cheap, so the constraint has relocated to review. Throughput metrics hide this shift entirely.
Can I run these without a Jira instance or a repository?
Yes. The companion repository includes a generator that produces synthetic data shaped exactly like real Jira and GitHub API responses. The analysis scripts run unchanged against either source, so you can see the output before you request any API access.
Should I automate the Sprint Retrospective?
Automate the system data only. Tickets, commits, timestamps and pipelines are fair game. Do not put team members' words through a model without explicit consent. A Retrospective works because people say the true thing, and they stop doing that when they suspect their words are being processed.
Why not just buy a flow metrics app?
Apps are a reasonable choice when procurement is straightforward and the questions are standard. Scripts win when you need a question the vendor did not anticipate, when you want to join Jira data to Git data, or when you want to inspect something this Sprint rather than next quarter.
How often should a Scrum Master run these?
Once per Sprint, before the Retrospective. A single chart is a talking point. The same chart across six Sprints is a trend, and trends are what actually move leadership, because they survive the objection that one Sprint was unusual.
What should a Scrum Master never automate?
Anything that replaces a conversation rather than starting one. Writing to the board removes the human act that makes it a shared artifact. Auto-generated stakeholder reports remove the interpretation that makes them trustworthy. If the output means a discussion does not happen, that is avoidance, not efficiency.