Build a Google Form to Sheets Approval Workflow with Apps Script

Approval workflows — expense requests, leave applications, purchase orders — are a perfect fit for Google Forms + Sheets + Apps Script. Here's how to build one end to end.

The setup

  1. A Google Form collects submissions (e.g., an expense request).
  2. Responses flow into a linked Google Sheet.
  3. An Apps Script runs on form submit to notify an approver by email.
  4. The approver clicks Approve or Reject directly from the email.
  5. The sheet Status column updates automatically.

Step 1 — Create your Google Form

Create a form with fields like:

  • Name (Short answer)
  • Amount (Short answer)
  • Reason (Paragraph)

Link it to a Google Sheet via Responses > Link to Sheets.

Add a Status column manually in the sheet (column E, for example). This is where we'll write "Approved" or "Rejected".

Step 2 — Send an approval email on form submit

Open the linked sheet, go to Extensions > Apps Script, and add this script:

function onFormSubmit(e) { const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); const row = e.range.getRow(); const values = e.range.getValues()[0]; const name = values[1]; // Column B const amount = values[2]; // Column C const reason = values[3]; // Column D const approverEmail = '[email protected]'; const scriptUrl = ScriptApp.getService().getUrl(); const approveUrl = `${scriptUrl}?action=approve&row=${row}`; const rejectUrl = `${scriptUrl}?action=reject&row=${row}`; const subject = `Approval Required: Expense Request from ${name}`; const body = ` <p>${name} has submitted an expense request.</p> <p><strong>Amount:</strong> $${amount}</p> <p><strong>Reason:</strong> ${reason}</p> <br> <a href="${approveUrl}" style="padding:10px 20px;background:#34a853;color:#fff;text-decoration:none;border-radius:4px;">Approve</a> &nbsp; <a href="${rejectUrl}" style="padding:10px 20px;background:#ea4335;color:#fff;text-decoration:none;border-radius:4px;">Reject</a> `; GmailApp.sendEmail(approverEmail, subject, '', { htmlBody: body }); Logger.log(`Approval email sent for row ${row}`); }

Step 3 — Handle the approver's response

Deploy the script as a Web App (Execute as: Me, Who has access: Anyone) and add a doGet function:

function doGet(e) { const action = e.parameter.action; const row = parseInt(e.parameter.row); const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); const statusCol = 5; // Column E if (action === 'approve') { sheet.getRange(row, statusCol).setValue('Approved'); return HtmlService.createHtmlOutput('<h2>Request Approved</h2>'); } else if (action === 'reject') { sheet.getRange(row, statusCol).setValue('Rejected'); return HtmlService.createHtmlOutput('<h2>Request Rejected</h2>'); } return HtmlService.createHtmlOutput('<h2>Invalid action</h2>'); }

Step 4 — Set up the form submit trigger

function createFormTrigger() { const ss = SpreadsheetApp.getActiveSpreadsheet(); ScriptApp.newTrigger('onFormSubmit') .forSpreadsheet(ss) .onFormSubmit() .create(); }

Run createFormTrigger() once to wire up the trigger.

How it all works together

  1. Someone submits the form.
  2. onFormSubmit fires, sends an HTML email with Approve/Reject links.
  3. The approver clicks a link — it hits the deployed Web App URL.
  4. doGet updates the Status column in the sheet.

This is a solid foundation you can extend with notifications to the requester, multi-level approvals, or deadline reminders.