Zaibex
Workshop Exercise · Five Whys

A timed workshop exercise app where the AI decides when to stop asking why

Written by Shahzaib Shoaib, founder and lead engineer

A self-paced problem-finding exercise for an Australian program helping SMEs adopt AI. One person lists problems against the clock over six rounds, an AI asks up to five whys on each, and the session ends in an emailed analysis built from their own words.

Each session ends in an emailed analysis
Australian SME AI program: A timed workshop exercise app where the AI decides when to stop asking why
6
Timed rounds, 60 to 240 seconds each
5
AI follow-up questions at most per problem round
53
Commits on 21 days of work
Case file07 facts
Client
Australian SME AI program
Our role
Solo build
Industry
Education · Workshops
Region
Australia
Services
Full-stackAI Five Whys
Timeline
9 September to 16 October 2025
Year
2025
0116 October 2025

The 3am question

On the last day of commits, 16 October 2025, I pasted a real transcript into the five whys prompt as an example of what not to do. A participant had written about a 3am wakeup. The model's follow-up was “Why do you think getting less sleep affects your experience…?” Anyone could answer that. It used up one of the participant's five questions and told us nothing new.

That transcript is still in the prompt, next to a checklist for obvious questions, and the temperature was set to 0.4 in the same change. Most of the interesting work on this project looks like that: a small prompt fix made after reading what the model actually asked a real person.

The app is a timed problem-identification exercise for an Australian program helping SMEs adopt AI. I built it alone. One person lists problems against a countdown, then an AI asks why about what they wrote, up to five times.

02The exercise

Six rounds and a fridge

There was nothing to inherit. The first commit, on 9 September 2025, is a blank Create Next App project.

The exercise runs six rounds. The first two are 60-second warm-ups: list the things in your fridge, then list only the green things in your fridge. Then come four problem rounds: what you are least looking forward to in the next seven days (180 seconds), the biggest thing that didn't go your way in the last seven days (240 seconds), problems from past jobs that an outsider wouldn't know about (180 seconds), and a round called The Muse, which asks for the last thing someone complained to you about (180 seconds). That is about 15 minutes of typing against a clock.

While the timer runs on a problem round, the screen rotates short prompts like “Your colleague?” and “Your friend?”. Input behaves differently by round. On the warm-ups Enter adds a new line and Shift+Enter submits, since you are typing a list. On the problem rounds Enter submits.

It is one participant at a time, at their own pace, with no login. The app makes an anonymous session id, writes a participant row to Supabase and keeps progress in localStorage. There are no tables or teams and there is no facilitator screen. The one thing participants share is the leaderboard on the warm-up rounds, which ranks everyone who played in the last 24 hours by how many items they listed.

Laptop showing a 2:14 countdown timer above a typed list of short problems
Fig. 01Each round is a list typed against the clock.
03The decision I'd defend

Ask again, or stop

Until 5 October the five whys chain worked in the simplest possible way. It asked a set number of questions, every time. If the second answer already reached the root of the problem, the participant still got three more.

I changed it to forced function calling. The model gets two tools, generate_next_question and end_five_whys_session, and tool_choice is set to required, so every reply has to be one or the other. The limit of five became a ceiling instead of a count, and the API route keeps its own count and stops at five whatever the model decides.

This is the choice I would defend in a review. Stopping is a judgment call, and I wanted it to arrive as a typed decision the code can branch on, with the hard limit kept in code where the model can't argue with it.

The rest of the stack is plain: Next.js 15, React 19, TypeScript, Zustand and Supabase Postgres. The AI calls are raw fetch requests to an OpenAI-compatible endpoint, with no SDK in between. An OpenAI model writes the questions and the analysis. When OpenAI fails, the request falls back to Grok through OpenRouter. If every provider fails, the participant gets one of five static fallback questions and the round carries on.

The countdown runs in an inline Web Worker, with a setInterval fallback. A worker keeps time better than a main-thread timer, which the browser can throttle.

Five whys thread of connected question cards, the last one marked finished
Fig. 02The model chooses to ask again or stop, and the code caps it at five.
The model kept asking textbook questions like why stress affects sleep, so I gave it a test: is this about the person, or about the world?
From the engineer who built it
04September

53 commits on 21 days

The git log has 53 commits between 9 September and 16 October 2025, all mine, spread over 21 days. The first real version landed on 16 September. After that it is bursty. On 30 September alone there are 14 commits, mostly interface polish and the wording of the rounds.

The worst bug was my own. At some point I renumbered the rounds so their IDs start at 181. The database still had a check constraint that only allowed round numbers 1 to 5, so it refused the new rounds, and the leaderboard requests came back with 406 and 400 errors. The fix was to widen the constraint to 1 to 1000.

The cleanup never finished. There are still checks in the code that assume the old numbering. One route treats any id of 101 or more as a problem round. The mock email route looks for round 3 and up. A query helper calls anything numbered 2 or below a warm-up. Some of them may be harmless. I should have searched for every place that assumed a round number before I changed it.

A smaller one: a participant who submitted nothing got a blank score. The score is a generated column built on Postgres array_length, which returns NULL for an empty array where you would expect 0. The code now falls back to counting the items itself, and the interface guards against the null.

05October

The model kept asking textbook questions

The five whys and analysis rework ran between 1 and 16 October, and the parts worth telling are in the six prompts in the code.

On 9 October I changed the direction of the chain. The prompt told the model to treat the last answer as the new problem, with a line that said “Don't reference the original problem.” The next day I took those lines out and anchored the chain back on the original problem. The current prompt says “Stay connected to the original problem.” That reversal cost a day.

The bigger fix in that 10 October commit was the kind of question the model asked. It kept asking things like “Why does stress affect sleep?” That has a textbook answer, and it tells you nothing about the person who wrote the problem. I added a personal versus factual test, with good and bad examples of each, and a rule to end the session when the next question would be factual. That last rule is my favorite part. Running out of personal questions is a decent sign the chain has gone deep enough.

The same commit removed something I had added earlier, word-count tracking on answers. It was penalizing people who answer briefly.

Six days later came the 3am transcript and the checklist for obvious questions.

Latency was the other problem. The screen now shows a skeleton the moment a question is requested, duplicate calls for the first question are blocked, and each question's 180-second timer starts only once the question is on screen. A slow response no longer eats into the participant's answer time.

06The screens

What a participant sees

You land on an intro screen with a Start button. Each round explains itself with a short animation, counts down 3, 2, 1, and opens a text box under a timer. When a warm-up ends you see the top five for the last 24 hours, with yourself shown as “You” and everyone else as “Participant” and a four-digit number.

After each of the four problem rounds the five whys starts on its own, and the AI asks why about what you wrote. You get 180 seconds per answer. If the time runs out, whatever is in the box is submitted, even if it's empty. A Stop & Continue button ends the chain early, and the model can end it too.

At the end, the completion screen fires a request with keepalive set, so closing the tab doesn't kill it. That request runs the analysis, renders a react-email template and sends it through Amazon SES. The recipient is a fixed address set in the server environment, plus CC addresses from the same place. The participant never types an email address.

The analysis used to do a lot more. An earlier version gave recommendations, pointed out patterns and suggested next steps. On 12 October I cut the recommendations and a section called Who This Affects, because both made assumptions about the person that they hadn't stated. What is left are three sections, starting with The Real Pain, and all of them stick to what the participant actually wrote. The email also carries the program's own advice on choosing which problem to work on first, which is theirs to write.

Leaderboard with five ranked rows, one highlighted as You and the rest shown as Participant
Fig. 03The warm-up leaderboard ranks everyone who played in the last 24 hours.
07Honest status

What I can't tell you yet

I don't have usage numbers I can publish for this one, so there is no before and after here. What I can describe is the state of the code at the last commit.

The leaderboard isn't live. It loads once and polling is turned off. A 15-second refresh interval sits in the config unused, and a subscribeToLeaderboard function for Supabase Realtime exists that nothing calls. Timers run per device with no sync between devices. That suits a self-paced exercise and would not suit a room running in lockstep.

There is no handling for rate limits beyond a tip in the testing doc. The repo has no tests. It also has no login and no admin export of the data.

I'd flag the Regenerate button on the analysis dialog to anyone taking this over. I added it to test prompt edits without replaying the workshop. The docs say users won't see it in production, but nothing in the code hides it. A later change stopped the completion screen from opening that dialog, so in practice nobody reaches it. That is luck, and I'd want a proper gate.

The docs drifted from the code too. They say the temperature is 0.7 and max tokens 150. The code sets no max tokens and defaults to 0.4. They also list a nudge endpoint that doesn't exist; the nudge feature was taken out. The model names disagree between the code defaults, the example env file and my local env, which is why this page says “an OpenAI model” and leaves it there.

08Next

What I'd change

I'd start with the cleanup I skipped. The leftover round-number checks should go, with the round type stored as a field on each round. The Regenerate button needs an environment flag or needs deleting, and the docs need rewriting from the code.

Then I'd write tests for the stop decision. I changed the five whys prompt on 5, 9, 10 and 16 October, and none of those changes had an automated check behind it. A small set of saved transcripts, each with the expected decision to ask or stop, would have told me much sooner whether the 9 October change was a mistake.

The last commit is from 16 October 2025. On the data side there is a cleanup_old_data function in Postgres and a guide for running it every night at 2am with pg_cron.

Built with
  • Next.js
  • React
  • TypeScript
  • Zustand
  • Supabase
  • OpenAI
  • OpenRouter
  • Amazon SES
The takeaway

A five whys bot is easy to build and hard to make useful. Here the useful part came from reading what the model asked real people and fixing it one failure at a time, first the textbook questions and then the obvious ones. Making the model choose ask or stop through a forced function call kept that judgment visible to the code, with a hard cap of five that the model cannot talk its way past.

Your project

Have a workflow that looks like this?

Start with a free call. If a build makes sense, we run a free audit and send a one-page plan with a fixed quote in writing. No obligation.