Google Sheets

Append every form submission to a Google Sheet with a small Apps Script web app and the SilentForms webhook. No add-ons.

You can log every submission to a Google Sheet without add-ons or a third-party automation service. A short Google Apps Script, published as a web app, accepts the webhook from SilentForms and appends a row. It runs on Google's side for free, and you keep full control of the code.

Apps Script answers in an unusual way (a redirect after running your script); SilentForms counts that answer as delivered, so each submission becomes exactly one row. The details are below.

1. Create the sheet#

Create a new Google Sheet and give the first tab a header row matching your form's field names, plus receivedAt:

ABCD
receivedAtnameemailmessage

The script uses this header row to decide which value goes in which column, so you can reorder columns or add new ones later. Fields that have no matching header are added as new columns automatically.

2. Add the script#

In the sheet, open Extensions > Apps Script. This creates a script bound to the spreadsheet. Replace the contents of Code.gs with:

const SHEET_NAME = 'Sheet1'; // the tab to write to
const TOKEN = 'replace-with-a-long-random-string';

function doPost(e) {
  if (!e || e.parameter.token !== TOKEN) {
    return ContentService.createTextOutput('forbidden');
  }

  const data = JSON.parse(e.postData.contents);
  const lock = LockService.getScriptLock();
  lock.waitLock(10000); // two submissions at once must not write the same row

  try {
    const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME);
    const lastColumn = Math.max(sheet.getLastColumn(), 1);
    let headers = sheet.getRange(1, 1, 1, lastColumn).getValues()[0].filter(String);

    // Add a column for any field the sheet has not seen yet.
    const missing = Object.keys(data).filter((key) => !headers.includes(key));
    if (missing.length) {
      sheet.getRange(1, headers.length + 1, 1, missing.length).setValues([missing]);
      headers = headers.concat(missing);
    }

    const row = headers.map((key) => {
      const value = data[key];
      if (value === undefined) return '';
      const text = Array.isArray(value) ? value.join(', ') : String(value);
      // A leading apostrophe stops Sheets from treating "=..." as a formula.
      return /^[=+\-@]/.test(text) ? "'" + text : text;
    });
    sheet.appendRow(row);
  } finally {
    lock.releaseLock();
  }

  return ContentService.createTextOutput('ok');
}

Change SHEET_NAME if your tab is not called "Sheet1", and set TOKEN to a long random string. Save with the disk icon or Ctrl/Cmd+S.

What the script does:

  • e.postData.contents is the raw request body, the JSON that SilentForms sends.
  • e.parameter.token reads a query-string parameter. Webhooks carry no signature, so the token in the URL is what stops strangers from writing rows to your sheet.
  • The lock makes concurrent submissions wait for each other instead of racing for the same row.
  • Arrays, from checkbox groups, are joined into "a, b, c" so they fit in one cell.
  • The apostrophe prefix guards against formula injection: a visitor who types =IMPORTXML(...) into your form gets text in the cell, not a formula that runs when you open the sheet.

3. Deploy it as a web app#

  1. Click Deploy > New deployment.
  2. Next to Select type, click the gear icon and choose Web app.
  3. Set the app to execute as Me, so it can write to your sheet.
  4. Set who has access to Anyone. SilentForms does not sign in to Google, so any other option makes Google answer with a sign-in page instead of running the script.
  5. Click Deploy. The first time, Google asks you to authorize the script to access your spreadsheets; review and allow it.
  6. Copy the Web app URL. It ends in /exec.

"Anyone" means anyone with the URL can call the script, which is why the token check matters.

4. Set the webhook#

Add your token to the URL as a query parameter:

https://script.google.com/macros/s/AKfycb.../exec?token=replace-with-a-long-random-string

Sign in to the dashboard, open the form and save this as its webhook URL. Saving it there rather than in a hidden webhookUrl field keeps the token out of your page source.

Send a test submission. A new row should appear within a few seconds.

How Apps Script answers#

Apps Script web apps do not answer a POST directly. Google runs doPost, then replies with a 302 redirect to a separate URL that holds the output. SilentForms does not follow redirects, but it counts a 302 as delivered because the script has already run. That matters with retries: if it counted as a failure, the submission would be sent again and the row added again.

Updating the script#

Editing and saving the code does not change what the /exec URL runs: it keeps running the version you deployed. To publish changes:

  1. Click Deploy > Manage deployments.
  2. Select the active deployment and click the edit (pencil) icon.
  3. Under Version, choose New version, then click Deploy.

This keeps the same URL. Creating a New deployment instead gives you a new URL, and you would have to update the webhook in the dashboard.

Troubleshooting#

  • No row appears. In the Apps Script editor, open Executions in the left sidebar to see each run and any error. A TypeError on getRange usually means SHEET_NAME does not match the tab name.
  • Nothing is written and Executions shows a run that ended at once. The token in the URL does not match TOKEN in the script.
  • Changes to the script have no effect. You saved but did not deploy a new version.

Next steps#

  • What the payload contains: Webhooks.
  • Route submissions to many apps without code: Zapier or Make.
  • Download everything at once: CSV export in the dashboard.