How to write steps to reproduce that anyone can follow
Steps to reproduce are the part of a bug report that decides whether it gets fixed this week or bounces for a month. Write them so a stranger gets the same bug on the first try, and most of the back and forth disappears.
By the VeoRec team · · 11 min read
In short
Good steps to reproduce start from a state anyone can reach, use one action per numbered step with the exact data and labels, and end on the step where the bug shows. Before filing, cut every step that is not needed to trigger the bug, then state the environment that matters and how often it happens. For intermittent bugs, count attempts, keep timestamps and vary one condition at a time.
- Write the starting state as step zero: who you are signed in as, on what data, with which flags.
- One action per step, with the exact text you typed and the exact label you clicked.
- Cut steps until removing any one of them makes the bug disappear; that is your minimal repro.
- Say how often it happens in numbers ("4 of 20") and keep the time of each failure.
- For regressions, find the last version that worked; it narrows the search more than any other clue.
- Follow your own steps once from scratch before you file; it catches the step you did without noticing.
Steps to reproduce are a recipe for making a bug happen on purpose. When they work, a developer reads them, follows them, sees the bug and starts fixing it. When they do not, the ticket gets a "cannot reproduce" label and a polite question, and everyone loses a day. Mozilla's bug writing guidelines call them the most important part of a bug report, and anyone who has spent time triaging tickets would agree.
This guide covers how to write them so they survive contact with a stranger: where to start, how to phrase each step, how to cut a long sequence down to the few steps that matter, and what to do when the bug only shows up some of the time.
Pass the stranger test with your steps to reproduce
The test is simple. Could someone who has never seen this part of the product, sitting at a different computer, follow your steps and see the bug on the first try? If the answer depends on something only you know, the steps are not done.
When steps fail that test, look for one of four gaps:
- A hidden starting point. You were already signed in, with items in the cart, on an account with a feature flag. None of that is in the steps.
- Vague actions. "Fill in the form" hides the one value that triggers the bug.
- Missing environment. The bug only happens in Safari, or for admins, or on accounts created before last year.
- Unstated frequency. It happens one time in five, so the developer tries twice, sees nothing, and closes the ticket.
Each of the sections below closes one of those gaps. If you want the wider picture of what else goes in a report (title, expected and actual results, severity), see how to write a bug report.
Start from a state anyone can reach
Write the starting state before step one, as its own line. It is the most often skipped part of any repro, because to you it is invisible: you have been staring at that screen for an hour.
A good starting state answers four questions. Who are you signed in as? What data exists? Where are you in the product? What is switched on? For example: "Signed in as an admin of a workspace on the Business plan, with at least one saved billing address, new checkout flag on, starting from Settings."
If the starting state takes setup, write the setup as steps or link to the record. "Use the test workspace qa-billing-03" is better than a description of how it got into its current shape. If the bug only happens on one customer's real data, say so, and say which part of their data seems to matter: "only on invoices with more than 100 lines" turns a customer-specific bug into one anyone can recreate with a test account.
Write one action per step, with exact inputs
Each numbered step should be one thing a person does: click, type, select, press, wait, scroll. Two actions in one step invite the reader to do them in a different order or skip one. Some rules that make steps precise without making them long:
- Use the label on the screen. Write "Click Save changes", not "click the button". If two buttons share a label, say where it is: "the Save button in the Billing section".
- Give the exact input. "Type
anna@examplein Email" is reproducible. "Enter an invalid email" is not, because there are a thousand invalid emails and only some of them trigger the bug. - Say how you did it when it might matter. Pressing Enter and clicking the button can run different code. So can pasting and typing, or keyboard and mouse.
- Include waits. "Wait for the spinner to stop" or "click again immediately, before the page responds" are real steps for timing bugs.
- End on the observation. The last step is where the bug becomes visible, followed by separate lines for the expected and the actual result.
Toggle between a vague set of steps and the same steps with those rules applied. Same bug, very different chance of being reproduced on the first try:
Notice that the precise version found something the vague one hid: the bug needs a slow network. The reporter only noticed because writing exact steps made them try it again carefully. That happens a lot. Writing steps properly is not just documentation; it is the first round of debugging.
Cut the steps down to a minimal reproduction
Your first set of steps is usually the path you happened to take: twelve steps through three pages, with a detour into settings. Most of them have nothing to do with the bug. A minimal reproduction is the shortest set of steps (and the smallest data) that still triggers it, and it is often worth more than any other detail you can give.
Browser teams have done this for years. Mozilla's guidelines ask anyone reporting a bug on a specific web page to make a reduced testcase, and WebKit publishes a whole guide to test case reduction for web pages. The method works just as well for click paths:
- Confirm the full version reproduces Run your original steps once more from the starting state. If they do not reproduce reliably, fix that first; reducing a flaky repro wastes time.
- Remove one step, or a block of steps Start with the ones that look least related: the detour to settings, the second product in the cart, the profile picture upload.
- Run it again If the bug still happens, the removed step was not needed. Leave it out. If the bug disappears, put the step back: it matters, and that is a clue worth noting.
- Shrink the data the same way Fewer cart items, a shorter name, one invoice line instead of a hundred. Stop when making the data any smaller makes the bug go away.
- Write down what you learned The steps you could not remove are the interesting ones. "Only happens if a coupon is applied before the address is changed" is half a diagnosis.
A worked example. A tester finds that an order confirmation email shows the wrong delivery date. Her original path has twelve steps: sign in, browse a category, add two products, open the cart, apply a coupon, change the shipping address to another country, pick express delivery, go back and remove a product, check out with a saved card, and open the email. She starts cutting. Without the coupon: still wrong. With one product instead of two: still wrong. Without changing the address: the date is correct. Put the address change back, and use standard delivery instead of express: still wrong.
Twenty minutes later the report reads: "Starting state: signed in, one item in the cart, shipping address in the UK. 1. Change the shipping address to an address in Germany. 2. Check out. Expected: delivery date based on the German delivery estimate. Actual: the email shows the UK estimate." Three lines instead of twelve, and the developer goes straight to the code that recalculates delivery dates when the country changes. Most of the debugging happened before the ticket was filed.
For developers reporting bugs in a library or framework, the same idea applies to code. React's bug report template says it directly: the bug gets fixed faster if they can run your code and it has no dependencies other than React. A twenty line example in a fresh project beats a link to your whole application.
Pin down the environment that makes it happen
Every bug happens in an environment. The question is which parts of it matter, and the fastest way to find out is to change one thing at a time. You do not have to test every combination. Test the cheap ones, and write down the results whichever way they go.
| Variable | Quick check | What to write |
|---|---|---|
| Browser | Try a second browser | "Chrome 129: yes. Firefox 131: no." |
| Extensions, cache, stored settings | Repeat in a private window | "Also in a private window" or "Not in a private window" |
| Account and role | Try a non-admin test account | "Admins only" or "All roles" |
| Data | Try a fresh record | "Only on invoices with 100+ lines" |
| Window size | Resize the window or use device mode | "Below 768 px wide" |
| Network speed | Throttle the network in DevTools | "Only on Slow 4G throttling" |
| Language and time zone | Switch the account locale | "Only with German locale" or "only after 23:00 UTC" |
| Build | Check the latest build or staging | "Still happens on build 2026.10.1" |
Negative results are as valuable as positive ones. "Does not happen in Firefox" tells the developer to look at browser-specific code. "Happens for every role" tells them not to bother with permissions. A short line of results like this often does more to locate a bug than a paragraph of theory.
Reproduce bugs that only happen sometimes
Intermittent bugs are where careful steps matter most, because the developer cannot tell a fix from luck without them. A few habits make them much more tractable:
- Count. Run the steps a fixed number of times and report the ratio: "4 of 20". Now everyone knows that two clean runs prove nothing.
- Keep timestamps. Note the time of each failure, with the time zone. Developers can find those exact requests in the logs and compare them with the ones that worked.
- Look for timing. Many intermittent bugs are races: two requests finishing in an unexpected order, a click landing before the page is ready. Throttle the network or the CPU in DevTools and see whether the ratio changes. If slowing things down makes it happen every time, you have turned a flaky bug into a reliable one.
- Vary one thing. Fast versus slow clicks, tab in front versus background, page fresh versus open for an hour. Change one at a time and keep counting.
- Record the session. Start a screen recording, repeat the steps until it fails, and mark the moment. The passing attempts next to the failing one often show the difference.
Simon Tatham's essay on reporting bugs has the right attitude for these: try it two or three times to see how often it fails, try another machine if you can, and report what you find. An honest "happened once, could not repeat in 20 tries" is a useful report. A guessed "always" is not.
For the recording part, VeoRec's chapter markers help: press M the moment the failure appears and the marker becomes a chapter you name when you finish, so the developer can jump straight to the failing attempt in a long session. More on that approach in video bug reports.
Adapt the steps for phones, APIs and delayed effects
Not every bug lives in a desktop browser tab, and the same principles need a slightly different shape elsewhere.
Mobile web. How the page was opened is part of the starting state. A link tapped in an email or chat app may open in an in-app browser rather than the phone's main browser, and the two can behave differently. Write "Tap the reset link in the Gmail app on an iPhone 15, iOS 18" rather than "open the link on mobile", and say which orientation you were in when the bug appeared.
APIs. The best steps for an API bug are a single request someone can run: the method, the endpoint, the headers that matter and the body, written as a curl command, followed by the response you got. Replace tokens with a placeholder like <YOUR_TOKEN> before you paste it anywhere. One runnable request beats a paragraph describing what the frontend sent.
Delayed effects. Some bugs appear minutes or hours after the action: a reminder email with the wrong date, a nightly job that skips a record, a session that expires early. Write the wait as a step ("wait 15 minutes for the reminder email") and give the exact times of the action and the effect. If your team has a way to trigger the job or move the clock forward on staging, mention it, because nobody wants to wait overnight to test a fix.
Find out when it started
If something used to work, the most useful fact you can add is the last version where it did. "Worked in 2026.09.4, broken in 2026.10.1" turns a search through the whole codebase into a search through one release.
Testers can often find this without touching code: check an older staging build, a preview deployment of a previous release, or simply remember when it was last seen working ("the September invoices exported fine"). Developers can go further. git bisect does a binary search through the commit history: you mark one commit as good and one as bad, it checks out the commit halfway between, you test and mark it, and it repeats until it finds the first bad commit. With a script that checks for the bug, git bisect run does the whole search on its own.
Even if you cannot find the exact version, a rough date helps. "Started some time in the last two weeks" still rules out most of the history.
Turn the steps into a script you can replay
For web apps, the steps themselves can be recorded and replayed. Chrome DevTools has a Recorder panel that captures clicks, typing and navigation as a list of steps. You can edit the steps, replay them at normal speed or slowed down, and, according to the Recorder reference, export them as JSON or as a Puppeteer script that a developer can run or turn into an automated test. This short video from the Chrome team shows how it works:
Record and replay user flow with the Recorder panel #DevToolsTips (video, Chrome for Developers)
A replayable script is the most precise form steps to reproduce can take, and it doubles as a regression test once the bug is fixed. Two cautions. The exported file contains whatever you typed, so record with a test account and fake data, never a real password. And keep the numbered steps in the ticket as well: not everyone reading it will want to run a script to understand the bug.
Show the steps as well as writing them Record the tab while you follow your own steps. Each click is highlighted, so the developer can match the video to the numbered list. See the screen recorder for QA
Check your steps before you file
The last step is the one most people skip: follow your own steps from the starting state, exactly as written, as if you were the developer. Do not fill gaps from memory. If you catch yourself doing something that is not on the list (closing a banner, picking a product), add it. If the bug does not happen, the steps are wrong, and it is far better that you find out than the developer.
A quick way to do this check and produce evidence at the same time is to record it. With VeoRec, clicks are highlighted in the video, so you and the developer can line each click up against a numbered step. Then test your instincts on a few common situations:
What to do next
Open the bug you are working on and write its starting state as a separate line, if it is not already there. Then run the cut-down routine once: remove steps until the bug stops, put the last one back, and you have a minimal repro. Add the environment checks you did, including the negative ones, and the frequency as a ratio.
If your team writes a lot of reports, put this structure into the form people fill in. The bug report templates post has versions with a starting state field and separate templates for intermittent, performance and visual bugs.
Frequently asked questions
What are steps to reproduce in a bug report?
They are the numbered actions that make a bug happen, starting from a stated state such as a signed-in account on a particular page. Anyone following them should see the same problem. They are usually followed by the expected result and the actual result.
How many steps to reproduce should a bug report have?
As few as still trigger the bug. Many web app bugs need three to eight steps once unrelated actions are removed. If you have more than fifteen, try cutting steps one at a time until the bug stops, then restore the last one.
What is a minimal reproducible example?
It is the smallest set of steps, data or code that still shows the bug. Everything that is not needed to trigger it has been removed, so the developer can see the cause quickly. For code, that usually means a short standalone example with no unrelated dependencies.
How do I write steps to reproduce for an intermittent bug?
Write the steps that sometimes trigger it, then report how often they do as a ratio, such as 4 of 20 attempts. Note the time of each failure, describe anything that differed between failing and passing attempts, and try throttling the network or CPU to see whether timing makes it more frequent.
What should I do if a developer cannot reproduce my bug?
Follow your own steps again from a clean starting state and add anything you did that is missing. Compare environments: browser version, account role, data, flags and locale. A short screen recording of you following the steps often reveals the missing detail on its own.