Dashboard

How to Add a Return and Exchange Flow to an AI-Built App

A returns flow is a state machine, not a form. Model requested, approved, in transit, and resolved states with one resolution_type field before you prompt.

Steve Jefferson
Steve Jefferson
Developer Advocate
9 September 20261 min read

How to Add a Return and Exchange Flow to an AI-Built App

A returns flow looks simple until you build one: a customer wants their money back, or a different size, or store credit instead, and each of those is a different state machine with different edge cases. Build it as one linear "request a return" form and you will spend the next month handling the cases it does not cover in support tickets instead. Here is the version that holds up.

The four states a return actually moves through

Most AI-built apps that add returns start with a single boolean, `return_requested`, and discover within a week that a return is not binary. It needs a state field with a fixed set of values:

text
requested -> approved -> in_transit -> resolved
                \-> denied
  • Requested: the customer opened a return, has not been reviewed yet.

  • Approved: you have agreed to it, a shipping label or drop-off instructions go out.

  • In transit: the item is on its way back, resolution is not final.

  • Resolved: refunded, exchanged, or credited. This is the only state where money or inventory actually moves.

  • Denied: a branch off "requested" for out-of-window or non-returnable items.

Prompting an AI builder for "a returns feature" without specifying this will usually get you a form and a status column with whatever values the model guesses, often missing "denied" entirely, which then gets bolted on later as a special case that breaks reporting.

Refund, exchange, and credit are different resolutions, not different flows

The mistake that costs the most rework is treating "exchange" as a separate feature from "refund." They are the same state machine with a different final resolution:

Resolution

What happens to the original order

What happens to inventory

Refund

Payment reversed, order line marked returned

Item goes back to stock (if resellable) or written off

Exchange

Original order line closed, a new order created for the replacement

Original item returns to stock, new item reserved

Store credit

Payment not reversed, credit balance issued

Item goes back to stock (if resellable)

Build the return record to carry a `resolution_type` field with these three options, decided at the "approved" step, rather than three separate database tables or three separate flows. This is the single decision that determines whether adding exchanges next month is a two-hour job or a rebuild.

The prompt sequence that gets this right

Building this in stages, verifying each before moving on, works better than asking for the whole flow at once:

text
1. "Add a `returns` table: order_id, item_id, reason, status
   (requested/approved/in_transit/resolved/denied), resolution_type
   (refund/exchange/credit, nullable until approved), requested_at,
   resolved_at, notes."

2. "Add a return request form on the order detail page. Customer picks
   the item, a reason from a fixed list, and submits. Creates a row
   with status='requested'. Do not process payment yet."

3. "Add an admin view listing requested returns. Approving sets
   status='approved' and prompts for resolution_type. Denying sets
   status='denied' and requires a reason, shown to the customer."

4. "When resolution_type is set to refund or credit and status becomes
   resolved, trigger the refund. For exchange, create a new order
   referencing the original and mark the original item's inventory
   as returned."

Splitting it this way means you can test the request-and-approve loop before any money moves, which is where you want to catch mistakes.

Policy questions to answer before you prompt

The model will invent defaults for anything you leave open, and its defaults are rarely your actual policy:

  • Return window. 14, 30, 60 days? Left unspecified, expect 30 as the model's default guess.

  • Who pays return shipping. Unspecified, most builders assume the merchant does, which is the more generous and more expensive default.

  • Restocking fee. If you charge one, it needs to be a field on the return record, applied at resolution, not calculated ad hoc.

  • Final sale items. Anything exempt from returns needs a flag on the product itself, checked before the request form even shows the item as eligible.

Write these down before the first prompt in the sequence above. A returns feature built on the wrong assumptions here is the kind of bug that only surfaces when a real customer hits it, not during your own testing.

Testing it like a customer, not like a developer

The state machine above has edge cases that only show up with adversarial testing:

  • Request a return on an item that has already been returned once. It should be blocked, and a developer testing happy paths will not think to try it.

  • Deny a return, then have the customer try to resubmit for the same item. Decide, deliberately, whether that is allowed.

  • Approve as exchange, then have the replacement item go out of stock before the exchange order is created. The flow needs to handle that, not just the case where stock is available.

  • Partial returns: two items on one order, only one being returned. If your data model tracks returns at the order level instead of the item level, this case will corrupt the record.

Run these before shipping, because a returns flow is the one part of a store that customers interact with when they are already unhappy, and a broken edge case there compounds the frustration rather than just being an inconvenience.

For the surrounding data model decisions, see how to plan your data model before building an app with AI. A support ticket system is the natural companion feature for the cases this flow cannot resolve automatically, covered in how to add a support ticket system to an AI-built app, and the same request-approve-resolve pattern applies to account deletion requests in how to add GDPR data deletion to an AI-built app. For the billing side of the same app, see how to add a subscription paywall to an AI-built app. The general playbook for building with AI instead of hiring developers is in our guide to building an app with AI.

FAQ

Should returns and exchanges share a database table?

Yes. Model them as one `returns` table with a `resolution_type` field (refund, exchange, credit) rather than separate tables or flows. This is the decision that keeps adding a new resolution type cheap later.

Who should pay for return shipping in an AI-built store?

There is no universal answer, but the decision needs to be explicit in your prompt and stored as a policy setting, not left for the model to assume. Unspecified, expect it to default to the merchant paying, which is the more expensive assumption.

How do I stop customers from returning an item twice?

Check for an existing non-denied return record on the same order item before allowing a new request, and block the request form if one exists. This needs to be an explicit rule in your prompt, since it is exactly the kind of edge case a first draft skips.

What is the difference between store credit and a refund in this flow?

Both return inventory to stock the same way. The difference is entirely on the payment side: a refund reverses the original charge, while store credit issues a balance without touching the original payment. Model them as two values of the same `resolution_type` field.

How did this land?

About the author

Steve Jefferson
Steve Jefferson

Developer Advocate

Steve builds something with Swarmz every week and writes up what worked, what broke, and what he'd do differently. Tutorials and hands-on guides are his lane.

Share

Get the next post in your inbox

One email a month. Product updates, engineering posts, and the best of Built with Swarmz.

I agree to receive emails about AI building tips and Swarmz product news. Unsubscribe any time.