Run a small CRM in Airtable that texts
Keep your clients in an Airtable table, text any of them from your own number by ticking a box or changing their status, and get their replies, their stops and each text’s state written back to the row.
Nina runs Suds on Wheels, a mobile dog-grooming van, and keeps her clients in one Airtable table: the owner, the dog, the phone number and when the next groom is due. When a groom is due she copies the number into her phone and types the same text forty times a month. With this guide, she ticks Send text on a row (or moves a client to Reminder due) and the text goes from her own number. The client’s reply, a STOP, and whether the text went all come back to the same row.
What you’ll have
- A Send text box on each row. Tick it and the row’s text goes from your number within seconds.
- The same automation on a status change, so moving a client to Reminder due texts them.
- Each reply written to the row with its time, the row’s Status set to Replied, and Stopped when someone texts STOP.
- Each text’s state (
sent,delivered,failed) on the row, so you can see who never got theirs.
What you need
- A BlueBot account with your Mac connected.
- An Airtable base you can add fields and automations to, on a plan that includes the Run a script automation action.
- A Make account for writing replies back. Make’s free plan is enough for a small table.
How it fits together
Airtable texts through a BlueBot Text Hook: an address at BlueBot that turns each post it gets into a text. An Airtable automation posts the row to the hook, so no BlueBot API key sits in your base. Replies and states come back through BlueBot’s Make app, which hears about them the moment they happen and updates the row.
The row’s Airtable record ID travels with the text as . BlueBot echoes it on every update about that text, so the state always finds its row, however the phone number is written.
Set up the table
Make a table called Clients (or add these to yours). Names matter, because the steps below refer to them.
| Field | Type | What it holds |
|---|---|---|
| Name | Single line text (primary) | “Bella (Rosa Diaz)” |
| First name | Single line text | “Rosa” |
| Mobile | Phone number | Any format: (555) 555-0123 is fine. |
| Mobile digits | Formula | RIGHT(REGEX_REPLACE({Mobile}, "[^0-9]", ""), 10), the last ten digits, for matching replies. |
| Status | Single select | New, Reminder due, Texted, Replied, Booked, Stopped |
| Text to send | Long text | The words for this row’s next text. |
| Send text | Checkbox | Tick to send. The automation unticks it. |
| Last text status | Single line text | Filled in by Make. |
| Message ID | Single line text | Filled in by Make, msg_…. |
| Last reply | Long text | Filled in by Make. |
| Last reply at | Date, with time | Filled in by Make. |
Text only clients who agreed to hear from you by text. A column such as OK to text (checkbox) that the automation checks is a good habit.
Make the Text Hook
In the BlueBot dashboard, open Text Hooks, press New hook, name it “Airtable clients” and leave the preset empty. Copy the hook’s address, https://usebluebot.com/h/ followed by a long token. Anyone with it can make your hook text somebody, so keep it as you would a password.
Set the mapping (the next step sends a sample post so you can pick fields, or type the paths now):
| Text Hook setting | Set it to |
|---|---|
| Recipient | {{body.phone}} |
| Text | {{body.text}} |
| Metadata | record_id set to {{body.record_id}}, and source set to airtable |
| Create a contact | On, with the tag airtable |
The text is the row’s own Text to send, so each client can get different words. Save the hook.
Add the automation that sends
In Airtable, open Automations and press Create automation. Name it “Text the client”.
- Trigger: When a record matches conditions, table Clients, conditions: Send text is checked, Mobile is not empty, and Status is not Stopped.
- Action: Run a script. Add four input variables from the trigger’s record:
recordId(the record’s ID),phone(Mobile),firstName(First name) andtext(Text to send). Paste this script and put your hook’s address inHOOK_URL:
// Posts this row to BlueBot's Text Hook, which texts it from your own number.
const HOOK_URL = 'https://usebluebot.com/h/YOUR_HOOK_TOKEN'; // anyone who can edit automations can read this
const { recordId, phone, firstName, text } = input.config();
if (!phone || !text) {
throw new Error('This row has no Mobile or no Text to send.');
}
const response = await fetch(HOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ record_id: recordId, phone, first_name: firstName, text }),
});
if (!response.ok) {
throw new Error(`BlueBot answered ${response.status}. Open the hook in BlueBot to see why.`);
}- Action: Update record, table Clients, record ID from the trigger. Set Send text to unticked and Status to Texted.
Unticking Send text takes the row out of the condition, so ticking it again later sends again. A failed run leaves the box ticked, which tells you which rows didn’t go.
To text on a status change instead, make a second automation with the condition Status is Reminder due and the same two actions.
Turn the automation on.
Write replies, stops and states back with Make
In Make, make a scenario called “BlueBot replies to Airtable”:
- BlueBot’s Watch Incoming Texts module, connected with a BlueBot API key that has
messages:readandwebhooks:manage(the module makes its own BlueBot webhook). - Airtable’s Search Records on Clients, with this formula, putting BlueBot’s From where the quotes are:
{Mobile digits} = RIGHT("From", 10). Set Limit to 1. - Airtable’s Update a Record with the Record ID from the search. Set Last reply to BlueBot’s Text, Last reply at to Received At, and Status to Replied.
Make a second scenario, “BlueBot states to Airtable”:
- BlueBot’s Watch Text Status Changes module.
- A filter that passes only texts whose Metadata has
record_id, so texts from elsewhere are left alone. - Airtable’s Update a Record with Record ID set to Metadata: record_id. Set Last text status to State and Message ID to ID.
And a third, “BlueBot stops to Airtable”:
- BlueBot’s Watch Contact Stops module.
- Airtable’s Search Records with
{Mobile digits} = RIGHT("Handle", 10), putting the contact’s Handle where the quotes are. - Airtable’s Update a Record, setting Status to Stopped.
Turn all three scenarios on. They run when BlueBot tells them something happened, not on a timer.
Test it with your own number
Add a row for yourself: your own first name, your own mobile number, and a short Text to send (“Test from Airtable”). Tick Send text.
| You got | It means | Do next |
|---|---|---|
| A text on your phone, Send text unticked and Status at Texted | The sending half works. | Wait a few seconds for Last text status. |
Last text status shows delivered or sent | The state half works. SMS stops at sent; iMessage goes on to delivered. | Reply from a second phone of your own to test replies. |
| Send text still ticked and the automation’s run history shows an error | The script couldn’t post, or BlueBot refused the post. | Read the error in the run history. A 404 means the hook’s address is wrong. |
The text is in Messages in the dashboard with a hold | It’s accepted and waiting, often for quiet hours at night. | It goes on its own when the hold ends. |
| Your reply came but the row didn’t change | The search found no row, often because Mobile digits isn’t ten digits. | Check the formula field on your row. |
When it goes wrong
- Ticking twenty boxes sends twenty texts slowly. That’s pacing: your Mac sends one text every 20 seconds by default, the way a person would. They all go; the last one about seven minutes after the first.
- A reply from somebody not in the table. The search finds nothing and Make stops that run, which is what you want. Their text is still in BlueBot’s inbox and on your phone.
- Two rows share a number (two dogs, one owner). The reply goes to whichever row the search finds first. Give each owner one row and list the dogs in it, or tell Make which to pick by a second condition.
- You changed the hook’s address. Update
HOOK_URLin the script. The old address stops working as soon as you make a new one.