Blog / Product Brand Operations

Return Request Form and Returns Log in Google Sheets, With Status Emails

By Keyona Meeks, Founder, ReRev Labs · Oct 31, 2026 · 11 min read

The short answer: Make a Google Form with order number, email, item, reason, details and the outcome the customer wants. Link it to a sheet. A small Google Apps Script numbers each request (RET-0001), suggests a route of Exchange, Refund or Review from the reason you set, and emails the customer a receipt. When you change the Status cell to Approved, Label sent, Received, Refunded or Declined, the customer gets one email for that status. The route is a suggestion. A person decides every return. I mock-tested the script; I did not run it in a live Google account.

By the end of this post, you'll be able to:

  • Build a return request form and link it to a sheet.
  • Paste in a script that numbers returns, suggests a route and sends status emails.
  • Change the triage rules to match your policy.
  • Test it on a copy before a real customer sees an email.

A customer wants to send something back. They email, then email again, then reply on a different thread. By the time you find the order, the first message is buried and you are not sure whether anyone answered. A form fixes the first half of that: every request arrives in the same shape. The returns log fixes the second half: one row per return, one status, and the customer is told when it changes.

I am not a lawyer, and I am not claiming this replaces a returns platform. If you handle hundreds of returns a week with labels, restocking and payment processing, software built for that will fit better. This is for a small brand that wants to stop losing requests in an inbox.

What does a returns log need?

When I searched "return request form google forms" on October 30, 2026, page one was mostly form-template pages from form builders. Titles only. I did not review them and I name none. A template gives you the fields. It does not give you a log that numbers requests, suggests a route and keeps customers informed.

Start with the form. Six questions are enough:

  1. Order number. Short text.
  2. Email. Short text. Say it must match the order.
  3. Item. Short text.
  4. Reason. Multiple choice: Wrong size, Changed my mind, Not as described, Defective, Other.
  5. Details. Paragraph. Optional.
  6. Preferred outcome. Multiple choice: Refund, Exchange, Store credit.

One more thing: do not use a file upload question. Google's help says "To answer this question, responders need to sign in to a Google Account," and many customers will not. Ask them to reply to your receipt email with photos instead. Source: Google's help on file upload questions, checked October 30, 2026.

Keep the Reason choices as plain words. The script matches them exactly.

Google's help page gives the steps. In Google Forms: open your form, click Responses, then the Summary tab. At the top right, click More, then Select destination for responses. Choose Create a new spreadsheet or Select existing spreadsheet, then click Create or Select. Source: Google's help page on choosing where to save form responses, checked October 30, 2026.

Question titles become the column headers. Open the sheet and add four more headers to the right of the form columns: Return ID, Suggested route, Status and Last emailed status. The script fills them.

If you want the Status cell to be a dropdown with the values New, Approved, Label sent, Received, Refunded and Declined, Google's help covers adding one. A dropdown stops typos, and a typo means no email.

What are the triage rules?

The script suggests one of three routes:

  • Exchange. For example, Wrong size.
  • Refund. For example, Changed my mind.
  • Review. Anything that needs a person to look first.

You write the rules in the ROUTES block, one line per reason. A reason that is not in the list goes to Review. A second rule overrides the first: if the Details text contains a word from REVIEW_WORDS, such as "damaged", "broken", "missing", "allergic" or "burn", the route is always Review. That keeps a safety or damage complaint from being filed as a routine refund.

The route is a suggestion in a column. It does not refund anything, and it does not decide. Your return policy decides, and I do not write it for you. Refund rules differ by place and by how the customer paid. If you are unsure what you owe a customer, ask an attorney or check your payment processor's terms.

How do you set up the script?

  1. Open the sheet. Choose Extensions, then Apps Script. If this is new to you, read what Google Apps Script is first.
  2. Delete the placeholder code and paste the script below.
  3. Change STORE_NAME, REPLY_TO and the rules to your own.
  4. Click Save.
  5. In the function list, choose setupReturnTriggers and click Run. Google asks you to allow the script to read your sheet and send email as you. Read the list. Approve it only if you agree.
// Returns log: number each request, suggest a route, send a receipt, and email status updates.
// Paste into Extensions > Apps Script in the sheet that receives your return form.
const CONFIG = {
  SHEET_NAME: 'Form Responses 1',
  STORE_NAME: 'Your Store',
  REPLY_TO: 'returns@example.com',        // replies from customers go here
  // Reason (exactly as written in the form) -> suggested route. Anything not listed goes to Review.
  ROUTES: {
    'Wrong size': 'Exchange',
    'Changed my mind': 'Refund',
    'Not as described': 'Review',
    'Defective': 'Review'
  },
  // If the free-text details contain any of these words, the route is always Review.
  REVIEW_WORDS: ['damaged', 'broken', 'defective', 'missing', 'wrong item', 'allergic', 'injur', 'burn', 'fire'],
  // Status you type in the sheet -> email the customer gets (once per status).
  STATUS_EMAILS: {
    'Approved': 'Your return {id} is approved. We will send the next steps shortly.',
    'Label sent': 'We sent return instructions for {id}. Please follow them to ship the item back.',
    'Received': 'We received the item for return {id}. We are checking it now.',
    'Refunded': 'Your refund for return {id} has been issued. Timing depends on your bank or card.',
    'Declined': 'We could not approve return {id}. Reply to this email and we will explain.'
  }
};

const NEEDED = ['Order number', 'Email', 'Item', 'Reason', 'Details', 'Return ID',
                'Suggested route', 'Status', 'Last emailed status'];

function headerMap_(sheet) {
  const header = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
  const col = {};
  NEEDED.forEach(function (n) {
    const i = header.indexOf(n);
    if (i === -1) throw new Error('Missing column: ' + n);
    col[n] = i + 1;   // 1-based column number
  });
  return col;
}

function suggestRoute_(reason, details) {
  const text = String(details || '').toLowerCase();
  for (let i = 0; i < CONFIG.REVIEW_WORDS.length; i++) {
    if (text.indexOf(CONFIG.REVIEW_WORDS[i]) !== -1) return 'Review';
  }
  return CONFIG.ROUTES[String(reason).trim()] || 'Review';
}

function onReturnSubmit(e) {
  const sheet = e.range.getSheet();
  if (sheet.getName() !== CONFIG.SHEET_NAME) return;
  const row = e.range.getRow();
  const col = headerMap_(sheet);
  const get = function (name) { return sheet.getRange(row, col[name]).getValue(); };

  if (get('Return ID')) return;                       // already numbered
  const id = 'RET-' + ('0000' + (row - 1)).slice(-4);
  const route = suggestRoute_(get('Reason'), get('Details'));
  sheet.getRange(row, col['Return ID']).setValue(id);
  sheet.getRange(row, col['Suggested route']).setValue(route);
  sheet.getRange(row, col['Status']).setValue('New');

  const email = String(get('Email')).trim();
  if (!email) return;
  MailApp.sendEmail({
    to: email,
    replyTo: CONFIG.REPLY_TO,
    name: CONFIG.STORE_NAME,
    subject: 'We got your return request ' + id,
    body: 'Hi,\n\nWe received your return request ' + id + ' for order ' + get('Order number') +
          ' (' + get('Item') + '). A person will review it and email you with the next step.\n\n' +
          CONFIG.STORE_NAME
  });
  sheet.getRange(row, col['Last emailed status']).setValue('Receipt');
}

function onStatusEdit(e) {
  const sheet = e.range.getSheet();
  if (sheet.getName() !== CONFIG.SHEET_NAME) return;
  const col = headerMap_(sheet);
  const row = e.range.getRow();
  if (row < 2 || e.range.getColumn() !== col['Status']) return;

  const status = String(e.value || '').trim();
  const template = CONFIG.STATUS_EMAILS[status];
  if (!template) return;                              // statuses with no email, such as New
  const get = function (name) { return sheet.getRange(row, col[name]).getValue(); };
  if (get('Last emailed status') === status) return;  // already told them
  const email = String(get('Email')).trim();
  const id = get('Return ID');
  if (!email || !id) return;

  MailApp.sendEmail({
    to: email,
    replyTo: CONFIG.REPLY_TO,
    name: CONFIG.STORE_NAME,
    subject: 'Update on return ' + id,
    body: 'Hi,\n\n' + template.replace('{id}', id) + '\n\n' + CONFIG.STORE_NAME
  });
  sheet.getRange(row, col['Last emailed status']).setValue(status);
}

function setupReturnTriggers() {
  const ss = SpreadsheetApp.getActive();
  ScriptApp.getProjectTriggers().forEach(function (t) {
    const fn = t.getHandlerFunction();
    if (fn === 'onReturnSubmit' || fn === 'onStatusEdit') ScriptApp.deleteTrigger(t);
  });
  ScriptApp.newTrigger('onReturnSubmit').forSpreadsheet(ss).onFormSubmit().create();
  ScriptApp.newTrigger('onStatusEdit').forSpreadsheet(ss).onEdit().create();
}

The script needs installable triggers, because it sends email. Google's docs say installable triggers "can call services that require authorization" and "always run under the account of the person who created them." Source: Installable Triggers, checked October 30, 2026. So the emails go out from the account that runs setupReturnTriggers. Use an address you are happy to send from.

What happens when a request arrives?

  1. The customer submits the form.
  2. The script numbers the row RET-0001, RET-0002 and so on, from the row number.
  3. It writes the suggested route and sets Status to New.
  4. It emails the customer a receipt with the return ID, the order number and the item. The email says a person will review it.
  5. It writes Receipt in Last emailed status, so it does not send twice.

If the Email cell is blank, the row is still numbered and routed, and no email goes out.

How do status emails work?

You work the log. When you decide, you change the Status cell:

  • Approved: the customer is told the return is approved and next steps are coming.
  • Label sent: the customer is told instructions were sent.
  • Received: the customer is told the item arrived and is being checked.
  • Refunded: the customer is told the refund was issued, and that timing depends on their bank or card.
  • Declined: the customer is told to reply for an explanation.

Each status sends one email. If you set the same status again, the script sees it in Last emailed status and stays quiet. New sends nothing. Edit the sentences in STATUS_EMAILS to match your voice.

Two warnings. First, an email that says "Refunded" before you refunded anything is worse than no email, so change the status only after the action is done. Second, the script reacts to a status you type or choose by hand. I could not confirm from Google's docs whether a change made by another script or a paste would trigger it, so test the ways you will use it.

What should you check before you trust it?

I tested the script with a mock harness (19 of 19 checks passed) and a syntax check. I did not run it in a live Google account, and I did not test the form. Test it on a copy, using email addresses you own:

  1. Submit three test returns: Wrong size, Changed my mind with "arrived damaged" in the details, and Other.
  2. Check the numbers, the routes (Exchange, Review, Review) and the receipt emails.
  3. Change one Status to Approved. Check that one email arrives.
  4. Change it to Approved again and check that nothing arrives.
  5. Change it to Refunded and check that a second, different email arrives.

Step 3 matters most. I could not confirm in my environment how e.value behaves when you change a cell with a dropdown, or how MailApp shows the sender name and reply-to address. Look at the actual email.

What else should you know?

  • Word matching is rough. REVIEW_WORDS matches parts of words, so "fire" also matches a longer word that contains it. That sends a few extra returns to Review. It does not hide any.
  • Email limits. Scripts have a daily email quota. See Google's quotas page for the current numbers.
  • Personal data. The sheet holds customer emails and order numbers. Share it only with people who need it.
  • Do not delete or sort rows. The return ID comes from the row number.
  • Order lookup. The script does not check that the order exists. A person does that.
  • Auto-replies. The customer's reply goes to REPLY_TO. Check that inbox.

Where does an LLM fit in this?

Not in the routes or the status emails. A reason from a list, a keyword and a status are rules, and rules are easier to inspect than a model. An LLM can help in one place: reading the free-text Details and tagging them, for example "size," "damage," "late delivery" or "changed mind," so you can see patterns across a month. A person should check those tags, because a wrong tag on a damage complaint costs more than an untagged one. My stance: AI writes the code, plain automation runs it, and an LLM shows up only where the input is messy and a person checks the result. For where that line goes wrong, read why AI agents fail.

Where this has limits

  • Not run in a live account. Mock-tested only. Test on a copy.
  • It sends email as you. Read the permission screen. Use an address you are comfortable sending from.
  • Triage is a suggestion. A person decides every return.
  • No payments. It does not refund, ship or print labels.
  • Policy and law are yours. I do not say what you owe a customer. Ask an attorney if you are unsure.
  • Template pages change. My look at the search results is from October 30, 2026.

If returns are eating your week, see our AI-powered operations work or tell us about it on /letstalk. Bring your return policy in plain words and the part that gets dropped. Do not send customer data, passwords or login details.

Frequently asked questions

How do I make a return request form in Google Forms?

Add questions for order number, email, item, reason, details and preferred outcome, then link the form to a sheet. Use multiple choice for the reason so the script can match it.

Can Google Sheets send status emails automatically?

Not by itself. A Google Apps Script can send an email when you change a Status cell, which is what the script above does. Test it on a copy first.

Does the script decide whether to refund?

No. It suggests Exchange, Refund or Review from your rules. You decide, and you change the status.

Why does a damaged item go to Review?

The script looks for words like "damaged" and "broken" in the details and sends those to a person. Edit the word list to suit your products.

Can customers upload photos?

Not through a form upload question, because Google requires sign-in for it. Ask them to reply to the receipt email with photos.

Do I need to know how to code?

No. You paste the script, change a few lines and approve the permissions. An AI tool can help you edit the rules. Test it on a copy first.

Read next: Order confirmation emails from a sheet starts the paper trail this form closes, review requests after delivery is the other email worth automating, and operations automation for product brands maps the rest.