> ## Documentation Index
> Fetch the complete documentation index at: https://parabola.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# How do I watch for new rows in Google Sheets and send them to Parabola?

> Use a Google Apps Script to automatically detect new rows added to a Google Sheet — for example, by a Slack workflow or Google Form — and send each one to a Parabola flow via a webhook.

You can set up a Google Apps Script that checks a Google Sheet for newly added rows and automatically sends each new row to a Parabola flow's webhook as JSON. This is useful when another tool writes rows into a sheet and you want that data flowing into Parabola without manual exports.

## Prerequisites

* A Parabola flow with a **Pull from webhook** step configured (you'll need the generated webhook URL)
* A Google Sheet that's already being updated with new rows by another process
* Access to [Google Apps Script](https://script.google.com)

## Step-by-step setup

**1. Set up your Parabola flow**

Create a flow with a **Pull from webhook** step, connected to whatever destination step you'd like. Publish the flow and run it once — Parabola needs at least one run before it generates a live webhook URL. Then open the **Schedules / Triggers** pane and copy the **webhook URL**.

**2. Open Google Apps Script from your Sheet**

Open the Google Sheet that gets new rows added to it, then go to **Extensions → Apps Script**.

**3. Paste the script**

Replace the default code with the following script. Update `SHEET_NAME` with the name of the tab you want to watch, and `WEBHOOK_URL` with your flow's webhook URL:

```javascript theme={null}
const CONFIG = {
  SHEET_NAME: 'Sheet1',                                         // the tab the workflow writes to
  WEBHOOK_URL: 'https://parabola.io/api/hook/YOUR_WEBHOOK_ID',  // your Parabola webhook URL
  HEADER_ROW: 1,                                                // row number containing column headers
};

function checkForNewRows() {
  const props = PropertiesService.getScriptProperties();
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName(CONFIG.SHEET_NAME);

  if (!sheet) {
    Logger.log(`Sheet named "${CONFIG.SHEET_NAME}" not found. Check CONFIG.SHEET_NAME.`);
    return;
  }

  const lastRowWithData = sheet.getLastRow();
  const lastCol = sheet.getLastColumn();
  const storedLastRow = props.getProperty('lastProcessedRow');

  // First run ever: just record where we are now, don't send old rows.
  if (storedLastRow === null) {
    props.setProperty('lastProcessedRow', String(lastRowWithData));
    Logger.log(`First run — starting point set at row ${lastRowWithData}. New rows after this will be sent.`);
    return;
  }

  const lastProcessedRow = Number(storedLastRow);

  if (lastRowWithData <= lastProcessedRow) {
    return; // nothing new
  }

  const headers = sheet.getRange(CONFIG.HEADER_ROW, 1, 1, lastCol).getValues()[0];

  for (let row = lastProcessedRow + 1; row <= lastRowWithData; row++) {
    if (row <= CONFIG.HEADER_ROW) continue; // never re-send the header row

    const values = sheet.getRange(row, 1, 1, lastCol).getValues()[0];
    const isBlank = values.every(v => v === '' || v === null);
    if (isBlank) continue;

    const rowData = {};
    headers.forEach((header, i) => {
      if (header) rowData[header] = values[i];
    });

    const payload = {
      sheetName: sheet.getName(),
      rowNumber: row,
      sentAt: new Date().toISOString(),
      data: rowData,
    };

    sendToWebhook(payload);
  }

  // Update the marker even if a send failed, so one bad row can't jam
  // the whole pipeline forever. Failed rows are logged in Executions.
  props.setProperty('lastProcessedRow', String(lastRowWithData));
}

function sendToWebhook(payload) {
  try {
    const response = UrlFetchApp.fetch(CONFIG.WEBHOOK_URL, {
      method: 'post',
      contentType: 'application/json',
      payload: JSON.stringify(payload),
      muteHttpExceptions: true,
    });
    const code = response.getResponseCode();
    if (code >= 200 && code < 300) {
      Logger.log(`Sent row ${payload.rowNumber} to Parabola (${code})`);
    } else {
      Logger.log(`Parabola returned an error for row ${payload.rowNumber}: ${code} ${response.getContentText()}`);
    }
  } catch (err) {
    Logger.log(`Failed to send row ${payload.rowNumber}: ${err.message}`);
  }
}

function setup() {
  removeTriggers();
  ScriptApp.newTrigger('checkForNewRows')
    .timeBased()
    .everyMinutes(1)
    .create();
  Logger.log('Trigger installed — checking for new rows every minute.');
}

function removeTriggers() {
  const triggers = ScriptApp.getProjectTriggers();
  triggers.forEach(t => {
    if (t.getHandlerFunction() === 'checkForNewRows') {
      ScriptApp.deleteTrigger(t);
    }
  });
}
```

**4. Run the script once manually**

In the function dropdown at the top of the Apps Script editor, select **`setup`**, then click **Run**. This installs a trigger that checks the sheet every minute, and records the current last row so existing rows aren't re-sent.

You'll be asked to authorize permissions the first time — click **Advanced**, then **Go to \[project name] (unsafe)**, then **Allow**. This warning is expected for any script you write yourself.

**5. Confirm it's working**

Have your external workflow add a test row. Within about a minute, check your Parabola flow's run history — you should see a new run containing that row's data. To see the script's own logs, open **Executions** in the left sidebar of the Apps Script editor.

## Things to know

* **Polling delay** — Apps Script triggers can run at most once per minute, so there's up to a \~1 minute delay between a row being added and it reaching Parabola. This is a limit of Apps Script, not something the script itself controls.
* **Header names become field names** — Whatever's in row 1 of the sheet becomes the JSON keys sent to Parabola (e.g. a "Status" column becomes `"Status": "..."` in the payload). If you rename a header column later, downstream steps in your Parabola flow may need to be updated to match.
* **Multi-row pastes are handled** — If several rows are added at once, the script sends each one as a separate webhook call, in order.
* **One bad row won't block the rest** — If a single row fails to send (e.g. a temporary network issue), the script logs the failure and still moves its marker forward, so it won't get stuck retrying the same row forever. Check **Executions** periodically if you want to catch failures.
* **Stopping the automation** — Select `removeTriggers` in the function dropdown and click Run to turn it off.
