Automated Shopify markdowns without giving away margin: AI writes the code, a rule sets the price
4 promotions in 5 weeks, 0 errors when applying and a rollback rehearsed 9,440 out of 9,440 on Shopify Plus. The rule, the controls and what we learned.
AI, digital marketing and medical marketing consultant · btodigital
Between 28 August and 4 October 2026 we automated four price promotions on the Shopify store of a Colombian fashion brand that sells online and in its own physical stores, and not one of them hit an error when applying prices. The largest covered up to 9,440 size variants, and before switching it on we rehearsed the full rollback: 9,440 out of 9,440.
This is for e-commerce managers and brand owners who sell on Shopify both online and in their own stores. The core idea fits in one line: AI wrote and monitored the code, but the price is set by a rule the client approves in writing. No AI decides prices in production.
What happened across the four promotions?
| Date | Channel | Scope | Result |
|---|---|---|---|
| 28 August | Online only | Sale items at 50% off | 1,455 variants went down; 1,579 were already at 50% off or more and were left alone |
| 4 September | Online and stores | Ageing-stock clearance: 372 products at 50, 40 or 30% off | Applied in both channels |
| 30 September | Online only | 50% off across 7,604 variants | 1,881 went down; 5,723 untouched thanks to the ceiling model |
| 2 to 4 October | Physical stores only | 50% off | Plan of up to 9,440 variants |
All four applied with 0 errors. And in every case the promotion was scheduled within hours of the client’s approval.
Look at the third row. Out of 7,604 variants, the system moved only 1,881. The other 5,723 were already at half price or cheaper. A badly written rule would have discounted them again.
Where does a markdown give away margin?
Almost always in two places.
First: applying the percentage to a price that is already discounted. If a garment lists at COP 74,900 and the stores already have it at 30% off, COP 52,430, a “50% off” calculated on that price takes it to COP 26,215. That is 65% off the original, not 50%. Nobody decided that. A formula did.
| Calculation | Price (COP) | Real discount on the original |
|---|---|---|
| Original price (struck through) | 74,900 | 0% |
| Current store price | 52,430 | 30% |
| 50% off the current price | 26,215 | 65% |
| 50% off the original (our rule) | 37,450 | 50% |
The 74,900, 52,430 and 37,450 figures come from a real garment in the 30 September promotion, name removed. The 26,215 is what the naive formula would have produced.
Second: one channel’s discount leaking into the other, or lasting longer than agreed. An online-only sale that also cuts store prices, or a weekend promotion nobody switches off on Monday.
Our rule tackles both:
- The target is half the regular, struck-through price, never half the current price.
- Ceiling model: only lower what sits above the target. Anything already cheaper is left alone, and nothing is ever raised.
- Write only to the price list of the channel running the promotion.
- Start and end times are fixed, and the rollback is scheduled from day one.
How do you separate online and store prices in Shopify?
This store runs on Shopify Plus, and its website and physical stores read different price lists through Markets and Catalogs. Each variant has a fixed price on the online list and another on the store list.
Before the first promotion we tested it live: we changed the price of a single variant on the online list and the website showed it instantly, while the store list and the base price stayed untouched. We reverted it on the spot. In October we ran the reverse test before the store-only promotion.
The practical consequence: an online-only sale means writing to the online price list and nothing else. No need to touch the base price or freeze anything.
How is the automation built?
| Component | What it does |
|---|---|
| Frozen plan | File with every variant, its target price and its previous price as a backup |
| Apply-and-repair Cloud Run Job | Applies the plan and, on every run, repairs only what drifted above the target |
| Rollback Cloud Run Job | Restores each variant’s previous price from the backup |
| Cloud Scheduler | Triggers the periodic repair and the rollback at the agreed time |
| Start and end gates | Before the start or after the end, the job does nothing |
| Alert emails | Only when something happens: launch, a large repair, or an error |
| Webhook guard | In the first promotion, instantly corrected any edited product that ended up above 50% |
The repair frequency changed as we learned: it started every three hours and, by the store promotion, ran every 10 minutes.
What controls run before any price changes?
- Written approval of scope, percentage and dates.
- CSV preview for the client: which variant goes down, from what to what.
- Mandatory dry run: the job runs in test mode and reports what it would do, writing nothing.
- Frozen plan with a backup of every variant’s previous price.
- Start and end gates in the code itself, not just in the scheduler.
- Post-apply verification: after applying, the job reads the prices back and confirms they landed where they should.
- The repairer stops before the rollback. Otherwise the rollback restores prices at 9:00 and the next repair run marks them down again. So the repairer stops acting 15 minutes before the rollback.
- Rehearsed rollback: for the store promotion, the rollback dry run restored 9,440 out of 9,440 variants.
One detail that takes a while to learn: Colombia is on UTC-5 and servers think in UTC. The gates are written in UTC (Colombian time plus five hours) and the scheduler is set to Bogotá time. We checked the conversion independently every time.
What did the AI do, and what did it not do?
Claude Code designed the architecture, wrote the code, built the plans by reading live prices, ran the dry runs and monitored the promotions. When something moved, it spotted it and proposed the fix.
What it did not do: choose the percentage, the scope or the dates. The client’s marketing team decides that, in writing. In production, each variant’s price comes from a fixed, reviewable formula, half the regular price with a ceiling, not from a model with opinions. My position: if an AI ever proposes prices, fine, let it propose. A rule someone signed off should apply them.
What we learned
- Watch the outcome, not the cause. What counts is the price the shopper sees, not the cause you think you know. That is what the periodic repairer and the webhook guard are for.
- A hand-curated sale collection is not a reliable source of truth. The store plan grew from 7,604 to 9,440 variants as we found markdowns outside the collection and the client approved them. We recommended turning it into an automated collection with a “has a compare-at price” rule.
- At the till, an item already scanned keeps the old price. If it was scanned before the change, staff have to remove it and scan it again. You cannot see that through the API, which is why we asked the client for a test purchase in store.
- More instances do not buy speed when the API is the limit. In the same store’s inventory sync we scaled the service from 1 to 10 instances to absorb a spike, and our monitor started hitting the limit of Shopify’s API, which accepts a fixed number of calls per second per store. We went back to one instance and made the monitor tolerant of those temporary rejections.
Does this work outside fashion?
Two smaller cases from other projects say the same thing: validate before you load.
- For a home décor store’s outlet, the client sent 180 SKUs to mark down. We applied 144, with 0 errors. The other 36 did not exist in the store. Without validating against the catalogue, nobody would have known that one in five SKUs on the list pointed at nothing.
- In a product import, 4 prices came in multiplied by 1,000 because of a typo. A price like that does not get quietly fixed after it goes live: you catch it by checking ranges before loading.
It is the same logic we followed when automating maintenance for 24 WordPress sites: automation does not replace verification, it makes it mandatory.
How do you apply this to your store?
- Check how your channels read prices. If online and stores share one price, an online-only sale is impossible until you separate them.
- Define the regular reference price, the struck-through one, and calculate the discount from it, never from the current price.
- Use a ceiling: lower what is above the target and leave anything cheaper alone.
- Freeze the plan with every variant’s previous price before applying.
- Require a dry run and a preview the commercial team can read and approve in writing.
- Put start and end times in the code, and schedule the rollback from day one.
- Stop the repairer before rolling back.
- Rehearse the full rollback before switching on.
- Watch the price the shopper sees, not just what you wrote.
- Make a test purchase at the till: it is the one thing the API cannot show you.
A well-applied markdown is also a trust issue: a shopper who sees one price online and another at the till does not come back the same. I covered that in why people visit and do not buy.
Frequently asked questions
Can you run a Shopify sale online only without touching physical stores? Yes, if the website and the stores read different price lists, as in this store, which runs on Shopify Plus with Markets and Catalogs. You write to the online list only. Before each type of promotion we tested it live on a single variant.
What is the ceiling model? A rule that only lowers prices sitting above the target and never raises those that are already cheaper. In the 30 September promotion, out of 7,604 variants only 1,881 went down; the other 5,723 were already at half price or less.
Does the AI decide the prices? No. Claude Code designed, built and monitored the system, but the client decides percentage, scope and dates in writing, and each variant’s price comes from a fixed formula: half the regular, struck-through price.
How do you roll back an automated promotion? With each variant’s previous price, backed up in the plan before applying. A rollback job restores it at the agreed time, after the repairer has been stopped. For the store promotion, the rehearsal restored 9,440 out of 9,440 variants.
What happens to items that were already discounted? If they are already at the promotion’s percentage or lower, they are left alone. That is how you avoid stacking discounts and giving away margin nobody decided to give.
What can the system not verify? The physical till. Through the API you can confirm the store price list is right, but an item scanned before the change keeps the old price. That is why we ask for a test purchase in store.
If you want to run promotions like this in your store, it is part of the work I do as a digital marketing consultant.