How do people handle win-back/return follow-up separately from their main email tool?

Been thinking about this for a while and curious how others approach it.

Most email tools (Klaviyo, Omnisend, etc.) treat post-purchase stuff like win-back emails, refund follow-ups, re-engaging repeat customers — as just one flow among many. Feels like it’s rarely the main focus of any platform, more like something bolted on.

For context: my store deals with a fair amount of returns, and I’ve been wondering whether it’s worth having something dedicated just for that piece (detecting refunds/returns and following up automatically), separate from whatever handles newsletters and campaigns — or whether that’s overkill and everyone just makes do with what’s built into their main platform.

Anyone here split this up, or is it always just one flow inside a bigger tool? What’s actually worked (or not) for the return/win-back side of things specifically?

The reason it feels bolted on is not priority, it is the trigger. Klaviyo and Omnisend both get a refund event out of the Shopify integration, and that event does not carry the thing you actually need in order to write the email. A full return of a jacket, a cancelled order, a partial refund because you undercharged shipping, and a chargeback reversal all arrive looking roughly the same. Split refunds on a multi item order are the worst of it. So a win back flow built on that trigger ends up emailing people who never sent anything back, and you usually find out from a reply rather than from a dashboard.

The reason lives one object over. Shopify’s return object carries a return reason per line item, size too small, colour, defective, not as described and so on, and that never reaches your email tool because the integration syncs refunds, not returns. If your returns run through native returns or a returns app, push that reason onto the customer as a tag or a customer metafield at the point the return is approved. Then the segment in the tool you already have is doing the work, and you keep one sending domain and one suppression list. That covers most of what people think they need a second tool for.

Where a separate thing genuinely earns its place is when the follow up has to wait on a physical event instead of a clock. Email platforms are built around N days after X. They are bad at when the parcel is scanned by the carrier, or when the item is back on the shelf and restocked, because they never see the carrier event at all. If your returns are slow, or your exchange offer depends on having the item back first, that timing difference is the whole thing and no amount of flow branching gets you there.

The argument against splitting is unsubscribes. Two senders means two suppression lists, and unless you are syncing them back, someone who opted out of your newsletter still gets a refund follow up from the other tool. That is the failure that costs you a complaint instead of a sale, and it is worth checking before anything else.

How long does a return usually take to physically get back to you? A week or more pushes towards the dedicated side, two days and the tag approach is probably enough.

This is genuinely one of the more useful breakdowns I’ve read on this — thanks for going deep on it.

To answer your question: it’s usually 5-9 days for us door-to-door, so sounds like we’re sitting in the ambiguous middle of your framing rather than clearly on one side.

The return-reason-as-tag approach makes sense for the “why did they return it” side. The part I’m still turning over is your third paragraph — the physical event dependency. If the useful trigger there is a carrier scan or a restock event rather than a calendar date, is that something people typically wire up themselves via Shopify Flow + a carrier webhook, or does that usually end up needing something purpose-built because the fulfillment/tracking data doesn’t route cleanly into a flow trigger either?

Also appreciate the suppression-list point — hadn’t thought about the two-sender unsub gap that concretely.

The split that decides it is who owns the event.

Return state and stock are Shopify’s own, and both are already triggers: Return requested, approved, closed, and Product variant inventory quantity changed. Restock is the honest signal that you physically have it back.

The carrier scan isn’t Shopify’s. Flow’s fulfillment triggers stop at created, fulfilled, and the hold and split states, with no delivered trigger at all. So a scan-based flow rests on data that arrives unevenly by carrier and country, and when it doesn’t arrive nothing tells you. The flow just never fires.

So restock as the real trigger, with a fulfilled-date plus N days fallback for the quiet carriers. Purpose-built only earns its keep if you need the return reason inside the message, which is lumine’s point and the one thing neither trigger carries.

Appreciate the correction on the Flow triggers — good to know delivered isn’t there at all, that would’ve been a dead end if I’d gone looking for it.

Restock + fulfilled-date fallback makes sense as the practical answer for timing.

On the reason point you both landed on — if the email copy needs to differ based on why they returned it (say, different message for “wrong size” vs “defective”), does tagging + native triggers still hold up once you’re routing on more than one or two reasons, or does that combination get brittle fast? Trying to figure out if that’s a “just add more tags” problem or something that actually pushes you toward needing purpose-built logic.

The number of reasons is not what breaks it. Two things are, and only one of them gets worse as you add branches.

The first is that a customer tag is a set with no timestamp. It sticks. Someone who sent a jacket back as too small in March and a lamp back as defective in August is carrying both tags at once, and every segment reading tags now matches on a return you closed five months ago. That already misfires at two reasons, so going from two to six does not create the problem, it just makes it visible. Staying on tags means the same workflow that adds one has to remove it later, and that second half is the part nobody writes, because adding a tag is one action and expiring it is a whole other workflow with its own trigger.

The second is worse for a store with real return volume. The reason is not on the return, it is on the return line item. ReturnLineItem carries its own returnReasonDefinition plus a returnReasonNote, so a three item return legitimately carries three different reasons. Tagging the customer forces you to flatten that to one and there is no non-arbitrary way to pick. An email that talks about sizing when the same box also had a defective item reads worse than a generic one, so you lose the thing you went looking for.

So it is not a just add more tags problem, but purpose-built logic is not the answer either. Move the reason off the person and onto the event. If your tool accepts a custom event through its API, fire one at return approval carrying the reason, the order and the line items as event properties, then branch on the properties. Properties are point in time, so nothing accumulates and nothing needs expiring, and a multi reason return arrives as one event you can read rather than three tags competing.

Then collapse the list before you branch on it. Shopify’s reasons are a closed set of ten. Colour, style, changed their mind, size too large, size too small, damaged or defective, received the wrong item, not as described, other, unknown. Most of those want the same email. What differs is what you are asking for next, so three buckets covers it. Fit and preference gets sizing help and an exchange nudge. Defective and wrong item gets an apology and a replacement offer, and should sit out of win-back campaigns for a while rather than receive a different version of one. Changed their mind is your ordinary win-back. Branch on the bucket you assign rather than on Shopify’s taxonomy, because then a reason you add later is a mapping change instead of a new flow.

Worth knowing that other and unknown will be a bigger slice than you expect, and the detail for those sits in the freeform note, capped at 255 characters, which you cannot route on. Those go down the generic path no matter what you build.

Are your returns mostly single item, or do people tend to send back several things from one order?

I’d keep the return/win-back side inside the main email tool unless you’ve got a very specific reason not to, because the hard part is deciding what should happen after a refund or return starts.

In practice, the split I’d make is simple, one flow for the operational stuff, like return started, label sent, item received, refund issued. Then have a separate branch for customer recovery, like a short check-in after the return closes if the customer is still worth bringing back.

One correction to the list, because it changes how the collapse gets written. The ten-value returnReason is deprecated on ReturnLineItem; what a line item carries now is returnReasonDefinition, an entry from Shopify’s own library of reasons with a stable handle, with a subset suggested per product category. So the set is still closed and still Shopify’s, but it’s bigger than ten and it can grow. Write the buckets against the handles and give every unknown handle a default bucket, otherwise the first reason Shopify adds falls through the branch silently.

And one case none of this catches: a refund issued from the order page without a return. Refund only has an optional link to a Return, so there’s no line item and no reason. If your team ever refunds that way, that customer gets no email at all — decide whether that’s intended before building the rest.

The tag-decay point is the part that really lands — hadn’t thought about the “who removes it” half of that workflow, and you’re right that nobody builds that part.

The bucket framing is clean. The exclusion rule on defective/wrong-item is the bit I hadn’t considered at all — makes sense you wouldn’t want someone who just got a broken item to receive a win-back nudge shortly after, even a well-worded one.

To answer your question — it’s a mix, probably 70/30 single vs multi-item, but the multi-item ones are disproportionately our higher-value orders, so getting those wrong on a flattened single-reason tag would already be a real cost, not an edge case.

On the “fire a custom event with reason + line items as properties” approach — for stores without a dev team maintaining that themselves, is that realistically something a merchant configures once through an app, or does it usually end up needing custom work per store because the mapping (Shopify’s 10 reasons → your 3 buckets) has to be hand-tuned anyway?

That operational-vs-recovery split makes sense as a mental model regardless of tooling — appreciate the framing.

Curious about “still worth bringing back” though — how do you typically decide that? Is it LTV/order count based, or something more specific to the return itself (e.g. how it was handled, whether they seemed frustrated)? Trying to figure out if that’s a segmentation call your main tool handles fine, or if it’s specific enough that it needs its own logic separate from whatever’s routing the return reason stuff being discussed above.

Appreciate the correction — good to know the enum’s deprecated, would’ve built against stale docs otherwise.

The default-bucket-for-unknown-handle point is well taken, that’s a “silent failure six months from now” bug waiting to happen if skipped.

On the no-Return refund case — in our setup the reason-based stuff would just be an enhancement layer on top of a more generic “refund happened” flow, so a refund with no line item would just fall through to that generic version rather than getting nothing. Does that match what you’d expect, or is there a reason that fallback itself is risky in some way I’m not seeing?

The mapping is the part that does not need hand tuning, which is the opposite of how it looks. returnReasonDefinition entries have stable handles and they come out of Shopify’s library rather than yours, so the same handle means the same thing in every store on the platform. A bucket table shipped as a default works everywhere, and the only per-store work left is a merchant deciding they want two of your buckets treated as one. That is a settings screen, not development.

What is genuinely per store is the collapse rule for multi-item returns, because it depends on what you sell. With a 70/30 split where the 30 is your higher value orders, I would not let a generic default pick the winning reason for you there.

On the wiring, you do not need a dev for the event itself. Flow has Return requested, approved, closed and processed as triggers, a For each loop to walk the line items, and a Send HTTP request action, so reason plus line items can leave for your email tool without anyone writing code. The bit that bites is the receiving end. Most email tools accept the payload happily and then only let you segment on top level properties, so an array of line item reasons arrives and is unusable in a condition. Worth testing that before you build the sender, because it decides whether you send one event carrying an array or one event per bucket already collapsed.

And put the unknown handle default Bristan flagged on the sending side rather than the receiving side. A handle you have never seen should leave as your fallback bucket, otherwise it becomes an event your email tool quietly drops and nobody notices for a quarter.

The fallback is right, and it has one trap. Refund created fires on every refund, partial or complete, and a return ends in a refund. So the order that just went through your reason-based flow fires the generic one too, and that customer gets both emails. The Flow docs warn about workflows firing together.

Two conditions on the generic flow before it sends anything. The refund’s return field is empty, which tells you it isn’t the tail end of a return. And the refund has line items in it: a shipping refund or a goodwill gesture has none, and someone who kept the product and is perfectly happy shouldn’t get a win-back for it. Not “full refund only”, or you’d silence the one-broken-item-out-of-three case, which is the customer who most needs the email.

That’s the trap I wouldn’t have caught until a customer complained about getting two emails — appreciate you spelling out the exact condition instead of just “check for duplicates” as a vague warning.

The “not full-refund-only” callout is the sharper point though. Would’ve been tempting to reach for that as the simple filter, and it would’ve silently broken exactly the case everyone in this thread has been circling back to — the partial refund on a multi-item order being the one that most needs the email, not least.

Genuinely useful thread, this has been more rigorous than most of the docs I’ve found on this.

Good to know the mapping itself doesn’t need per-store tuning — that changes how I was thinking about this being “too custom” to build once and ship.

The multi-item collapse rule being the real per-store variable makes sense, especially given what you said about the array-vs-segmentable-property issue — sounds like whatever’s deciding the “winning” reason needs to happen before the data reaches whatever’s doing the segmenting, not after, otherwise you’re stuck with an unusable array at the point you actually need to act on it.

That’s a useful thing to know before building anything — appreciate you spelling out where the failure mode actually shows up rather than just that it exists.

The split I would actually keep is not Klaviyo vs a second tool.

After a return, the two emails that still land in the inbox are “did my refund go through” and “the label didn’t work.” Those are not campaigns. If you put a win-back in the same flow, people reply to that instead of the refund question, and you end up answering both in one thread.

If those two are still daily paste-jobs, keep them as saved replies and leave Klaviyo on newsletters. A dedicated returns tool only pays if those two never leave the inbox.