Hoop: [H]uman in the L[oop]
written: 27 October 2026
A new member signs up. Collect their details, arrange their complimentary items, collect any additional payment, and mark the onboarding complete.
Then someone corrects a phone number. A parent signs up a second child. A Bronze member upgrades to Silver halfway through.
The workflow has to accommodate those changes and give the operations team a way to resolve what it cannot decide.
This is the story of building that into the new member onboarding workflow at Optimum Badminton Academy (OBA).
The 'Simple' Task
In OBA there are 3 tiers of membership (renamed for simplicity):
- Gold (Private Lessons)
- Silver
- Bronze
While their membership fees have already been collected, they have to be brought through an onboarding process to collect miscellaneous (optional) fees, and have their complimentary items ordered and delivered.
"Looks manageable..." I thought to myself hesitantly, as Jeron Wong (founder and chief coach of OBA) and Dominic (Operations Manager) finished scoping the skeleton of what it took to manually handle each sign-up:
| Step | Description |
|---|---|
| 1 | Manually detect a new sign-up on the legacy CRM. |
| 2 | Add the new member’s contact to Google Contacts. |
| 3 | Collect the appropriate form for the membership tier:
|
| 4 | Collect the form responses. |
| 5 | Collect payment if necessary. |
| 6 | Complete onboarding once the items have been delivered. |
| 7 | Aggregate the inventory changes. |
This is a full-on 1000 year old cedar tree in the Amazon rainforest, not a young sapling tree lol. My mind churned as more questions arose:
-
Google Contact Syncing:
- What if contact already exists? Clearly this is critical because a converted lead is likely nurtured.
- What if contact is invalid? -
Google Forms:
- How to correlate the form back to the sign-up?
- How to handle multiple form responses from the same user? Solely relying on form response alone will result in a double-claim exploit -
Payment Collection:
- How to wire the response of the Form to setup a conditional 'need-to-collect-payment' gate? -
Additional Complexity:
- The workflow needs to be tolerant to handle users upgrading/downgrade membership :O
The Elephant In the Room
So I think before implementation we need to address the elephant in the room:
What exactly is a workflow, and what are we trying to model?
I thought long and hard about this, and I think this is the answer for this deployment:
We are modelling the lifecycle of a sign-up
and this means that the unit is the "new member": every step above is something that happens to one member, and every operator gate is a decision about one member. Hold on to that thought, because it is about to be tested.
Where the Human Sits
The full implementation has been challenging, even more so when one unpacks the can of worms at each step mentioned above. Below are the key moments, and where sometimes 'levers' are given to the operators because human judgement is required.
Contact Syncing
On paper this is the easiest step: take the new member's name and number, create a Google Contact, done. Plot twist: it's not.
Edge Case 1: CRM does not enforce contact number correctness, and the welcome message with the form link goes out over WhatsApp through OBA's other automation provider, so a bad number silently breaks a hand-off to someone else's system. Therefore, we need to flag them for the Ops Team to pursue, and special handling once the invalid contact is resolved.
Edge Case 2: Families with more than 1 child often sign up with the same contact number. Naively adding would result in duplicates that the Ops Team have to clean up.
This means the 'unit' for this sync procedure is actually the contact number, not the 'new member': a second key that sits alongside the member, and the whole group has to be re-rendered whenever a member joins or leaves it.
Edge Case 3: The contact has already been added while nurturing the lead.
We would then need to search for existing contacts first before adding. If the contact exists, we need to provide resolution commands to handle such cases so that the operator does not always need to manually touch it.
The rule holds in reverse too. If an operator later edits a card the automation created, the next sync notices the drift and freezes that card instead of clobbering the edit. The human's change wins by default.
The operator has three commands to resolve these contact cases:
manual_fix: handle the contact manually.merge: automatically join the names.replace: replace the contact name using our naming style.
The automation flags the case; the operator chooses how it should be resolved.
Form Response
When a user submits a response, there isn't anything reliable for us to use to map it back to the new member. Fields in the form that can be potentially used are (but problematic as well):
- Trainee Name: name is non-unique
- Email: impossible to enforce user to fill up the exact same email as the one they signed-up with
- Contact: typo issues, and maybe valid but different number from the one used during sign-up
So, instead of relying on those typed details, we assign each member a stable, opaquereference_id, derived using HMAC from their canonical CRM user ID. The same member always gets the same reference, letting us associate their form responses without exposing the raw CRM user ID in the form. We pre-fill this reference in the form so that submitted responses can be associated with the member’s CRM record.
That stable reference also lets us treat multiple submissions as updates to the same member's response, so the latest one always wins. The catch is payment: a verification is recorded against the specific response the operator looked at, so if the member submits again afterwards, the verification lapses and the operator has to confirm payment once more. The member can change their mind; the system just refuses to let a stale confirmation carry over.
Collecting Payment
The form response decides whether there is anything to collect: name printing, a restring. We aggregate the payable items for the operator's reference. Once payment has been made and verified, the operator marks the payment verified, with their name on it, and only then can the member progress to the next workflow stage.
Upgrades and Downgrades
Members change their minds: a Bronze sign-up upgrades to Silver, perhaps even after they have already submitted the Bronze form. The operator fires the appropriate reassign-tier command, and the workflow rewinds itself to 'awaiting form response' for the new tier, because every stage is recomputed from what is known about the member rather than stored as a status. Stages can move backwards. That is not a bug; it is the system being honest about where the member actually is.
This handles tier changes while onboarding is still in progress. Once a member has completed onboarding, any later tier change falls outside this workflow.
Reaping the Fruits
It's been almost 4 months since the very first "phase 1a" deployment, and two weeks since our finalized "phase 2b" deployment. I'm glad that I invested the hard engineering work at the start: being disciplined with the tightly-scoped roll-outs, hammering out all the possible ways (as many as possible) the workflow can fail and hardening failure points. You don't want to be firefighting in the forest.
Implementing 'all at once' and then deploying to 'debug along the way' is kinda irresponsible (at least to me). This would create a lot of friction for the Ops Team and a lot more unnecessary clean-up work for me. I absolutely abhor the idea of 'babysitting automation' where the human has to clean-up the mess made by the automation (there might have even been more time savings trying to complete it yourself). So, just like a tree that grows over time, the workflow was broken into phases, each testable and each taking on more responsibility:
| Phase | Objective |
|---|---|
| 1a | validate 'new member detection' routine with browser automation |
| 1b | tier assignment for the members and operator commands to mutate a member's tier |
| 1c | form link generator & form response processor |
| 1d | the operator gates + accompanying commands to close the gates |
| 2a | contact sync-ing |
| ... | ... (omitted for article brevity) |
Look back at the sections above and the pattern repeats: automation pursues progress as eagerly as possible until it hits a roadblock that needs an operator's decision. The operator decides with one command, and work is handed back to the automation. I guess this is what human-in-the-loop means in practice: not a human checking the automation's homework, but a workflow that was shaped around the moments where only a human can decide.