Series: AI × Supply Chain build breakdown
Stack: claude code · google apps script (hello@ owned) · shopify admin api · python for the hashing
This whole entry is free to lift: copy and paste it into your own AI and ask it to build the same thing for your company.
The journey
Dispatch rules at oac have been through four stages:
- Remembering them. The rules lived in our heads. Fine with one person packing, until it wasn’t.
- Posters on the wall. We wrote them up and stuck them next to the bench. Better, but posters drift out of date and nobody re-reads a poster.
- A dispatch sheet on my computer. I built the rules into claude code. Every morning, about 40 minutes: pull the unfulfilled shopify orders, check each customer’s history, decide who gets a free sample, cross-check the UK waitlist, apply the edits, screenshot the pick sheet into the chat. It worked, but it only survived on my laptop. Away, on a call, or asleep: no sheet.
- An app anyone can open. A web app with the rules built in, owned by our shared hello@ account, restricted to oacsnacks.com logins. Whoever is doing dispatch that day opens it, presses one button, and packs.
The real problem was never the tooling. It was that the rules lived in my head, and a process you cannot hand over gets done differently by whoever picks it up next. Dispatch is getting more complex, not less: UK exports, subscriptions, winback gifts, B2B cases. Everyone needs to be able to step in.
What it does
Four tabs:
- Dispatch sheet. One button: pulls open orders, runs the rules, applies free samples directly to the shopify orders (customer notification off, so it stays a surprise), renders the pick sheet.
- Fix an order. One form for anything that went wrong.
- The rules. Every rule in force, in plain english.
- Help. Common questions, plus a box you can ask.
The rules engine
This is the actual work, and it is not code. Four rules, strict priority order, one fires per order:
- On the UK waitlist, ever received a sample box: nothing, ever.
- Tagged for a winback gift, not yet redeemed: a crumb plus a ball flavour they have never tried. Then stamp them redeemed.
- Never had the crumb, paid or free: a 30g crumb.
- Never had a protein ball, paid or free: one ball, rotating flavours evenly.
Getting to that list took the week. The code took a day.
Why it took a week: rules 3 and 4 ask “have they ever had this”. Simple, until you remember bundles. A customer who bought the birthday bundle has had four ball flavours and a crumb, but the order line just says “oac birthday bundle”. We shipped a sample to someone who had already tried four flavours before I caught it. The fix is a lookup of what is inside each bundle, so a bundle marks its component flavours as tried.
Most of the week was that shape: the rule sounds obvious, then the data disagrees.
The two decisions I would keep
Hashed matching, so the PII never travels. The UK waitlist is about 1,300 names, emails and home addresses. The app only needs “is this person on the list”. So it never sees the list:
- a python script on my machine turns it into SHA-256 hashes: hashed emails, plus hashed surname + postcode pairs
- the app hashes the customer in front of it and checks for a match
- roughly 5,700 pairs, no readable names anywhere in the cloud
The name + postcode half matters. Our best test case is a customer whose waitlist email and shopify email are different people’s addresses. Email matching alone misses her. Name + postcode catches her.
# what actually leaves my machine
{"emails": ["9f3c...", ...], # sha256(lowercased email)
"namezip": ["a71b...", ...]} # sha256("hartley|rg213hy")
Do not build a shopify app. The obvious move, and I decided against it. An embedded shopify app means oauth, app bridge, review, hosting. We are also looking at moving to a fulfilment partner, which could make this whole bench process irrelevant within the year. Apps script is free, two files, deployed in ten minutes, thrown away without regret. The valuable part is the rules, and those port anywhere.
What failed first (so you can skip it)
- The €0 sample SKU that cannot leave the country. In Ireland a free sample is a €0 product on the order. On a UK order the DPD label fails with Error 217: a UK parcel is an export, every line needs a customs value, and a €0 line has none. Fix: a UK freebie is the real full-price product with a 100% discount on that line. Two UK orders went out before this was written down anywhere.
- Buttons that went nowhere. Three pages, navigating by changing the URL. Clicking “replacement” did nothing: no error, nothing. Apps script serves your page in a sandboxed iframe that silently blocks navigation. Fix: one page, tabs, javascript showing and hiding sections.
- A number that was completely wrong. I asked how many customers carry a particular tag and got 9,666, basically our whole customer file. The shopify customersCount endpoint silently ignores a tag filter and returns everything. The real answer was 630. The lesson is not about shopify: I built a story on a number without testing whether the query did what I thought.
- Two forms that made sense to me and nobody else. A replacement flow (our fault) and a reship flow (carrier’s fault). Correct split, wrong question. At the bench nobody thinks “whose fault is this”, they think “we forgot the crumb”. They are now one form that asks what happened and what should go out, and derives the tags, the carrier claim and the stock correction itself.
The part I did not write
The layout came from Maja, who actually packs the boxes. We sat down and went through my version:
- she cut half the columns
- my explanatory notes were noise; “no insert” should just be an empty cell
- she needed the customer’s name and first address line, because sometimes the buyer and the recipient are different people and she writes the card
- the sample column should be a tick, not a sentence explaining my reasoning
- and one thing I would never have found in the data: she forgets to print multiple DPD labels on multi-box B2B orders almost every time. That is now four words on the B2B row.
The version she marked up is the version that shipped.
Where it landed
Anyone with an oac login opens it, presses one button, and the sheet is there. Replacements and reships run instantly and email me a receipt, which keeps the oversight without me being the bottleneck.
The rules tab generates itself from the same constants the engine uses, so it cannot drift from what actually runs.
Still open: per-person edit permissions, and retiring the older ten-minute job that applies the same rules in parallel. Two engines on one rule set is a bad idea, so editable settings wait until there is only one.
Leverage rating: 8 / 10
High because it removed a person-shaped dependency, not a task: the first oac process that runs properly without me in the room. Not a 10 because it is one process in one company, and the fulfilment partner decision could make the bench half redundant.
The thing I would tell another operator: the app was the easy part. The week went on making every rule precise enough that it did not need me to interpret it. That is the work worth doing even if you never build the app.