- A refund and a restock are two different events, and only one of them has a customer chasing it.
- The fix is not auto restocking. It is a reconciliation that notices when a refunded unit never came back into sellable stock.
- The prompt turns one month of export data into a list of units that went missing between the refund and the shelf.
- The script joins returns against inventory history on a schedule, with no model running at the moment of truth.
- The same join pattern works anywhere two systems are supposed to agree, which is most of what it takes to keep stock counts honest automatically.
Welcome to The Loop. Every Wednesday we take one real, repetitive workflow that someone does by hand and unwind it into something you can run. This week: the Friday afternoon spent checking, one order at a time, whether the things customers sent back ever made it onto the shelf.
The boring task you repeat is a specification in disguise. Opening two tabs and comparing them is not judgment work, it is a rule you have already written down in your head and never typed out. The moment you can describe the comparison, you can hand it off. Describing replaces doing.
This loop comes straight from a merchant thread on the Shopify Community, where somebody asked what manual task people still do every week and the top answer was reconciling returns against inventory, because, in their words, there is no simple view that links the two. Let us walk it.
The Loop, in Full
See the loop clearly before automating it, because the automatable part is smaller than the job it sits inside. Priya runs operations for a forty-person apparel brand. On Friday she opens the returns list, filters to anything refunded in the last two weeks, and starts working down it. For each line she opens the order in one tab, then opens the product's inventory history in another, and looks for an adjustment that matches: same SKU, same size, roughly the same date, quantity one. Sometimes it is there. Often it is not, and she cannot tell whether the unit is sitting in a receiving bin, went to the damaged pile and nobody logged it, or walked out the door.
- Open the returns report and filter to refunds issued in the last fourteen days.
- For each return, open the order and note the SKU, the variant, and the refund date.
- Open that variant's inventory history and scan for a matching positive adjustment.
- If there is no adjustment, message the warehouse to ask whether the unit arrived.
- Record the answer in a spreadsheet so next Friday does not start from zero.
Every step here is easy, which is exactly why it never gets fixed. Nothing in the list requires a decision harder than "do these two records refer to the same unit," and that is the signature of work a machine should be doing. The hard part is not the comparison. The hard part is that nobody has written down what counts as a match, and so the comparison lives in one person's head and happens only when that person has an hour. Brands that get past this stop treating it as a chore and start treating it as one more piece of back office they can hand off to a workflow that runs without them.
The Manual Tax
The cost is not the hour on Friday. It is the units you own, cannot sell, and do not know about.
The scale underneath this is enormous. In its 2025 returns research, the National Retail Federation put total US merchandise returns at roughly $850 billion, about 15.8% of annual sales, with online returns running higher still at an estimated 19.3%. Nearly a fifth of everything you ship online is coming back, and every one of those units has to be caught, inspected, and either put back on the shelf or written off on purpose. The ones that fall between those two outcomes are pure loss, and they are invisible by construction.
- Errors. A unit that is physically in the building but not in the system is a unit you will reorder. You pay for it twice, once to the supplier and once in the shelf space it takes up while your own stock count says you have none. The mirror of that mistake is just as bad: a unit marked sellable that is actually damaged, which becomes an oversell, a second refund, and a customer who now has two bad experiences instead of one.
- Latency. Every day a returned item spends in limbo is a day you are running ads for a product you already own. The listing says sold out, the reorder goes in, and the item sits in a bin twelve feet from the picker. For seasonal apparel this is not a rounding error, because the unit becomes unsellable at full price on a calendar that does not wait for your receiving queue.
Stock that exists but is not in the count is worse than stock you never had, because you make confident decisions on top of a number that is wrong.
So the goal is not to save Priya her Friday hour, though it will. The goal is to make the gap between refunded and restocked impossible to hold open quietly, which is what it means to keep stock counts honest automatically instead of auditing them by hand after the damage is done.
Unwinding the Loop
Automating starts with description, not code. Write the loop down as a spec and the steps turn out to be trivial, while the decisions turn out to be the entire job.
| Part of the loop | What it is for this workflow |
|---|---|
| Trigger | A schedule, not an event. Every weekday morning, look at refunds issued more than N days ago. |
| Input | Two lists: refunded return line items (order, SKU, variant, quantity, refund date) and inventory adjustments for those variants over the same window. |
| Decision | What counts as a match. Same variant, a positive adjustment, quantity greater than or equal to the returned quantity, dated after the refund and within the grace window. |
| Decision | What the grace window is, and whether a unit written off as damaged counts as resolved. It does, as long as somebody recorded the write-off on purpose. |
| Output | One list of unresolved returns, each with order number, SKU, refund date, days outstanding, and a direct link to the order, delivered to the ops owner. |
| Success signal | The list gets shorter week over week, and no item on it is older than the grace window plus one cycle. |
Look at what the table actually says. Four of the six rows are plumbing and two are judgment, and the two judgment rows are things you already decide every Friday without saying them out loud. That is the whole move: say them out loud once, and the plumbing becomes somebody else's problem. It is the same shape as the reconciliation in our installment on checking invoices against a sheet, and the reason it generalizes is that both are a join with a tolerance. Writing the spec first is also how BYOBot turns a described task into a running workflow, which is why the description is not a preamble to the work, it is most of the work.
Tell BYOBot how your returns flow works and it will design the reconciliation: the match rule, the grace window, and the morning list of units that never came back.
The Build: One Loop, Three Ways to Run It
The same described loop becomes three builds, and which one you want depends only on how hands-off you need it to be. Start at the top and stop whenever it is good enough.
The Prompt
You can run this today with two CSV exports and no setup at all. Pull a returns export and an inventory adjustment history export covering the same date range, paste both into any AI chat tool, and give it the match rule.
You are reconciling returns against inventory movements for an online store. I am giving you two CSVs. CSV A (returns): order_number, sku, variant, qty_returned, refund_date, return_reason CSV B (inventory adjustments): sku, variant, qty_delta, adjustment_date, adjustment_reason A return counts as RESOLVED if CSV B contains a row for the same sku and variant where: - qty_delta is positive and >= qty_returned, AND - adjustment_date is on or after refund_date, AND - adjustment_date is within 7 days of refund_date A return also counts as RESOLVED if CSV B has a row for the same sku and variant with an adjustment_reason containing "damaged", "write-off", or "disposed" in that same window. Everything else is UNRESOLVED. Return a single table of UNRESOLVED rows only, sorted by refund_date oldest first, with columns: order_number, sku, variant, qty_returned, refund_date, days_outstanding, best_guess_reason. For best_guess_reason, use only what the data supports: "no adjustment found", "adjustment too small", "adjustment before refund", or "adjustment outside window". Do not invent explanations. End with a one-line count of unresolved units and their total at cost if I gave you a cost column.
Run that once and you will almost certainly find units you did not know were missing, which is the cheapest possible proof that the loop is worth closing. It works the same in ChatGPT, Claude, or Gemini, and it is a sensible first step before you commit to anything that touches your store, whether you are on Shopify or something else you want to wire into a workflow that runs on its own.
The Script
Here is the honest version: this build needs no AI at runtime. The reconciliation is a join between two lists with a date tolerance, and a plain script does that deterministically, every time, for free. Both sides are available over an API, so nothing needs to click through a UI either. On Shopify, returns and inventory adjustments both live in the Admin GraphQL API, and the semantics of what restocking actually does to an inventory level are spelled out in the Shopify Help Center. Read both before you pick your match rule, because the exact adjustment reason your platform writes is the thing your join keys on.
GRACE_DAYS = 7
returns = fetch_refunded_return_lines(since = today - 60d)
adjusts = fetch_inventory_adjustments(since = today - 60d)
index = group adjusts by (sku, variant)
unresolved = []
for r in returns:
if today - r.refund_date < GRACE_DAYS:
continue # still inside the window
candidates = index[(r.sku, r.variant)]
match = first a in candidates where
a.date >= r.refund_date and
a.date <= r.refund_date + GRACE_DAYS and
(a.qty_delta >= r.qty or a.reason in WRITE_OFF_REASONS)
if not match:
unresolved.append(r with days_outstanding, order_url)
if unresolved:
write_to_tracker(unresolved) # the durable record
notify(owner, summarize(unresolved))
else:
log("clean run", count = len(returns))
Two details carry most of the value. The first is the grace window skip at the top, which stops the report crying about returns that are legitimately still in receiving. The second is writing to a tracker before notifying anybody, so the list is a record with a history rather than a message that scrolls away. BYOBot will generate the full working version against your actual platform, including the pagination and the rate limits, which is the part nobody enjoys writing when all they wanted was to stop losing units between the refund and the shelf.
The Schedule
Now make it boring. Put the script on a timer and the Friday hour disappears, replaced by a short list that shows up on its own. A cron entry on any always-on machine does it, and so does a free scheduled workflow: the GitHub Actions schedule trigger runs on a cron expression at no cost for a job this small. Daily at 7am beats weekly, because a unit found on day eight is still findable and a unit found on day thirty is usually gone. Send the empty-list run too, at least at first: a report that only appears when something is wrong is a report you stop trusting the moment it goes quiet.
| Build | What runs it | Setup | Runs unattended? |
|---|---|---|---|
| The prompt | You, in any AI chat tool, with two CSV exports | Minutes | No |
| The script | A short join you run against the API when you want | An hour, once | On demand |
| The schedule | The same script on a daily timer, writing to a tracker | A few extra minutes | Yes |
Steal This Build
Here is the whole loop as a spec you can copy into your own stack this afternoon, or hand straight to a system that will turn the description into a running workflow for you.
- Trigger: daily at 7am, no event required.
- Pull: refunded return lines and inventory adjustments for the last sixty days.
- Match: same variant, positive adjustment of at least the returned quantity, dated between the refund and the grace window, or an intentional write-off in the same window.
- Skip: anything still inside the grace window, so the list only contains real problems.
- Report: unresolved units to a tracker with days outstanding and an order link, and a summary to the one person who owns receiving.
The shape is universal. Two systems are supposed to agree about the same real-world thing, nobody owns the agreement, and so a person checks it by hand on a day when they have time. Swap returns and inventory for payments and invoices, or signed contracts and the CRM, or shipped orders and tracking numbers, and the spec barely changes: pull both sides, define a match with a tolerance, report only the gap. Once you have built it once you will see the pattern everywhere, and most of what passes for routine data work you can hand to a bot turns out to be this exact join wearing a different hat.
Tell BYOBot about a loop in your week
Describe the task you keep doing by hand and BYOBot will design the full playbook: the prompt, the script, and the schedule that runs it for you.
Frequently Asked Questions
- Because the refund and the restock are two separate events in two separate systems, and only the refund is urgent. The customer is chasing the money, so the refund always happens. Putting the unit back into sellable stock depends on somebody in the warehouse finishing an inspection and remembering to click restock, and nothing complains when that step never arrives. Closing that gap is a reconciliation job, which is why it belongs with the rest of your automated store operations rather than on a Friday checklist.
- Only for categories where every return is sellable on arrival, which is rare. Auto restocking a damaged or worn item puts a unit on your storefront that you cannot ship, and that failure costs more than the one you were fixing: an oversell, a second refund, and a support conversation. The safer build keeps the human inspection and automates the chasing, so the workflow flags what never came back and a person decides what it is.
- Set the threshold from your own receiving times rather than a generic number. Seven days from the refund is a reasonable starting point: long enough for receiving and inspection at most warehouses, short enough that a missing unit surfaces inside the same selling week. Measure your actual median first, then set the window a few days past it, and tighten it once the list stops being noisy.
- Not at runtime. The reconciliation is a join between two lists and a plain script does it perfectly with no model involved, which is cheaper and more predictable. AI earns its place earlier: writing the script, pressure testing the match rule, and summarizing the exceptions in plain language for whoever has to act on them. That split is how most durable builds look once you put inventory checks on a schedule.
