Shopify orders land in the warehouse sheets by themselves, with formulas and manual edits intact
Orders from two Shopify stores go straight into the warehouse team's working Google Sheets: order numbers, tracking numbers, statuses and cancellations are filled in every 30 minutes. Daily checks against the stores, backups and automatic recovery of lost rows run alongside.
Every paid order was copied into the sheet by hand
A US outdoor-gear brand sells through two Shopify stores, and the goods ship from China. All the logistics (shipments, warehouses, tracking numbers) lived in two large Google Sheets kept by two operations staff. They copied every paid order into them by hand: order number, product, shipping warehouse, date, tracking number.
This took hours every day, and the sheet was always behind. That led to the usual errors: orders went missing, cancelled ones were still listed as active, and tracking numbers never reached their row. A mistake came to light only after someone had already made a decision based on the sheet.
Why hadn't anyone automated this before? Here the sheet is a living tool that people work in: manual edits right in the cells, formulas, conditional formatting, dropdown lists and a row order built up over time. Simply adding a row at the bottom breaks that order and makes the sheet awkward for the people who use it.
Two parts: Python fetches the orders, Apps Script writes them carefully
The Python part decides what to write. It fetches paid orders from both stores through the Shopify Admin API, brings the data into one format and assembles a batch of rows. The Google Apps Script part (code that runs inside the sheet itself) decides how to write them without getting in people's way. That turned out to be the hardest part.
Paid orders from two stores: every 30 minutes only the new ones are fetched
Product name lists, the shipping warehouse, cancellation and partial-refund statuses
What to insert, what to update and what to mark as cancelled is decided before anything is written
The row is inserted in the middle of the sheet, and the formulas and formatting of neighbouring rows are copied down
The operator sees the order in its place, with the row order and manual edits intact
Python brings product names into line
Two stores, two sheets, and the same product is named differently in each, a habit built up over years. So the product name lists are kept separately for each sheet. Each sheet has its own dictionary, and it can be edited without changing the code. The same step works out the shipping warehouse and sorts out the tricky statuses: a partial refund and a cancellation are different states, and they look different in the sheet.
Moving from SpreadsheetApp to Advanced Sheets Service
The first receiver was written with the familiar
SpreadsheetApp,
the standard way to work with a sheet from Apps Script. On sheets this size it kept running out
of memory. I rewrote the receiver on
Advanced Sheets Service:
it now sends batched requests to the Sheets API and doesn't go through cells one by one. The
receiver has reached
version 87, and almost every new
version dealt with another quirk of Google Sheets.
Inserting rows mid-sheet and copying formulas down
An order has to land in its own place, where the person expects it according to the row order they've built up. When a row is inserted in the middle, it arrives without formulas. The receiver copies the formulas down itself and marks cancelled orders with a colour through conditional formatting. It also adds a reserve of empty rows in advance, so the next run never hits the end of the sheet.
The cancellation colouring has a story of its own. The conditional formatting rule was in the sheet but never once fired: the main module wasn't passing the cancellation mark to the receiver, so there was nothing to colour. I measured how far this went on the stock ledger sheet: across 16,478 rows the mark was missing in exactly one. Now the mark gets through, and cancelled orders really are highlighted.
Four safeguards that keep the bot away from people's work
Getting data into a working sheet is only half the job. The other half is making sure the bot leaves people's work alone. For that there are four independent safeguards, and each closes off one way of doing damage:
- Write only over a formula. Shipping cost is written only into a cell whose original formula is untouched. A cell where someone typed a value by hand is left alone.
- Check the target rows before writing. If there's anything at all in them, the whole write is cancelled. A refusal with the row numbers goes to the log and the chat, and the orders are added on the next run. This came from a real case: a second operator typed a product into a row with no order number, and 13 minutes later the bot wrote its own order into it.
- Insert at the end of the first unbroken block of rows. One row out of place used to drag every following order down with it. Now the shift stays where the error is.
- Refuse an unknown command. An unknown command name used to fall through to the branch that adds rows. A typo would have written row numbers into the sheet as orders while the receiver answered "success". Before switching this safeguard on, I checked all 19 commands the main module sends.
The check after the release took three requests: a command with a typo returns an error, an empty add answers "0 added", and a real command runs as usual.
The other direction: checking the sheet against the stores, read-only
The same stock ledger sheet is now checked against the stores every day. The export is strictly read-only, and two tests on the module's code make sure of it: the module has no function that writes. For a system that writes into people's working sheets, this follows naturally from the safe-writing rules. A check on live data: 701 rows, with differences within the threshold.
Merging overlapping data-validation rules
A separate discovery: when rows are inserted, the rules behind the dropdown lists multiply and start to overlap. Once enough of them pile up, the sheet stops opening. The receiver merges the overlapping rule ranges into one, and the sheet stays usable.
Eight timers around the exchange
The exchange runs unattended, so it's surrounded by scheduled jobs on systemd timers:
- Writing orders: every 30 minutes, new and changed orders
- The day's file for the partner: a check every 5 minutes; the wait for the file went from about 50 minutes to 15
- Files from the shipping partner: every 2 minutes from 07:00 to 22:00 Moscow time, picked up from both the group topic and direct messages
- Rebuilding the product-name list: about 187 successful builds a day
- Health check: is the system running, and are the writes getting through
- Daily check: the sheet compared with Shopify row by row
- Nightly backup: a snapshot of the sheets before the next day's writes
- Receiver tests: run against the real environment
A note on the faster day's file: besides the check every five minutes, there's now a chat command that builds the file straight away. The read load was measured in advance: about 380 requests a day, well within the limits. The whole system for this project runs on a server that currently has 58 timers, with a daily check of their state.
On top of that there are Telegram notifications to the operator, retries when Apps Script fails, and a separate test that fails if the business-rules reference drifts away from the code. The last one sounds minor, but it's exactly what keeps the written rules in line with what the code really does.
Automatic recovery that gives way to people
The recovery module could put lost rows back into the sheet. It also put back rows that a person had deleted on purpose. Replaying it on real snapshots of the sheets showed 67 such orders out of 80 restores in one day: in 84% of cases the bot was overruling a person.
Now the bot keeps a register of the base order numbers it has already seen. The register was built from 14 nightly snapshots, holds 7,696 numbers and is topped up with every check. Before putting a row back, the bot can tell a row a person deleted from one that failed to be written. If the register is empty, the bot assumes it hasn't seen the number, so even a lost register file won't switch recovery off.
Replaying the same data with the register in place: 67 restores would not have happened, and all 13 genuinely missed writes would still have been restored. The safety net stayed fully in place, and the person's decision comes first.
What checking the watchdogs turned up
Watchdog scripts are only worth something once you've confirmed that their warning reaches a person. The check turned up three findings.
- When one of the two stores failed, the count of products sold beyond what's in stock quietly dropped from 9 to 3. The figure looked like good news, even though stock is ordered based on it. A product listed only in the store that was down dropped out of the count entirely.
- The watchdog for stuck orders on the work computer wasn't working: when run on a schedule, it had no access to the ssh agent with the keys. Meanwhile it reported "the source returned no orders, staying quiet", in the very words a healthy watchdog uses when there's nothing to report. Run by hand, the same code found real stuck orders, 5 and 11 days old. The problem went away once the watchdog moved to the server.
- The check of the warehouse sheets ran faithfully every four hours and wrote a file that nobody ever opened.
What the first months of work showed
The system is built and running. Over this time there were three failures worth describing separately, because each of them was easy to miss.
The daily sync reported a failure after a successful run.
The output of the deploy command was piped into a text search. With
pipefail
switched on, the exit code comes from the last failed command in the chain. An empty search
returns exit code 1 exactly when the system says "no changes". So the calmer the day, the more surely
the alarm went off. The false alarm fired for several days in a row, and people stopped reading
it. The fix: the exit code is taken from the deploy command itself, the output is written to a
file and read afterwards, the "no changes" case is logged in words and counted as a success, and
a real failure puts the end of the output into the log.
The nightly backup failed every night on the steps that dump the databases of a service that had been paused. A real backup failure would have been lost among these daily errors. Now these steps are skipped if the containers aren't there, and nothing will need editing when the service is switched back on.
Deploying by a list of checksums. The code that writes into the working sheets had no deploy command of its own. It was copied by hand, and an old version missing one of the fixes sat unnoticed on the server for two weeks. Now there is such a command: rsync without deleting files (the environment settings, daily files and manual backups live next to the code), its own list file and 62 files checked against their checksums. Before that, I checked each file to see which way it differed from the repository. No file had lines that were on the server and missing from the repository; otherwise a deploy would have quietly wiped out a manual edit. On the next deploy, the comparison before upload showed 107 matching files.
Switching to the working sheets through parallel writing
Handing a robot a sheet that two people rely on every day is frightening, and rightly so. So the switch was made in parallel: for a while the bot wrote into the working sheets at the same time as into reference copies. The daily check caught any differences, and rolling back meant switching off a single timer. There was never a big switch-on moment.
Nothing unusual: the hard part is how Google Sheets behaves
Fetches paid orders from both stores; requests go through urllib with no extra dependencies
Writes to the sheet: inserting mid-sheet, copying formulas and formatting down
Reads the sheet for the checks and the export, applies batched edits
Eight schedules, protection against overlapping runs, alerts on failure
Notifications to the operator: what was written, what differed, what failed
1,362 tests in the reporting folder (up from 1,249) and 2,472 across the whole repository, including a check that the business-rules reference matches the code
Before and after, side by side
how often orders are written automatically, for both stores
711 problem cells before the bot went live, and exactly 711 after
timers: writing orders, the day's file, incoming files, the name list, health check, daily check, backup, tests
Nobody has to copy orders into the sheets by hand any more. Paid orders from both stores appear in the working sheets by themselves, with the tracking number, warehouse, status and a cancellation mark if the order is cancelled.
The "zero new errors" figure needs explaining. Before launch I checked the whole sheet: 711 problem cells were left over from years of manual work. After the bot went live, the same check returned exactly the same 711. The automation added not a single new error to the working document, even though it wrote into it every half hour.
The daily check against Shopify finds differences by itself and lists them: in one run, 28 differences were marked as resolved. A person only has to work through a ready list.
Three recent checks that the operator's decisions reach the sheet exactly as they were made.
- The name list became the single source of product names. A name the operator set in the list was used in the shipments sheet. In the stock ledger sheet, though, the long original name from the store quietly stayed: the automatic name fix there had replaced 0 cells, which means it had never fired. The 260-row list gives 244 matches. After the fix, both rows that were in use carry the intended name, and nothing was lost.
- Spotting "one product in the sheet, another in the store". The sign is a product that appears only in the sheet or only in the store, with names matched through the list. A run on live data found exactly 2 cases across 710 active orders and 7,680 base numbers, with no false alarms. The mark is a character in a hidden column plus a conditional formatting rule, because a plain fill gets wiped out when an inserted row copies its neighbour's formatting.
- Dropdown colours. A colour is tied to an exact value, so a one-letter difference switches it off. There were 28 grey cells out of 12,239, and 20 of them differed by exactly one letter. The bot now writes the value exactly as it appears in the sheet's own lists, and the lookup ignores case, look-alike Cyrillic and Latin letters and extra spaces. Along the way, 39 empty cream marks out of 73 were cleared, so the highlighting means something again.
"Everything through Shopify — much faster and more convenient, it's become far easier."
The operations staff member who joined second, after the first was already working with the live sheets.
Timeline: about five weeks from the first working version to writing into the working sheets. Fetching orders from Shopify can be done in a day. Most of the time went into making it safe to write into a sheet that people work in.
Where else the same approach works
Behind this case is a common task: "data from a business system has to get into a spreadsheet that people work in". It comes up often, and simply adding rows at the bottom of the sheet breaks the way people work:
- → Marketplaces (Amazon, Wildberries, Ozon) → the purchasing and supply sheets that managers keep
- → CRM and ERP systems → the owner's summary sheet, full of manual comments and home-made formulas
- → Logistics and shipment tracking: carrier statuses into the shipment register with no copy and paste
- → Payment systems → financial registers, where exact reconciliation is what matters most
- → Any spreadsheet that someone has kept for years and that can't simply be swapped for a ready-made report
- Writing through Apps Script: inserting mid-sheet, copying formulas down and merging data-validation rules
- Product name lists for each sheet that the operator edits directly, with no code release
- A daily check against the main system that produces a ready list of differences
- Switching over through parallel writing: the working sheet and a reference copy at the same time, and rolling back means switching off one timer
- A test that fails when the business-rules reference drifts away from the code, so the written rules can't quietly go out of date
Work updated in October 2026
In October 2026 I refined shipping-cost imports and readback of saved results. Each write is tied to the order and product; an occupied cell or a manual edit needs a separate decision.
The checks cover spreadsheet versions and calculation rules. Similarly named product variants remain separate, and saved values are read back. A source-read error remains visible and is not converted into a zero.
Do your people move data from a system into a spreadsheet by hand? That can be automated
The spreadsheet stays as it is: the row order and formulas survive, and the data arrives by itself. We start with one sheet and one source, and adding more after that is inexpensive.
Blog article on this topic: Inventory sync between 1C, Wildberries and Ozon: choosing how often to sync, and everything around the sync itself
Related cases
File exchange with a shipping partner: costs and tracking numbers come back into the sheet
The daily file builds itself and looks just like the working sheet, row colours included. The partner's reply…
Reconciling revenue across three sources: an 8.4% gap explained down to 0.33%
The owner's working sheet, the warehouse book and the CRM each showed a different revenue figure. A one-off…
Researching a competitor from open sources: what you can see from outside
12 steps using free sources: WHOIS, DNS, certificates (crt.sh), URLscan and the web archive. A script…