← Overview
Session 1 · Setup & First Build
Session 1 · hands-on · half to full day

Setup &
First Build

Today the logic gets real. No Google Sheets, no email, no schedule — just three rules that work on messy data, on your own laptop.

The walking skeleton, built in public. Including the two bugs we plant on purpose.

Recap in one picture

Where we left off

⏱ Friday 18:00 — not today's problem
1 · DataGoogle Sheet
Session 2
connectionSheets API
Session 2
3 · HumanEmail / LINE
Session 2
↩ weekly_report + alerts tabs — Session 2
TODAY'S INPUT

A CSV file we generate ourselves — deliberately messy.

TODAY'S OUTPUT

Text printed on your screen, and one CSV report.

TODAY'S WIN

Three business rules, correct against data that fights back.

Chapter 1.1

Environment Setup

Forty-five minutes of plumbing. Then we never think about it again.

Tick every box before we move on

Install checklist

  1. VS Code is open
    The editor. Free. code.visualstudio.com
  2. Node.js is installed
    Check: node --version → prints something like v22.x
  3. Claude Code is installed
    npm install -g @anthropic-ai/claude-code
  4. You are logged in
    Run claude in the terminal, follow the browser login.
  5. It answers you
    Type hello, what model are you? and get a reply.
Why Node.js if we're writing Python? Because Claude Code itself is built on Node. It's a tool, not our language. You will never write a line of JavaScript.
Don't install Python yet. We'll do it in ten minutes, together, when we actually need it — and if you get stuck, Claude Code can install it for you.
DONE WHEN

Every person in the room has seen Claude reply to them once. Not "nearly." Everyone.

A small options table, because that's who we are now

Two ways to run Claude Code

Option What it is For Against Verdict
VS Code extension A Claude panel docked beside your files You see the files change as they change. Friendlier for beginners. Diffs are visual. Hides the terminal — which is where you eventually want to live. ✅ CHOSENfor today
Terminal CLI Type claude in any folder Works over SSH, on servers, in any editor. This is the real tool. A blank black rectangle is terrifying on day one. USE BOTHThe extension opens a terminal anyway. Same tool, two doors.
They are the same program. This is not a coin-flip decision worth ten minutes of debate — it's a preference. Start with the extension, drift to the terminal by Session 2.
Four things, that's all

Orient yourself inside VS Code

stock-alerts — VS Code
① File explorer 📁 stock-alerts
  📄 CLAUDE.md
  📄 create_sample_data.py
  📄 rules.py
  📄 movements.csv

Your project is a folder.
Nothing more mystical than that.
② Claude Code panel you ▸ read movements.csv and tell
me what columns it has


✓ Read movements.csv (1,847 rows)
Columns: date, sku, type, qty_sqm, note

You type in English.
It reads, writes, and runs.
③ Terminal $ python rules.py
…output appears here…

④ Editor The big space in the middle,
where the code shows up.
You will read it. Rarely type in it.

Four regions. You'll use two of them 95% of the time: ② the Claude panel and ③ the terminal.

Everyone's least favourite rectangle

The terminal, demystified

The terminal is just chat with your computer.

And now Claude sits inside it, translating.

You already send text to a machine and get text back, all day. The only new thing is that this machine takes the message literally, and never guesses what you meant.

The permission you need: you cannot break your computer by typing a wrong command into a project folder. The worst realistic outcome is a red error message. Copy it, paste it to Claude, move on.
THE ONLY 3 COMMANDS YOU NEED TODAY
cd stock-alerts   # go into the folder
python rules.py   # run our program
claude            # start Claude Code
Red text is not danger. Red text is information. It is the computer telling you, in unnecessarily hostile language, exactly what it needs.
Everybody. Right now. Two minutes.

First contact

Paste into Claude Code
Create a file called hello.py that prints today's date in a friendly format, then run it and show me the output.
$ python hello.py Today is Thursday, 09 July 2026.
WHAT JUST HAPPENED
  • Claude created a file
  • Claude ran it
  • Claude showed you the result
  • You typed one English sentence
Look at hello.py in the editor. Three lines of Python. You can read it. It says what it does. That is the whole language.
Watch · or bookmark for when you're stuck

The setup, slowly, by someone with infinite patience

Open on YouTube ↗  ·  freeCodeCamp.org
WHY WE STOP HERE

We just did setup in forty-five minutes and some of you are still not green. This is the same ground, covered slowly, that you can pause and rewind at home. Play the first few minutes now; keep the link for tonight.

HOLD THESE QUESTIONS WHILE YOU WATCH
  • Does your terminal look like theirs? If not, what's different?
  • Which step did you skip?
  • What does it look like when it works?
Don't watch this in full, in the room. It's a full course. We are stealing five minutes of it as a safety net for the people whose install misbehaved.
The presenter's insurance slide

Common setup failures, and fixes

Symptom What it means Fix
node: command not found Node isn't installed, or the terminal was open before you installed it. Close the terminal and open a new one. Really. That fixes it 70% of the time. Otherwise reinstall from nodejs.org.
EACCES on npm install -g npm is trying to write to a folder your user doesn't own. Don't reach for sudo. Ask Claude on your phone, or use Node's official installer, which sets permissions correctly.
Login loop — browser opens, nothing happens Usually a corporate VPN or an ad-blocker eating the callback. Disable the VPN for two minutes. Try a different browser. Try tethering to your phone.
Corporate wifi blocks everything A proxy is intercepting npm and the login callback. Tether to a phone hotspot. This is why the homework said "check this before you arrive."
python: command not found macOS ships python3, not python. Type python3. Or ask Claude Code: "set up a Python environment in this folder" — it will.
The meta-fix: paste the error into Claude Code and say "I got this error, what do I do?" It will be right about 90% of the time — and being right 90% of the time is enough when the other 10% costs you a Google search.
Chapter 1.2

Design the Data
Before Any Code

The client gave us a Friday Excel habit. Our first real job is turning a habit into a data model.

The job

From a habit to a data model

Somebody, every Friday, opens a file and types what changed this week.

That habit already contains a data model. It's just implicit, undocumented, and living in one person's head. Our job is to write it down — and, while writing it down, to notice the places where the habit quietly loses information.

A data model is the list of things you write down, and what you write down about each of them. That's it. No more mystical than a form.
WHAT THE HABIT LOSES

If Friday's file only shows today's totals, then last Tuesday's sale is gone forever. You can never answer "when did this last sell?"

WHAT WE'RE ABOUT TO DECIDE

Whether to record what things are or what happened to them. It sounds philosophical. It is the entire project.

The real file. Laugh with it.

What the client actually has

A B C D E F
1 STOCK REPORT — updated Friday (please don't delete)
2 Date รหัส / Code In Out qty (ตร.ม.) notes
3 3/7/26 GRN-60x60-MATT 500 from supplier, invoice 4471
4 3/7/26 MRB-80x80-POL 120 Khun Somchai project
5 TOTAL THIS WEEK 620 ← a totals row, in the middle
6 10/7/26 SLT-40x40-BLK 85 reorder soon!! last one
7 10/7/26 grn-60x60-matt 1,240 big job

Every real business file looks like this. Yours does too.

Six problems, and one of them isn't a problem

Reading the mess

1A merged title row. Row 1 isn't data. Code that assumes row 1 is headers reads "STOCK REPORT" as a column name.
2Mixed Thai/English headers. Fine for humans. Code needs one canonical name per column.
3In/Out as two tick columns. A row is either in or out — that's one fact wearing two columns.
4A totals row in the middle. Sum the column and you count 620 sqm twice.
5Inconsistent case. GRN-60x60-MATT and grn-60x60-matt are the same tile and two different SKUs, as far as a computer is concerned.
6Ambiguous dates. 3/7/26 — 3 July or 7 March? Depends who typed it and where they grew up.
THE ONE THAT ISN'T A PROBLEM

The notes column

"reorder soon!! last one" is a human running rule #2 in their head and writing the answer in the margin. That's not mess — that's the requirement, confirming itself.

Keep the notes column. Don't parse it. Just carry it along.

The instinct to resist: "let's make them clean up the file first." They won't. The file is how the business runs. Our code accepts reality; reality does not accept our code.
The deepest idea in this course

Events vs snapshots

A SNAPSHOT

"We have 350 sqm of MRB-80x80-POL today."

One number. Overwritten every Friday. It tells you where you are.

sku stock MRB-80x80-POL 350 TRC-30x30-TERR 640
A MOVEMENT LOG (EVENTS)

"+500 in on 2 May. −150 out on 9 May."

Many rows. Never overwritten, only appended. It tells you how you got here.

date sku type qty 2026-05-02 MRB-80x80-POL in 500 2026-05-09 MRB-80x80-POL out 150

Movements can rebuild any snapshot.
Snapshots can never rebuild movements.

Watch · the deepest idea in this course

The same idea, in the industry's own words

Open on YouTube ↗  ·  CodeOpinion
WHY WE STOP HERE

What we've called "log the movements, derive the snapshot" has a name in the industry: event sourcing. You just invented it from first principles because rule 3 forced you to. Hearing it named is worth five minutes.

HOLD THESE QUESTIONS WHILE YOU WATCH
  • Where does their "current state" come from?
  • What question can they answer that a snapshot database cannot?
  • What does it cost them? (There is always a cost.)
Careful: full event sourcing is a big, sometimes heavy pattern. We took the one idea we needed — append events, compute state — and left the rest. That restraint is the lesson, not the pattern.
The rule that decides the architecture

Rule 3 is impossible with snapshots

"Any SKU with no movement for more than 4 months."

Look at a snapshot. TRC-30x30-TERR: 640 sqm. Now answer the question: when did it last sell?

You can't. The number 640 is identical whether that tile sold yesterday or has been untouched since February. The snapshot destroyed the only fact the rule needs — when things happened.

Therefore: log movements, derive snapshots. Not because it's elegant. Because rule #3 does not exist otherwise.
RULE 1 · weekly movement

Snapshots? Impossible. Needs per-week in/out.

RULE 2 · low stock

Snapshots? Works fine. This is the only rule a snapshot can serve.

RULE 3 · dead stock

Snapshots? Impossible. Needs the date of the last outbound movement.

Two of three rules require events. The decision makes itself — but only if you check.
The whole data model. Two tables.

What we write down

movements

One row per thing that happened. Append only. Never edited.

date sku type qty_sqm note
2026-07-03 GRN-60x60-MATT in 500 inv 4471
2026-07-03 MRB-80x80-POL out 120 project
2026-07-10 SLT-40x40-BLK out 85 last one

sku_master

One row per product. Facts that don't change weekly.

sku name color is_bestseller low_stock_threshold
GRN-60x60-MATT Granite 60×60 Grey TRUE 200
MRB-80x80-POL Marble 80×80 Ivory TRUE 200
TRC-30x30-TERR Terrazzo 30×30 Sand FALSE 200
Where is "current stock"? Nowhere. It is not written down anywhere, because it is not a fact — it is an answer, computed from the movements whenever we need it. Storing it would mean keeping it correct forever.
Callback to Session 0

Every column earns its place

Column Which rule needs it Where the decision came from
movements.date Rules 1 and 3 Rule 3 is impossible without it. Stored as ISO YYYY-MM-DD, always.
movements.type Rules 1, 2, 3 One column, not two tick-boxes. Dead stock means no out — Session 0, ambiguity 2.
movements.qty_sqm Rules 1 and 2 Units in the column name, so nobody ever wonders. sqm, always.
movements.note No rule Carried, never parsed. It's where the humans put context, and one day we'll be glad it's there.
sku_master.is_bestseller Rule 2 Session 0, ambiguity 1. The manual flag. The owner ticks it. Our "cheap reversible default" is now a real column.
sku_master.low_stock_threshold Rule 2 Session 0, ambiguity 3. Per-SKU, defaulting to 200. One extra column bought us every future variation.

Two rows in this table are Session 0 decisions, landing as physical columns. That is what "architecture" was for.

Scope discipline

What we deliberately did not model

Prices

No rule needs money. The moment one does, add a column.

Suppliers

Interesting for reordering. Not required by any of the three rules.

Warehouse locations

The client has one shop. Multi-location is a fantasy about a company they don't have.

Units other than sqm

Boxes, pieces, pallets. All real. All irrelevant, because the client thinks in sqm.

The rule: model exactly what the three rules need. Nothing more. Every speculative column is a promise you'll have to keep filling in, forever, for a feature nobody asked for.
And when they ask for prices next year? You add a column. It takes an hour. That is the entire cost of being wrong — which is why guessing in advance was never worth it.
Meet the cast — they appear in every example from here on

Five tiles, five stories

SKU What it is Its situation on Friday 10 July 2026 Its role in this course
GRN-60x60-MATT Granite look, 60×60, matte, grey. Bestseller. 1,240 sqm in stock. Sells every week. Boring, healthy. The control. Nothing should ever fire for this tile.
MRB-80x80-POL Marble look, 80×80, polished, ivory. Bestseller. 180 sqm in stock, threshold 200. Triggers rule 2. The alert that should fire.
TRC-30x30-TERR Terrazzo, 30×30, sand. Not a bestseller. 640 sqm sitting there. Last sold 6 Feb 2026 — 154 days ago. Triggers rule 3. Real dead stock. Real money on a shelf.
WDX-20x120-OAK Wood-look plank, 20×120, oak. New product. Added 11 May 2026 — 60 days old. 900 sqm. Never sold. Not yet. The trap. Rule 3 will accuse it of being dead. It's a newborn.
SLT-40x40-BLK Slate, 40×40, black. Specialty. Movements imply −35 sqm. Negative stock. Physically impossible. The data error. Reality is broken and the app must cope.
Hands-on · everyone types this

Generate data that fights back

Paste into Claude Code
Create create_sample_data.py that writes movements.csv and sku_master.csv.

movements.csv columns: date, sku, type, qty_sqm, note
  - dates are ISO (YYYY-MM-DD), spanning 12 months ending 2026-07-10
  - type is exactly "in" or "out"
  - about 30 SKUs, roughly 1,800 rows total, realistic weekly rhythm

sku_master.csv columns: sku, name, color, is_bestseller, low_stock_threshold
  - low_stock_threshold defaults to 200

Include these five specific SKUs with these exact situations:
  1. GRN-60x60-MATT  bestseller, healthy, ends near 1,240 sqm
  2. MRB-80x80-POL   bestseller, ends at exactly 180 sqm
  3. TRC-30x30-TERR  not a bestseller, 640 sqm, last "out" on 2026-02-06
  4. WDX-20x120-OAK  first movement 2026-05-11, only "in", never sold
  5. SLT-40x40-BLK   movements sum to -35 sqm (a real data-entry error)

Then run it and show me the last 10 rows of movements.csv.
READ THE PROMPT AGAIN

Context. Exact column names. Exact date format. Exact scope. And five test cases specified by their outcome, not their implementation.

This is the Session 0 "good prompt" slide, in the wild.

Notice what we did not say: we never told Claude how to make the numbers add to −35. We described the world we want and let it find the arithmetic.
Say this out loud to the room

We just planted the bugs we'll need later.

WHY

Testing with clean data teaches nothing. Clean data proves your code runs. Messy data proves your code is right — and, more often, proves that you never decided what "right" meant.

Both bugs we planted are real bugs from real businesses. Neither is a trick.

THE TWO LANDMINES

SLT-40x40-BLK — negative stock. Someone sold tiles they never recorded receiving.

WDX-20x120-OAK — a two-month-old product with no sales. Innocent. About to be accused of being dead.

The professional habit: when you write test data, write the awkward cases first. The happy path is the one case you'll never get wrong.
Chapter 1.3

Build the Three Rules

Each rule is a mini case study. Each one goes wrong on purpose.

The shape of the next two hours

Six steps, three times

1 · State the rule in business language
2 · Translate to precise language
3 · Prompt Claude
4 · Run it
5 · Find the bug
6 · Refine
STEP 2 IS THE JOB

"No movement for 4 months" → "no row with type='out' whose date is within 122 days of today." Nobody but you can do this translation, because only you know the business.

STEP 5 IS NOT OPTIONAL

Every rule below has a failure moment, printed on a slide. If your version doesn't fail, your data is too clean — go back and dirty it.

Watch · before we prompt for real

Prompting for code, watched over someone's shoulder

Open on YouTube ↗  ·  Frank Nillard
WHY WE STOP HERE

We are about to write three prompts that must be exactly right, because a vague prompt produces confident, plausible, wrong stock rules. Watch how much of good prompting is just stating the context nobody told the machine.

HOLD THESE QUESTIONS WHILE YOU WATCH
  • How much of their prompt is context, versus instruction?
  • Do they say what not to do? (Our prompts always do.)
  • Where does project memory live?
Provenance matters: this is a community video summarising Anthropic's published guidance, not Anthropic itself. Useful, and worth saying out loud — you're teaching people to check sources as well as prompts.
Rule 1 · step 1

Weekly stock movement

The client said:

"Which SKU came in / went out and how much, each week."

STEP 2 · TRANSLATE

For each week, for each SKU that had any movement: sum of in, sum of out, and the net difference.

One word in that sentence is a landmine.
"week"
Rule 1 · the ambiguity

What, exactly, is a "week"?

THE ISO DEFAULT

Monday → Sunday

What every date library gives you for free. What Claude will use unless told otherwise. What standards bodies agree on.

THE AMERICAN DEFAULT

Sunday → Saturday

What half the spreadsheet software on earth assumes. Equally defensible. Equally arbitrary.

THE CLIENT'S ACTUAL WEEK

Saturday → Friday

Because someone updates the Excel file every Friday. A "week" for this business is everything since the last time I opened this file.

Business context beats the technical default. If we used Mon–Sun, every Friday's report would omit Friday itself — the very day the file gets updated — and nobody would notice for a month.

This decision goes straight into CLAUDE.md. It will never be re-litigated.

Rule 1 · steps 3 & 4

Prompt, and result

Paste into Claude Code
Create rules.py. Read movements.csv and sku_master.csv.

Rule 1 — weekly movement summary:
For each week, for each SKU with any movement,
print total in, total out, and net.

A week runs SATURDAY to FRIDAY, because the
client updates their file every Friday. Do not
use the ISO Monday-Sunday week.

Print the most recent week only, as a table.
Don't write any files yet.

Note the last line. An explicit non-goal, every time.

$ python rules.py WEEK OF Sat 2026-07-04 → Fri 2026-07-10 sku in out net ────────────────────────────────────────── GRN-60x60-MATT 500 1,240 -740 MRB-80x80-POL 0 120 -120 SLT-40x40-BLK 0 85 -85 WDX-20x120-OAK 300 0 +300 ────────────────────────────────────────── 4 SKUs moved this week.
Rule 1 works. Nothing broke. Enjoy it — it's the last time today.
Rule 2 · steps 1 & 2

Low stock — and the number that doesn't exist

The client said:

"For best-selling SKUs, if remaining stock drops below 200 sq.m., alert so we can reorder."

Read it again. "Remaining stock." Where is that number?
IT IS NOWHERE

Look at our two tables. There is no stock column. There never was. We chose movements over snapshots, so current stock isn't stored — by design.

SO WE DERIVE IT
stock[sku] = sum(qty where type == "in")
          - sum(qty where type == "out")

One line. Recomputed every run. Never stale, never wrong, never needs fixing.

And rule 2 needs two more things from sku_master: is_bestseller and that SKU's own low_stock_threshold. The Session 0 decisions, doing their job.
Rule 2 · step 3

The prompt

Paste into Claude Code
Add Rule 2 to rules.py — low stock.

Current stock is not stored. Derive it per SKU:
sum of "in" quantities minus sum of "out"
quantities, across all of movements.csv.

Then, for every SKU where is_bestseller is TRUE
and derived stock is below that SKU's own
low_stock_threshold (from sku_master.csv,
default 200), print a LOW STOCK line showing
the SKU, its stock, and its threshold.

Print the derived stock for all five of our
demo SKUs too, so we can sanity-check it.
THE LAST PARAGRAPH IS THE IMPORTANT ONE

"Print the derived stock for all five, so we can sanity-check it."

We didn't ask for the answer. We asked to see the working. Rule 2's alert would have been correct and useless without it.

Habit worth stealing: whenever a program computes a number you can't see, ask it to show the number. Always. Especially when it's about to make a decision on your behalf.
Rule 2 · step 5 · the failure moment

It ran. And something is wrong.

$ python rules.py DERIVED STOCK GRN-60x60-MATT 1,240 sqm MRB-80x80-POL 180 sqm TRC-30x30-TERR 640 sqm WDX-20x120-OAK 900 sqm SLT-40x40-BLK -35 sqm ← ??? LOW STOCK (1) MRB-80x80-POL 180 sqm (threshold 200)
THE GOOD NEWS

Rule 2 fired correctly. MRB-80x80-POL is a bestseller at 180 sqm against a threshold of 200. Exactly the alert the client asked for.

THE BAD NEWS

SLT-40x40-BLK has −35 sqm of stock.

Negative square metres of tile. In the physical universe. In a warehouse.

So — is this a bug in our code, or in their data?
Rule 2 · the diagnosis

It's their data. Now what?

Someone sold 85 sqm of black slate and never recorded the delivery that brought it in. The tiles exist. The row doesn't. Our arithmetic is perfect and our answer is impossible.

OPTION A

Crash

Stop the program. "Negative stock detected — aborting."

No. One bad row on a Friday night now means zero alerts sent, and MRB-80x80-POL quietly runs out.

OPTION B

Silently fix

Clamp it to zero. max(0, stock). Move on. Nobody's upset.

Absolutely not. Now the error is invisible and permanent. The missing delivery is never found. You have hidden a real-world problem inside a max() function.

OPTION C

Report it

Compute everything you can. Send every alert you can. Then add a data-quality warning to the bottom of the message.

Yes. The program keeps working. The human learns something is wrong. Both problems get solved by the person able to solve them.

The principle, generalised

Apps that touch reality need a "reality is broken" lane.

The refinement prompt
The -35 sqm for SLT-40x40-BLK is a real data-entry
error in the client's records, not a bug in our code.

Don't crash and don't clamp it to zero. Instead:
collect any SKU whose derived stock is negative into
a list of DATA QUALITY WARNINGS, print them in their
own section at the end, and keep all other rules
running normally.
$ python rules.py LOW STOCK (1) MRB-80x80-POL 180 sqm (threshold 200) ⚠ DATA QUALITY WARNINGS (1) SLT-40x40-BLK has -35 sqm. A movement row is probably missing. Someone should check the invoices.
Every serious system has this lane. Your bank calls it a suspense account. Accountants call it a reconciliation. We call it a warning at the bottom of an email — and it is the same idea, doing the same job.
Rule 3 · steps 1 & 2

Dead stock — "no movement for 4 months"

The client said:

"Any color/SKU with no movement for more than 4 months, alert."

SETTLED IN SESSION 0

"Movement" means outbound. Dead stock is stock that doesn't sell. Restocking a tile nobody wants makes it more dead, not less.

STILL AMBIGUOUS · from when, to when?

From the last outbound movement to today. Not to the end of the file, not to the last row — to today, the day the alert runs.

And "4 months" is? We'll say 122 days. Precise, boring, and it doesn't drift when February shows up.
Rule 3 · step 3

The prompt

Paste into Claude Code
Add Rule 3 to rules.py — dead stock.

For each SKU, find the date of its most recent
movement where type == "out".

If that date is more than 122 days before today
(2026-07-10 for our test data), print a DEAD STOCK
line with the SKU, the days since it last sold,
and its current derived stock.

Sort by days-since-last-sale, worst first.

Everything in this prompt is correct. Everything the client asked for is here. It is about to be badly wrong.

WHAT WE EXPECT

One line. TRC-30x30-TERR, last sold 6 February, 154 days ago, 640 sqm of sand-coloured terrazzo gathering dust.

Before you turn the slide, look back at the cast of five SKUs and ask yourself which other tile has never had an outbound movement.
Rule 3 · step 5 · the failure moment

The false alert

$ python rules.py DEAD STOCK (2) TRC-30x30-TERR 154 days 640 sqm WDX-20x120-OAK ∞ days 900 sqm ← never sold
The code is correct. WDX-20x120-OAK has genuinely never had an outbound movement. Claude did exactly what we asked. The rule, as written, is what's wrong.
WHAT WE JUST TOLD THE CLIENT

"Your brand-new oak plank range, launched eight weeks ago, is dead stock. Discount it."

The client's trust in the alert dies at exactly this moment — and it never fully comes back.

WHY THIS SLIDE EXISTS

Nobody thinks of this until the false alert fires. Not you, not Claude, not the client. It is not a failure of intelligence. It is a failure that only reality can teach.

Rule 3 · step 6 · refine

The missing guard

The refinement prompt
WDX-20x120-OAK is a false positive. It has never
sold because it launched 60 days ago, not because
it's dead.

Add a guard: a SKU can only be dead stock if it
has EXISTED for at least 122 days. Treat a SKU's
first movement date (of any type) as its birthday.

A SKU younger than 122 days is never dead stock,
no matter what. If it's older than 122 days and
has never sold at all, that IS dead stock.
$ python rules.py DEAD STOCK (1) TRC-30x30-TERR 154 days 640 sqm skipped: WDX-20x120-OAK (only 60 days old)
Now the rule is: no outbound movement for > 122 days AND the SKU is at least 122 days old.
Note the last line of the output. We asked it to say what it skipped. Silence is not the same as correctness — a rule that quietly excludes things is a rule you can't debug.
What just happened, and why it will happen again

The requirement was never wrong. It was incomplete.

THE CLIENT

Said "no movement for 4 months" and meant it sincerely. They were not thinking about products launched last May, because when they wrote the sentence, they were picturing the terrazzo.

CLAUDE

Implemented the sentence perfectly. It had no way to know that a SKU can be young. The requirement it was given did not contain that concept.

YOU

Ran it against real data, saw an oak plank accused of being dead, and flinched. That flinch is the job. That flinch is not automatable.

Every alert rule you will ever write has one of these. A boundary condition that only appears when the rule meets a world it wasn't imagined against. The only reliable way to find them is to run the thing on real data and look at the output like you mean it.
Resist the urge

Output for today = your screen, plus one CSV

Someone in the room wants to connect Google Sheets right now. Someone else has already opened the docs for sending email.

No. The logic core works, on data that fought back, and it is testable in one second by one person with no credentials. That is a finished thing.

Session 0, the walking skeleton: build the thinnest possible end-to-end version, then thicken. We are exactly on plan. Sheets and email are thickening.
The last prompt of Session 1
Also write the three sections (weekly summary,
low stock, dead stock, data-quality warnings)
to a file called weekly_report.csv, with a
"section" column so all four fit in one file.

Keep printing to the screen as well.
WHY A CSV AND NOT A SHEET

Because a CSV needs no credentials, no internet, and no permission from Google. When we swap it for a real sheet next session, only the last four lines of the script change.

Chapter 1.4

Session 1 Close

The skeleton walks

What we have

⏱ still a human, still on Friday
1 · DataGoogle Sheet
still a CSV on your laptop
connectionSheets API
next session
2 · LogicPython: 3 rules
✓ working, ✓ tested on bad data
3 · HumanEmail / LINE
next session
↩ weekly_report.csv — on your laptop, for now
RULE 1

Weekly summary. Sat→Fri, because the client's Friday habit beat the ISO standard.

RULE 2

Low stock, derived not stored — and a data-quality lane for when reality is broken.

RULE 3

Dead stock, with the age guard we only found by watching it accuse an innocent tile.

Everyone does this before leaving the room

Start your CLAUDE.md

Today you made six decisions. Write them down where Claude will read them every single session, forever.

Paste into Claude Code
Create CLAUDE.md capturing the decisions we made
today, so you never have to ask me again:

- A week runs Saturday to Friday (client updates
  their file on Fridays).
- Dead stock = no OUTBOUND movement for more than
  122 days AND the SKU is at least 122 days old.
- Low-stock thresholds are per-SKU, default 200 sqm.
- Current stock is derived from movements, never
  stored.
- Bad data is reported as a warning, never crashed
  on and never silently fixed.
- All quantities are square metres (sqm).
The decisions are the project.
The code is regenerable.
TEST IT TOMORROW

Delete rules.py. Open Claude Code. Say "rebuild rules.py from CLAUDE.md."

You'll get your program back. You would not get today's arguments back — which is why they're the part you wrote down.

Next session

Next time, the sheet talks to the script
and your phone buzzes.

CONNECT

Google Sheets, for real. The credential ceremony. And the thing the client actually asked — "how do we connect Claude to Google Sheets?" — turns out to have two different answers.

ALERT

Email that a tired human can act on at 18:05 on a Friday. Then LINE, where the team actually lives.

AUTOMATE

A schedule that fires whether or not anyone remembered. And a cron expression that lies to you about what time it is.

Bring your laptop, your Google account, and the rules.py you built today.

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.