← Overview
Session 2 · Connect, Alert, Automate
Session 2 · hands-on · half to full day

Connect, Alert,
Automate

Three boxes still grey. Today we light them — and then we close the laptop and watch the thing run without us.

Ending with the retro: everything that went wrong, and what it taught us.

Recap in one picture

One box lit. Three to go.

⏱ still a human, on a Friday, remembering
2 · LogicPython: 3 rules
✓ Session 1
↩ weekly_report + alerts tabs — this morning
MORNING

The sheet, the API, the credential ceremony, and the round-trip.

AFTERNOON

An alert a tired human can act on. Then a schedule that never forgets.

LAST HOUR

The post-mortem. Four things went wrong. Each one taught us a principle.

Chapter 2.1

Connecting to Google Sheets

The client's one throwaway question, taken seriously.

The consultant skill, in one slide

The client asked one question. It contained two.

"Also: how do we connect Claude to Google Sheets?"

It was the last line of the brief. An afterthought. It is the most interesting sentence in the document.

HIDDEN QUESTION A

The script needs the data every Friday, with nobody present

Unattended. Scheduled. No human, no browser, no chat window. This needs the Google Sheets API.

HIDDEN QUESTION B

The human wants to interrogate the data conversationally

"Which SKUs look dead?" "What sold most in June?" Asked out loud, answered in a chat. This needs MCP.

Unpacking questions like this is the consultant skill. The client said "connect Claude to Google Sheets" and meant two entirely different things, wearing the same words. If you build only one, you answer half the question and never find out which half.
The course's signature move, one more time

Three ways to connect code to a spreadsheet

Option What it is For Against Verdict
Export CSV by hand A human downloads the sheet each Friday and feeds it to the script Zero setup. Works today. No credentials, no Google Cloud, no YAML. The human is the integration. It breaks the automation goal completely, and it breaks the first Friday somebody goes on holiday. RETIREDIt was exactly right for Session 1. It is exactly wrong now.
Sheets API + service account Our Python script gets its own robot Google account, with read access to the sheet Fully automatic. Standard practice everywhere. Free. Works at 3am with nobody logged in. A one-time credential setup ceremony. About 30 minutes of clicking through consoles that were designed by different teams in different decades. ✅ CHOSENThe production path.
MCP server for Google Sheets Claude itself reads the sheet, in conversation, while you watch Magical for exploration and ad-hoc questions. Zero code. You just ask. It's Claude-in-the-loop. There is no Claude in the loop at 18:00 on a Friday. Wrong tool for unattended jobs. ✅ ALSO USEDAs the exploration tool — not the automation.

Two winners, because there were two questions. That is not a compromise — it's the correct answer.

Before we click anything

Three words, in plain English

API

A door for programs

Google Sheets has a door humans use — the website, with buttons. It has a second door for programs: no buttons, no pictures, just "give me rows 1 to 500 of this tab."

An API is that second door. It needs a key, because anyone could knock.

SERVICE ACCOUNT

A robot employee

Not you. A separate Google account that belongs to your script, with an email address like stock-bot@your-project.iam.gserviceaccount.com.

You share the sheet with the robot, exactly as you'd share it with a colleague. That's the whole idea.

MCP

Giving Claude hands

Model Context Protocol. A standard way to plug tools into Claude, so that during a conversation it can actually go and read your sheet rather than guessing.

You chat; it fetches. Wonderful with a human present. Useless without one.

Why a robot and not just your own account? Because you will leave the company, change your password, or turn on two-factor auth. The robot won't. And you can revoke the robot's access without touching your own.
Watch · then we do it together

Somebody else clicking through the same five screens

Open on YouTube ↗  ·  Azzrael Code
WHY WE STOP HERE

The next thirty minutes are the hardest and dullest of the course: five consoles, designed by five different teams. Watching someone do it once, end to end, makes the clicking far less disorienting when it's your turn.

HOLD THESE QUESTIONS WHILE YOU WATCH
  • How many separate screens does the robot's key touch?
  • Do they remember to share the sheet with the service account?
  • What do they do with the JSON file afterwards?
Google moves these buttons. If the console in the video doesn't match yours, the video is older than the console — the five steps are stable even when the screens aren't. That, too, is a lesson.
Credential ceremony · step 1 of 5

Create a Google Cloud project

  1. Go to console.cloud.google.com
  2. Click the project dropdown, top left, next to "Google Cloud"
  3. New Project → name it stock-alerts
  4. Wait ~20 seconds. Make sure the dropdown now says stock-alerts.
The classic mistake: creating the project and then doing every subsequent step inside a different project, because the dropdown silently reverted. Check the dropdown before every step.
WHAT IS A "PROJECT"?

A folder for permissions and billing. Nothing runs in it. It exists so Google knows which things belong together, and so you can delete all of them at once.

Will this cost money? No. The Sheets API is free at any volume you'll produce here, and Google will still ask for a credit card at some point in your life, but not today.
Credential ceremony · step 2 of 5

Enable the Sheets API

  1. In the search bar at the top, type Google Sheets API
  2. Click the result (it's a product page, not a settings page)
  3. Press the big blue Enable button
  4. Wait. The page will reload into a dashboard with zero traffic on it.
WHY IS THIS A STEP AT ALL?

Google has hundreds of APIs. They're all switched off by default, per project. You are turning on exactly one door and leaving the rest locked.

Later, if you write results back to the sheet and get a confusing permission error, the answer is almost always: you enabled the API in the other project. Check the dropdown.
You do not need the Google Drive API for this project. Many tutorials tell you to enable it. You only need it if your script goes hunting for the sheet by name. Ours will hold the sheet's ID directly.
Credential ceremony · step 3 of 5

Hire the robot

  1. Search for Service Accounts (it lives under "IAM & Admin")
  2. Create service account
  3. Name it stock-bot. Skip the optional "grant access" steps — those are about Google Cloud, not about your sheet.
  4. Done. Now copy its email address. It looks like this:
stock-bot@stock-alerts-472913.iam.gserviceaccount.com

Copy it somewhere. You need it in step 5, and step 5 is the one everybody forgets.

WHAT YOU JUST DID

You created a new Google identity. It has an email address. It cannot log in to anything, it has no password, and right now it has access to nothing at all.

Read that last part again. Creating the robot does not give it your sheet. Nothing has been connected yet. That's step 5, and its absence is why your script will fail with a 403 in about twenty minutes.
Credential ceremony · step 4 of 5

Download the robot's password

  1. Click your new stock-bot service account
  2. Go to the Keys tab
  3. Add key → Create new key → JSON
  4. A file downloads. Move it into your project folder and rename it service-account.json.
This file is a password. Anyone who has it can act as your robot, forever, from anywhere on earth. Google will never show it to you again — there is no "view key" button, only "create another one".
{
  "type": "service_account",
  "project_id": "stock-alerts-472913",
  "private_key_id": "a1b2c3...",
  "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEv...
  "client_email": "stock-bot@stock-alerts-472913
                   .iam.gserviceaccount.com",
  "token_uri": "https://oauth2.googleapis.com/token"
}

Note client_email. That's the robot's address — the one you need in step 5. It was in the file all along.

Credential ceremony · step 5 of 5 · the one everyone forgets

Share the sheet with the robot

  1. Open your Google Sheet
  2. Click Share, top right — the same button you'd use for a colleague
  3. Paste the robot's email: stock-bot@…iam.gserviceaccount.com
  4. Give it Editor (we'll write report tabs back). Viewer is enough if you only read.
  5. Untick "notify people". The robot does not read its email.
The robot is a colleague.
You share the sheet with it.
THAT'S THE WHOLE SECURITY MODEL

The robot can see exactly the sheets you shared with it and nothing else. Not your Drive. Not your email. Not your other spreadsheets.

To revoke it: remove it from the share list. Done. One click, and it is blind again.

Your first security lesson. Exactly one slide.

The JSON key is a password. Treat it like one.

NEVER
  • Paste it into a chat window
  • Email it to a colleague
  • Commit it to git
  • Put it in a slide deck (we didn't — that key above is fake)
ALWAYS
  • Add it to .gitignore before your first commit
  • Keep it out of the code — load it from a file path or an environment variable
  • In the cloud, store it as a secret (Session 2, later)
THE PROMPT
Claude Code
Set up .gitignore for this project so that
service-account.json and any .env file can
never be committed. Then show me the file.
If you ever do leak one: don't panic and don't delete the repo. Go to the Keys tab, delete that key, create a new one. The old key is dead within seconds. Rotation is routine; pretending it didn't happen is not.
Hands-on · 15 minutes

Move the CSVs into a real sheet

  1. Create a new Google Sheet called Stock Alerts
  2. Rename the first tab to movements. File → Import → movements.csv.
  3. Add a second tab, sku_master. Import sku_master.csv.
  4. Share it with the robot (step 5, if you haven't).
  5. Copy the sheet's ID out of the URL — the long string between /d/ and /edit.
docs.google.com/spreadsheets/d/1aB2cD3eF4gH5iJ6kL7mN8oP9qR/edit
A REAL DECISION, HIDING HERE

Two tabs in one sheet, or two separate sheets?

One sheet, two tabs. One thing to share with the robot, one ID to remember, one thing to back up. There is no benefit to splitting them and one more thing to forget.

The sheet ID is not a secret. It's an address, not a key. Knowing it gets you nothing without permission — which is why the robot needed step 5. Put the ID in your code; put the key in .gitignore.
Hands-on · the moment of truth

Teach the script to read the sheet

Paste into Claude Code
Change rules.py so it reads its data from Google
Sheets instead of the local CSV files.

- Use the service account at service-account.json
- Sheet ID: 1aB2cD3eF4gH5iJ6kL7mN8oP9qR
- Tabs: "movements" and "sku_master"
- Same columns as the CSVs, first row is headers

Keep all three rules exactly as they are. Only the
loading changes. Add the Python packages you need
to requirements.txt.

If the sheet can't be reached, print a clear error
saying WHY - don't just crash with a stack trace.
"ONLY THE LOADING CHANGES"

This sentence is the whole reason Session 1 refused to touch Google Sheets. The rules don't know where their data came from, so swapping the source is a small, safe, boring change.

THE LAST PARAGRAPH

We asked for a good error message before we had an error. In about forty seconds, roughly a third of the room will be very glad we did.

Hands-on · closing the loop

Write the answers back into the sheet

Paste into Claude Code
Now write the results back to the same sheet.

Create or overwrite a "weekly_report" tab with the
weekly movement summary.

Create an "alerts" tab and APPEND one row per alert:
timestamp, alert_type (low_stock | dead_stock |
data_quality), sku, detail.

Append, don't overwrite - the alerts tab is our
audit log. We want to be able to ask "did this fire
last Friday?" in six months.
OVERWRITE vs APPEND

weekly_report is a snapshot — this week's numbers, replaced each run.

alerts is an event log — appended forever, never overwritten.

The same distinction from Session 1, showing up in a completely different place. Once you see it, you see it everywhere.

Remember the rejected option? "Red rows in the sheet" lost the alert-channel decision in Session 0 — it's a pull medium. But we needed an audit log anyway, so we get it for free. Rejected options often survive as side effects.
Celebrate this. Genuinely, out loud.

The data round-trips

⏱ still a human pressing the button
1 · DataGoogle Sheet
✓ movements + sku_master
connectionSheets API
✓ service account
2 · LogicPython: 3 rules
✓ unchanged
↩ ✓ weekly_report tab · ✓ alerts tab (append-only audit log)
WHAT JUST HAPPENED

A program you directed into existence read a spreadsheet it was given permission to see, applied three business rules you defined, and wrote its conclusions back where a human can read them.

WHAT'S STILL MISSING

Nobody knows it happened. The answers are sitting in a tab. That's a pull medium — and we knew that in Session 0.

The client's other question

MCP: chatting with your own spreadsheet

Connect an MCP server for Google Sheets to Claude, and you stop writing code to ask questions. You just ask.

you ▸ which SKUs haven't sold since April?

claude ▸ Reading the movements tab… 3 SKUs have no outbound movement since 1 April: TRC-30x30-TERR (last sold 6 Feb), CRM-25x40-WHT (last sold 22 Mar), and WDX-20x120-OAK — though that one only launched in May.

It noticed the oak plank caveat. Because you're there, in the loop, reading it.

WHAT MCP IS PERFECT FOR
  • Exploring data before you know the rules
  • One-off questions nobody will ever ask twice
  • Sanity-checking what your script just wrote
  • Letting a non-technical owner interrogate their own business
WHAT IT CANNOT DO

Run at 18:00 on Friday while everyone is at dinner. There is no conversation happening. There is nobody to read the answer.

Watch · Anthropic on MCP

MCP, from the people who wrote the protocol

Open on YouTube ↗  ·  Anthropic
WHY WE STOP HERE

I've described MCP as "giving Claude hands." That's a metaphor, and metaphors leak. Here is the protocol described by the people who designed it, so you can judge my metaphor for yourself.

HOLD THESE QUESTIONS WHILE YOU WATCH
  • What problem existed before MCP that it solves?
  • Who is present when an MCP tool runs?
  • Would you build our Friday job on it? (We didn't. Listen for why.)
The distinction that matters for us: MCP puts Claude in the loop. Our Friday job has nobody in the loop. Two needs, two tools — which is what the client's single question was hiding.
Two tools, two jobs

When to reach for which

Sheets API + service account MCP server
Who is present? Nobody. That's the point. You are. That's the point.
When does it run? On a schedule. Every Friday, forever. When you feel like asking.
What does it produce? The same answer, the same way, every time. A conversation. Different every time — which is the feature and the bug.
Is it repeatable? Yes. It's code. You can read it, test it, and blame it. Not exactly. Ask twice, get two phrasings.
Use it for The Friday job. Anything that must happen without you. Exploration. Anything where the question is new.
The rule that generalises: automation must be boring and repeatable. Exploration should be fast and conversational. Reaching for the conversational tool to do a repeatable job is the single most common mistake of the AI era.
Chapter 2.2

Alerts That Reach Humans

Now we build the push.

Recall, from Session 0

Everything we've built so far is pull

THE SHEET

Someone must open it.

THE alerts TAB

Someone must open it, then find the tab.

THE TERMINAL OUTPUT

Someone must run the script and be looking at the screen.

An answer nobody sees
is not an answer.

Question 3 of the four: how do answers reach a human? We decided in Session 0 — email for v1, LINE for v2 — and we've been deliberately ignoring it for a day and a half. Now we build it.
Decision 3a · how do we actually send the thing?

Two ways to send an email

Option What it is For Against Verdict
Gmail SMTP
with an app password
Your script logs into your own Gmail and sends as you Zero signup. No new vendor. Works in fifteen minutes. Free forever at this volume. Requires 2FA plus an "app password" — another credential ceremony. Gmail may rate-limit you. The alert comes from a person, not a system, which is subtly confusing. ✅ CHOSENZero signup wins on a teaching day.
A transactional email API
e.g. Resend, Postmark
A service whose whole job is sending mail from programs Built for this. Better deliverability. Logs, retries, a dashboard. Generous free tier. A signup, an account, a domain to verify if you want to look professional. A vendor you now depend on. RIGHT ANSWER FOR PRODUCTIONIf this were shipping to a paying client, choose this.
Be honest about why we chose SMTP: because thirty people signing up for a new service mid-workshop costs twenty minutes and produces a lesson about signup forms. That is a teaching reason, not an engineering one — exactly like the Apps Script decision in Session 0. Say your real reasons out loud.
What Claude will write if you don't ask for better

A bad alert

From: stock-bot · To: warehouse@… · Fri 18:05
Subject: Report
{"week": "2026-07-04/2026-07-10",
 "movements": [{"sku": "GRN-60x60-MATT",
   "in": 500, "out": 1240, "net": -740},
  {"sku": "MRB-80x80-POL", "in": 0,
   "out": 120, "net": -120}, ...],
 "low_stock": [{"sku": "MRB-80x80-POL",
   "stock": 180, "threshold": 200}],
 "dead_stock": [{"sku": "TRC-30x30-TERR",
   "days": 154}],
 "warnings": [{"sku": "SLT-40x40-BLK",
   "stock": -35}]}
WHAT'S WRONG WITH IT
  • The subject line says nothing. It could be good news.
  • It's a data dump, not a message.
  • Everything is equally important, so nothing is.
  • It never says what to do.
  • An 18:05 Friday reader closes it and opens it Monday. Maybe.
And it is technically perfect. Every number is correct. Every rule fired properly. The software works and the system fails.
Alert design is UX writing

A good alert

From: stock-bot · To: warehouse@… · Fri 18:05
Subject: ⚠️ 1 SKU below reorder threshold · 1 dead · 1 data problem

⚠️ Reorder now — 1 bestseller is low

MRB-80x80-POL · Marble 80×80 Ivory
180 sqm left, threshold 200. Sold 120 sqm this week — about 10 days of stock.

💤 Dead stock — 1 SKU

TRC-30x30-TERR · Terrazzo 30×30 Sand
No sales for 154 days. 640 sqm on the shelf. Consider a promotion or stop reordering.

🔧 Data problems — 1

SLT-40x40-BLK shows −35 sqm, which is impossible. A delivery was probably never recorded. Please check the invoices.

Week Sat 4 Jul → Fri 10 Jul · 4 SKUs moved · full detail in the weekly_report tab

WHAT CHANGED
  • The subject line is the summary
  • Most urgent first, least urgent last
  • Every section says what to do
  • "About 10 days of stock" — a number we computed so a human doesn't have to
  • The data dump moved to the sheet, where data dumps belong
The intern reading this at 18:05 on a Friday must know what to do.
The prompt that produced it

You have to ask for a good alert

Paste into Claude Code
Send the results by email via Gmail SMTP. Read the
address and app password from environment variables,
never from the code.

The email is read by a warehouse manager at 18:05 on
a Friday, on their phone, while leaving. Write it for
that person:

- Subject line = the summary. Counts, not "Report".
- Order sections by urgency: low stock, then dead
  stock, then data-quality problems.
- Every section says what to DO, not just what is.
- For low stock, also estimate days of stock left,
  using this week's outbound quantity.
- Full tables stay in the sheet. Don't paste them
  into the email.
- If nothing is wrong, send nothing at all.
THE LAST LINE MATTERS MOST

"If nothing is wrong, send nothing at all."

An alert that arrives every Friday whether or not anything is wrong trains people to ignore it — and then it fails on the one Friday it mattered.

Push spends attention. Spend it only when you have something worth buying.

The counter-argument, which is real: silence is ambiguous. Did nothing go wrong, or did the job not run? Fix that with a monthly "everything is fine" heartbeat, not a weekly one.
v2 · the case's real ending

LINE — where the team actually lives

Email is where information goes to wait. In Thailand, the team's real nervous system is a LINE group, open all day, on everyone's phone.

The alert doesn't need to be seen. It needs to arrive where people already are, and where somebody can immediately reply "already ordered, arriving Tuesday."

That reply is the point. An alert in a group chat becomes a conversation with an owner. An alert in an inbox becomes a task nobody claimed.
WHAT IT COSTS
  1. A LINE Developers account
  2. A Messaging API channel
  3. A channel access token (another secret — same rules)
  4. Invite the bot to the group; get the group ID
  5. One HTTP POST from Python
Second credential ceremony of the day. If time is short, demo a pre-built version rather than making thirty people do it. Two credential ceremonies in one day kills a room, and the lesson is identical to the first one.
Chapter 2.3

Automate the Friday Run

Question 4: what makes it run without a human?

Recall, from Session 0

We chose GitHub Actions. Now justify it.

IT'S FREE

A weekly job that runs for thirty seconds uses roughly 0.02% of the free tier. This will never cost anyone anything.

NO SERVER

Nothing to patch, nothing to restart, no laptop that must be awake. It runs on a machine that appears for 40 seconds and then ceases to exist.

IT FORCES CODE INTO GIT

Which we should have done anyway. The "cost" of this option is a thing we wanted.

And the honest Against, from Session 0: three new concepts on day two — a repository, a YAML file, and secrets. Plus one genuine trap that we flagged in advance and are about to walk into anyway.

git / repository a folder whose entire history is remembered, and which can be copied to a server. GitHub is a website that hosts those folders.

Hands-on

Push the project to GitHub

Paste into Claude Code
Put this project on GitHub as a PRIVATE repository
called stock-alerts.

Before the first commit, double-check that
.gitignore excludes service-account.json and .env,
and show me exactly which files WILL be committed
so I can approve the list.
Read that file list. Every single week, somewhere in the world, a private key is committed to a public repo and found by a bot within ninety seconds. Reading the list is the whole defence, and it takes four seconds.
WHAT CLAUDE DOES FOR YOU
  • Runs git init, stages, commits
  • Creates the repo (via the gh CLI, if you have it)
  • Pushes
  • Explains any of it, in plain English, if you ask
Private, not public. Not because the code is secret — it isn't — but because your sheet ID, your email address, and your client's SKU names are in there. A private repo costs nothing.
The credential, one more time

The robot's key can't be in the repo. So where?

GitHub has a vault. You paste the secret in once; the running job can read it; nobody — including you — can ever read it back out.

  1. Repo → Settings → Secrets and variables → Actions
  2. New repository secret
  3. Name: GOOGLE_SERVICE_ACCOUNT_JSON
    Value: the entire contents of service-account.json
  4. Repeat for GMAIL_ADDRESS and GMAIL_APP_PASSWORD
THE PATTERN, EVERYWHERE

Code goes in the repo. Secrets go in a vault. The code asks the vault for the secret at the moment it runs, and never writes it down.

Every cloud platform on earth does this, with different button names.

Note the write-only door. GitHub will never show you that value again. If you lose it, you don't recover it — you rotate it. That property is a feature, and it's why leaked keys get rotated rather than retrieved.
Claude writes this. You review one line.

The workflow file

name: Weekly stock alerts

on:
  schedule:
    - cron: "0 11 * * 5"     # ← review THIS line
  workflow_dispatch:          # ← lets you run it by hand

jobs:
  run:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -r requirements.txt
      - name: Write credentials from the vault
        run: echo '${{ secrets.GOOGLE_SERVICE_ACCOUNT_JSON }}' > service-account.json
      - name: Run the rules
        env:
          GMAIL_ADDRESS: ${{ secrets.GMAIL_ADDRESS }}
          GMAIL_APP_PASSWORD: ${{ secrets.GMAIL_APP_PASSWORD }}
        run: python rules.py
WHAT IT SAYS, IN ENGLISH

"Every Friday at 11:00 UTC — or whenever I press the button — rent a Linux machine, put Python on it, install what we need, fetch the secrets from the vault, run our script, then throw the machine away."

YOUR JOB HERE

Not to write YAML. Claude writes YAML. Your job is to review the cron line, because it is the only line that encodes a business decision.

workflow_dispatch is the "run it now" button. Insist on it. A scheduled job you can't trigger by hand is a job you cannot test.
Watch · cron, without the folklore

Scheduling a job you never have to remember

Open on YouTube ↗  ·  The Techzeen
WHY WE STOP HERE

Claude wrote our YAML and we only reviewed one line of it. Before you trust a schedule that runs without you for the next two years, it is worth seeing someone explain what that line does.

HOLD THESE QUESTIONS WHILE YOU WATCH
  • What are the five fields of a cron expression?
  • Do they mention which timezone the schedule runs in?
  • How do they test it without waiting for Friday?
Listen very carefully for the timezone. Most tutorials mention UTC once, in passing, and move on. That single unremarked sentence is the bug that cost us a month of empty Friday reports.
The trap we flagged in Session 0, and walked into anyway

Cron speaks UTC. Your client doesn't.

What we wrote first: cron: "0 18 * * 5"
"Friday at 18:00." Obviously.
What actually happened: the job ran at 18:00 UTC — which is 01:00 on Saturday morning in Bangkok. The email arrived while everyone was asleep. And because it ran after midnight, "this week" had already rolled over. The weekly summary was empty.
The fix: Bangkok is UTC+7. 18:00 − 7 = 11:00 UTC.
cron: "0 11 * * 5"
WHY THIS BUG IS SO GOOD

It doesn't crash. Nothing turns red. GitHub reports a successful run, every week, forever.

It just produces a slightly wrong answer at a useless time, and nobody notices for a month.

THE PRINCIPLE

Time zones are not a detail. Any time a computer and a human disagree about what "Friday evening" means, the computer wins and the human is confused.

Write the timezone in a comment. Always. # 11:00 UTC = 18:00 Bangkok

The last hands-on step

Press the button. Then let Friday be Friday.

  1. Repo → Actions tab
  2. Choose Weekly stock alerts
  3. Run workflow → the button that workflow_dispatch gave you
  4. Watch the log scroll. Wait for the email.
  5. Open the email on your phone — that's where it'll be read.
If the email arrives and makes sense on a phone screen, you are done. The rest is Friday doing what Fridays do.
WHAT TO CHECK IN THE LOG
  • It found the sheet (not a 403 — that's step 5 again)
  • The row count looks right
  • The alerts tab got new rows appended
  • The email actually sent
AND NOW, THE ACTUAL WIN

Close the laptop. Go home. On Friday at 18:00 Bangkok time, a computer you have never seen will run code you directed into existence, and someone's phone will buzz.

Chapter 2.4

Retro: The Post-Mortem

This chapter is what makes it a case study and not a tutorial.

Four things went wrong. On purpose, and by accident.

What went wrong, and what it taught us

What happened Why it happened The principle it taught
SLT-40x40-BLK had −35 sqm of stock A real delivery was never recorded. Our arithmetic was flawless; the world wasn't. Apps that touch reality need a "reality is broken" lane. Report bad data. Never crash on it, never silently fix it.
We told the client their new oak range was dead stock Rule 3 was implemented exactly as specified. The specification had never met a two-month-old product. Requirements are incomplete, not wrong. Only running against real data finds the boundary. Show the bug, then fix it.
The Friday job fired at 01:00 Saturday, Bangkok Cron is UTC. Nothing errored. The green checkmark lied to us for a month. The worst bugs don't crash. Test the thing end-to-end, at the time and on the device where a human meets it.
Thirty minutes lost to a 403 PERMISSION_DENIED We created the robot and never shared the sheet with it. Step 5 of five. Credential ceremonies are a real cost. Budget for them, name them out loud, and never let a beginner conclude they're stupid.
Notice: not one of these was a coding error. Claude wrote correct code every time. Every single failure was a thinking failure — an assumption nobody had checked.
Not wrong. Right for now.

Decisions we'd revisit at 10× scale

Today's choice What breaks it What we'd move to Cost of the move
Google Sheets as the store Tens of thousands of movement rows. Two people editing at once. One person typing "abc" into a quantity cell. SQLite, then Postgres. Sheets survives as the input form. DAYSBecause movements are rows, and rows move. We designed for this without paying for it.
Manual is_bestseller flag 300 SKUs. Nobody maintains the ticks. It quietly goes stale and rule 2 stops firing. Computed from sales velocity — top N by outbound sqm over 90 days. HOURSNothing downstream cares where the flag came from. That was the point of choosing a column.
Email alerts Nobody acts on them. There is no record of who did what. LINE with acknowledgment buttons — "Reordered ✓ / Ignore". DAYSAnd now the alert has state, which means you need somewhere to store it. Question 1, again.
One threshold per SKU Seasonality. A tile that sells 400 sqm/week in November and 40 in May needs two different answers. Threshold as days of cover, not sqm. "Alert when less than 3 weeks of stock remains." HOURSAnd it's a better rule. We'd have got it wrong on day one by guessing.

Three of these four cost hours — because we chose reversible doors on purpose, back in Session 0.

The only scoreboard that counts

What the client actually got

BEFORE
  • One person opens an Excel file every Friday and types what changed
  • Low stock is noticed when someone walks past an empty rack
  • Dead stock is noticed during the annual count, if at all
  • The 640 sqm of terrazzo has been sitting there since February
  • Nobody knows about the missing slate delivery
AFTER
  • The Friday summary writes itself
  • A bestselling SKU below its threshold pushes an alert to the team's phones
  • Dead stock surfaces at 122 days, not at year end
  • Data-entry errors get reported instead of silently compounding
  • An append-only audit log of every alert ever fired
INFRASTRUCTURE COST

≈ $0 / month

Sheets free. GitHub Actions free tier. Gmail free.

TIME TO BUILD

2 days

Of learning-while-building, by people who could not code on Monday morning.

MOST VALUABLE OUTPUT

The CLAUDE.md

Because the decisions are the project, and the code is regenerable.

The bookend

The four questions, one last time

01 · DATA

Where does the data live?

Because the humans already lived there.

Google Sheet · movements (events) + sku_master · never snapshots
02 · LOGIC

What turns data into answers?

Because Claude is excellent at it and beginners can read it.

Python · 3 rules · a data-quality lane · an age guard we learned the hard way
03 · HUMAN

How do answers reach a human?

Because alerts must push, never pull.

Email → LINE · written for a tired person at 18:05 · silence when all is well
04 · TRIGGER

What makes it run without a human?

Because humans forget, and cron doesn't.

GitHub Actions · 0 11 * * 5 · which is 18:00 in Bangkok, and we will never forget that again
Same slide as Session 0, slide 7. The questions didn't change. Only the answers did — and every answer arrived from an options table where the rejected options were argued honestly.
The ideas ladder

Where to go next

RUNG 1 · an evening

Build the dashboard

Now it's a legitimate bonus rather than a substitute for an alert. A single page that reads the sheet. Claude will build it in an hour.

RUNG 2 · a weekend

Reorder quantity suggestions

Don't just say "low." Say "order 600 sqm — that's 8 weeks at the current rate." The data is already there.

RUNG 3 · a real project

Connect the POS

Stop typing movements by hand. Every sale becomes an out row automatically. This is the version where the Friday Excel habit finally dies.

RUNG 4 · the real test

Teach a colleague

Using this deck. If you can walk someone through the four questions and defend the options tables, you have the skill. That was always the deliverable.

Notice rung 3 changes the architecture. The POS becomes the source of truth, and Question 1 gets a new answer. That's not a failure of our design — that's what it looks like when a business grows into the next set of trade-offs.
The whole course, in one sentence

The skill is not writing code.
The skill is thinking clearly about the problem
and directing Claude well.

CLAUDE WROTE

Every line of Python. The YAML. The email formatter. The sample data. Roughly 400 lines, correct on the first or second attempt, every time.

YOU DECIDED

That a week starts on Saturday. That dead means outbound. That a young SKU can't be dead. That bad data gets reported, never hidden. That the alert must push.

AND YOU NOTICED

When the code was perfect and the answer was wrong. Four times. That flinch is the whole job — and it is the one part nobody can do for you.

Thank you. Now go and ask somebody the four questions.

Keyboard
space
Next slide
Previous slide
N / S
Toggle speaker notes
V
Play the video on this slide
O
Slide list / jump
F
Full screen
Home / End
First / last slide
Esc
Close panels

Tip: Cmd/Ctrl + P prints the whole deck, one slide per page.