Text each new row in a Google Sheet
Send a text from your own number for every new row in a Google Sheet and write back when it went, with Zapier, Make or a short Apps Script that needs nothing else.
Luis runs Northside Tutoring. Parents sign up through a Google Form, and every answer becomes a row in a sheet called “Sign-ups”. Luis wants each parent to get a text from his own number confirming the class they picked, and he wants the sheet to show which rows were texted and when, so his assistant never texts a parent twice by hand.
What you’ll have
- A text from your number for each new row, a few minutes after it appears (or at once, when a Google Form feeds the sheet).
- Two columns on the sheet that say when each row was texted and which BlueBot text it was.
- Rows that were already in the sheet left alone.
What you need
- A BlueBot account with your Mac connected.
- A Google Sheet with a header row and one row per person, including a mobile number column.
- People in the sheet who asked to hear from you by text. A sign-up form should say so beside the number.
Luis’s sheet has these columns. Add the last two to yours, empty:
| Column | Header | What’s in it |
|---|---|---|
| A | Timestamp | When the form was sent. Google Forms fills it. |
| B | First name | The parent’s first name. |
| C | Mobile | Their mobile number. |
| D | Class | The class they picked. |
| E | Texted at | Empty until the row is texted. |
| F | BlueBot id | The text’s msg_ id, for finding it later. |
There are three ways to build it. Zapier and Make take a few minutes and no code. Apps Script runs inside Google with no third party at all, and is the way to go when you don’t use Zapier or Make already.
With Zapier
Start the Zap with the sheet
In Zapier, make a Zap. For the trigger, pick Google Sheets and the New Spreadsheet Row event, sign in to Google, then pick the spreadsheet and the Sign-ups worksheet. Press Test trigger to load a row.
Add BlueBot’s Send Text action
Add an action step, pick BlueBot and the Send Text action. Connect your account with an API key from Developers, then API keys, in the BlueBot dashboard (the messages:send scope is enough). Then fill in:
| BlueBot field | Map it to |
|---|---|
| To | The Mobile column from the trigger. |
| Text | Hi, the First name column, then , this is Luis at Northside Tutoring. You’re signed up for, the Class column, . Reply here with any questions. |
| Metadata | source set to google-sheets, and row set to the trigger’s Row ID. |
| Service | Leave it on Auto: iMessage when the parent uses it, otherwise SMS through your iPhone. |
Write back to the row
Add a second action: Google Sheets, Update Spreadsheet Row. Pick the same spreadsheet and worksheet, set Row to the trigger’s Row ID, then set Texted at to the Send Text step’s Created At and BlueBot id to its ID.
Turn it on
Turn the Zap on. Zapier looks for new rows on its schedule (every one to fifteen minutes, by plan) and sees rows added at the bottom of the sheet only.
With Make
Watch the sheet
Make a scenario. Add Google Sheets, Watch New Rows, pick the spreadsheet and the Sign-ups sheet, say the table has headers, and choose to start from rows added from now on, so the parents already in the sheet aren’t texted.
Send the text
Add BlueBot, Send Text. Connect it with an API key. Map To to the Mobile column and write Text with the First name and Class columns mapped in, as in the Zapier step above. Set Metadata source to google-sheets and row to the row number Make gives.
Write back
Add Google Sheets, Update a Row, with the row number from the first module, and map Texted at and BlueBot id from the Send Text module’s output.
Schedule it
Set the scenario to run every few minutes and turn it on.
With Apps Script
Apps Script runs inside your Google account and can call BlueBot itself. The script below looks through the sheet, texts each row that has a number and no Texted at, and writes back when it went. A Google Form can start it the moment a parent presses Submit; a timer catches rows typed in by hand.
Mark the rows already in the sheet
Before anything runs, type before BlueBot in the Texted at cell of every row that’s already there. The script skips any row with something in that cell, so the parents who signed up last month won’t get a text today.
Add the script
In the sheet, choose Extensions, then Apps Script. Replace what’s in the editor with this script and save it:
/**
* Texts each new sign-up from your own number through BlueBot, then writes when it went and the text's id back to
* the row. Columns: A Timestamp, B First name, C Mobile, D Class, E Texted at, F BlueBot id.
*/
const BLUEBOT_URL = 'https://usebluebot.com/api/v1/messages';
const SHEET_NAME = 'Sign-ups';
function textNewSignUps() {
const lock = LockService.getScriptLock();
if (!lock.tryLock(1000)) return; // the last run is still going; this row will be there next time
try {
const apiKey = PropertiesService.getScriptProperties().getProperty('BLUEBOT_API_KEY');
const sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME);
const rows = sheet.getDataRange().getValues();
for (let i = 1; i < rows.length; i++) {
const [timestamp, firstName, mobile, className, textedAt] = rows[i];
if (!mobile || textedAt) continue;
const rowNumber = i + 1;
// One key per sign-up: if a run sends and stops before writing back, the next run gets the same text back
// instead of sending a second one.
const stamp = timestamp instanceof Date ? timestamp.getTime() : rowNumber;
const idempotencyKey = ['sheets', sheet.getSheetId(), stamp, String(mobile).replace(/\D/g, '')].join('-');
const response = UrlFetchApp.fetch(BLUEBOT_URL, {
method: 'post',
contentType: 'application/json',
headers: { Authorization: 'Bearer ' + apiKey, 'Idempotency-Key': idempotencyKey },
payload: JSON.stringify({
to: String(mobile),
text: 'Hi ' + firstName + ', this is Luis at Northside Tutoring. You’re signed up for ' + className + '. Reply here with any questions.',
metadata: { source: 'google-sheets', row: String(rowNumber) },
}),
muteHttpExceptions: true,
});
const answer = JSON.parse(response.getContentText());
if (response.getResponseCode() === 201) {
sheet.getRange(rowNumber, 5, 1, 2).setValues([[new Date(), answer.id]]);
} else if (answer.error.code === 'rate_limited') {
return; // try the rest on the next run
} else {
// Leave the row for a person: the reason is in the sheet, and the row is skipped from now on.
sheet.getRange(rowNumber, 5).setValue('Not sent: ' + answer.error.message);
}
}
} finally {
lock.releaseLock();
}
}Give it your API key
In the Apps Script editor, open Project Settings (the gear), scroll to Script Properties, press Add script property, and add BLUEBOT_API_KEY with a key from BlueBot’s Developers, then API keys. The key stays in the script’s settings, out of the code and out of the sheet, where anyone the sheet is shared with could read it.
Run it once by hand
Pick textNewSignUps at the top of the editor and press Run. Google asks you to allow the script to read the sheet and call an outside service; allow it. Google shows a warning because you wrote the app yourself; choose Advanced and go on to your project.
Run it on its own
Open Triggers (the clock) and press Add Trigger:
- To text as soon as a Google Form is sent, choose the function
textNewSignUps, event source From spreadsheet and event type On form submit. - For rows typed in by hand, add a second trigger: event source Time-driven, Minutes timer, Every 5 minutes.
The two can run side by side. The lock keeps them from texting the same row at once, and the key keeps a row from being texted twice even if one run stops halfway.
Each run makes this request for every new row:
/v1/messagescurl -X POST "https://usebluebot.com/api/v1/messages" \
-H "Authorization: Bearer $BLUEBOT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: sheets-0-1762890000000-15555550123" \
-d '{
"to": "+15555550123",
"text": "Hi Maria, this is Luis at Northside Tutoring. You’re signed up for Algebra I on Tuesdays. Reply here with any questions.",
"metadata": {
"source": "google-sheets",
"row": "14"
}
}'Test it
Add a row at the bottom of the sheet with your own first name and your own mobile number (or send your own Google Form), then wait for the trigger or press Run.
| You got | It means | Do next |
|---|---|---|
A text on your phone, and a time and a msg_ id in your row | It works. | Delete your test row, or leave it: it won’t be texted again. |
Not sent: … in the Texted at cell | BlueBot refused the text, and the message says why: an empty or unreadable number, or a key without messages:send. | Fix the row, clear the cell, and the next run tries again. |
| A time and an id in the row, but no text yet | BlueBot accepted the text and it’s waiting: quiet hours, your Mac asleep, or its turn in the pacing. | Look the id up in Messages in the dashboard; its hold says why and until when. |
| Nothing in the row after the trigger ran | The run didn’t reach your row, or didn’t run. | In Apps Script, Executions lists every run with its log and any error. In Zapier, the Zap’s history says why it stopped. |
Not sent: The request carried no key… | The script property is missing or misspelled. | Check BLUEBOT_API_KEY in Project Settings. |
When it goes wrong
- Every row in the sheet got a text. The rows that were there before weren’t marked. BlueBot paces texts and stops at your daily limit for new people, so check Messages at once and cancel what’s still queued there. Then mark the old rows as in the first step.
- Rows sorted or inserted in the middle. Zapier and Make watch for rows added at the bottom; a row inserted in the middle or a sheet sorted after the fact can be missed or seen again. Apps Script reads the Texted at column, so it doesn’t care where a row is.
- A long list arrives at once. Pasting 300 rows is a broadcast, not a reply. Your sending rules space texts 20 seconds apart and hold new people over your daily limit, so the last ones go hours later or tomorrow. For a one-off list, use Broadcasts in the dashboard, which shows when the last text will go before you send.
- Somebody replied STOP. They’re on your stop list, and BlueBot cancels any text to them from now on. The script writes the reason into the row the next time it tries.