A team I worked with closed a sprint at 47 story points, its best in a year. The same two weeks, git showed eleven merged pull requests, three reverts, and a main branch that was red for a day and a half. Both descriptions were accurate. Only one of them described what customers received.
That gap is the problem with story points and ticket status as measures of engineering progress. They are not wrong, exactly. They are answering a different question than the one leadership is asking. Points measure what the team predicted; ticket status measures what someone remembered to update. Neither one observes the work. This post is about what does: the signals that come from the code, the review, and the pipeline, and how to read them instead. It is the companion to why start measuring engineering metrics today (the case for measuring) and improve productivity with just these 5 engineering metrics (the precise definitions). Here the question is narrower: if not points and tickets, then what?
What story points and ticket status are actually for
I want to be fair to both, because they are useful for the thing they were designed for.
Story points are a planning tool. They let a team compare the relative size of work before doing it, and over time they give a rough capacity number so that sprint commitments are not fantasy. Used inside the team, for planning, they are fine. We have written about their good side in the power of sprint velocity.
Ticket status is a coordination tool. It tells the team, and the people waiting on the team, roughly where a piece of work is. "In review" is a helpful thing for a product manager to see.
The trouble starts when either one leaves the team and becomes a measure. Three things go wrong immediately.
Points are estimates, and estimates are made by the people being measured. The moment velocity becomes a number that goes up in a slide deck, it goes up. Not through fraud; through a hundred small recalibrations that nobody could point to. A team's velocity is not comparable to another team's, not comparable to its own velocity from a year ago, and not convertible into anything a customer experienced.
Ticket status is a manual field with no timestamp discipline. A ticket moved to "done" on Friday might have been done Tuesday. A ticket in "in progress" might have been abandoned two weeks ago. The data is only as good as the habit, and the habit decays under pressure, which is exactly when you most want the data.
Neither one maps to the unit of engineering work. A ticket can be one PR, five PRs, or half of one. A story can be "done" with its feature flag off, its tests skipped, and a follow-up ticket for the hard part. The map from business tasks to engineering changes is many-to-many, and status on one side does not tell you state on the other.
Observe the work, don't ask about it
The alternative is not a better ticket workflow. It is to measure from systems that record events as a side effect of doing the work, with timestamps nobody has to remember to set. That means git, the code host, CI, and deploys. Every commit, PR, review, run, and release is already logged. The measure is derived from behavior, not reported by the person being measured, and that one property fixes most of the problems above.
Here is what to measure instead, grouped by the question each group answers.
Flow: how long does a change take, and where does it wait?
The first replacement for velocity is cycle time: the elapsed time from first commit to production, broken into its stages (coding, review pickup, review, deploy). It answers the question velocity was pretending to answer ("how fast are we?") with an observed elapsed time rather than a predicted size.
What the example team at the top would have seen: the eleven PRs had a median cycle time of six days, and four of those days were review pickup. The 47 points said "record sprint." The flow data said "review is the bottleneck, and it got worse this sprint because two reviewers were on the big refactor." Only one of those is actionable.
Alongside cycle time, watch PR size (lines changed, or files touched) and work in progress (open PRs per engineer). Large PRs and high WIP are the two most reliable predictors of long cycle time, and both are visible in git without anyone estimating anything. A team whose median PR is 800 lines is not going to have a fast review stage no matter what its velocity chart says.
Signal: can we trust what the pipeline tells us?
This is the group that ticket-based measurement is completely blind to, and it is the one I would add first.
Main-branch pass rate: what fraction of CI runs on the main branch pass. If main is red a quarter of the time, every "done" ticket during those windows shipped on top of an unknown state.
Flaky failure rate: the fraction of test failures that are not caused by the change under test. This number quietly sets the ceiling on every other metric. A suite that fails one run in eight for no reason adds reruns to deploy time, makes engineers stop reading failures, and turns "tests passed" into a coin flip that happened to land well. Measure it per test and in aggregate.
Time to green: how long from a PR's last push until CI is green, including reruns. This is the honest version of "CI takes twelve minutes." When reruns are common, time to green is often two or three times the nominal pipeline duration.
Ticket status has no column for any of this. A story is "done" whether it merged on a clean run or on its fourth attempt. We wrote about how badly a flaky suite distorts the stability metrics leadership actually looks at in change failure rate is lying to you if your tests are flaky.
Outcome: did the change work?
The last group replaces the "done" column with what done actually means.
Deploy frequency: how often the team ships to production. It is a proxy for batch size and confidence, and it comes straight from your deploy tooling.
Change failure rate: the fraction of deploys that needed a hotfix, rollback, or revert. The three reverts in the opening example were invisible to the sprint report. In git they are three commits with "Revert" in the subject line, each of which is a deploy that did not work. Measuring it well is harder than it looks; change failure rate: the DORA metric everyone quotes and nobody measures right covers the definitional traps.
Coverage on the diff: not total coverage, which barely moves, but whether the lines that changed in this PR were exercised by a test. It is the closest thing to a mechanical check that "done" included "tested."
Making the data observable: an example
Most of this data already exists, but two pieces often need a small nudge: deploys need to be recorded as events, and test results need to be kept rather than discarded with the CI log. Here is a GitHub Actions workflow that does both: it uploads the JUnit report from every run (so flaky rate and time to green can be computed later) and records a deployment when the job ships.
name: ci
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test -- --reporters=default --reporters=jest-junit
env:
JEST_JUNIT_OUTPUT_DIR: reports
- name: Keep the test report even when tests fail
if: always()
uses: actions/upload-artifact@v4
with:
name: junit-${{ github.run_id }}-${{ github.run_attempt }}
path: reports/
deploy:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- run: ./scripts/deploy.sh production
- name: Record the deployment as an event
run: |
gh api repos/${{ github.repository }}/deployments \
-f ref=${{ github.sha }} -f environment=production \
-f description="run ${{ github.run_id }} attempt ${{ github.run_attempt }}"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Two details in there matter more than they look. The if: always() on the upload step means failed runs keep their reports, which is the only way to find out later which failures were flaky. And the run_attempt in the artifact name and the deployment description means reruns are distinguishable from first tries, so time to green can be computed honestly.
Once the events exist, the rest is arithmetic. Deploy frequency is a count of deployment events per week. Change failure rate is the fraction of them followed by a revert or hotfix deploy within a window. Flaky rate is the fraction of test failures where the same test passed on a rerun of the same commit.
What this looks like in practice
The shift is less dramatic than it sounds. Nobody has to stop using points for planning, and nobody has to stop moving tickets. What changes is what gets reported upward and what gets discussed in retrospectives.
A weekly review that used to say "we did 47 points, up from 41" says instead: "median cycle time 3.1 days, down from 4.4; pickup time is now the largest stage; main was green 94% of the week; two flaky tests accounted for 70% of the reruns; four deploys, zero reverts." Every one of those sentences comes from a system, has a timestamp, and points at a specific thing to do next.
That last property is the real reason to switch. A velocity number that dropped generates a debate about estimation. A pickup time that rose generates a question about who is reviewing. One of those conversations ends with a change.
If you want more on which of the git-derived signals are worth attention, git metrics that actually tell you something useful is a good next read. And this is the problem our engineering metrics product is built around: it reads the PR, review, CI, and test events already in your repos, computes the flow, signal, and outcome numbers per team, and separates flaky test failures from real ones so the signal group is not lying to you. The point is not another dashboard. It is that the numbers come from what happened rather than from what someone estimated or remembered.
FAQ
Should we stop estimating in story points?
Not necessarily. Points are a reasonable planning tool inside a team. The change is to stop treating velocity as a performance measure or reporting it outside the team. Plan with points if they help; measure with observed flow and outcome data.
Doesn't ticket status still matter for stakeholders?
It matters for coordination, and it should stay. What it should not be is the source of truth for progress. A stakeholder who sees "in review" on a ticket and a pickup-time metric showing review takes four days has more useful information than one who sees the ticket alone. Use the ticket for where, and the observed data for how long and how well.
How do I connect PR-level metrics back to business work?
Loosely, and on purpose. Most teams reference the ticket in the branch name or PR title, which is enough to roll up cycle time by epic or project when someone asks. Do not force a strict one-ticket-one-PR rule to make the join clean; that rule tends to produce either giant PRs or fake tickets, both of which make the underlying metrics worse.



