Four steps read and write your tables: Look up, List, Save and Delete. Each one branches on what happened, so a flow can tell a returning customer from a new one, or an empty menu from a full one, without you writing any conditions.
Finds one row by the table's key and puts it into the conversation. Pick a table, then give a value for each of its key columns, usually something the visitor just told you.
Table: Leads
Email: {{ask_email}}
It continues down one of three branches:
Not found is not a failure. It is how you tell a returning customer from a new one. Send Found to a short welcome back, and Not found to the questions you would ask anyone.
A match publishes the row under the step's variable, with each column available by name:
Welcome back {{find_lead.name}}.
We still have your budget down as {{money find_lead.budget}}.
Numbers are worth passing through a formatting helper as above, otherwise a budget of 31500 reads as 31500.0 in the middle of a sentence. See templating for the rest.
You can also reach {{find_lead.created_at}} and {{find_lead.updated_at}}.
Reads several rows at once, for the things you keep in a table rather than the record you keep about one person: a menu, a price list, the sizes you have in stock. The table does not need a key, so an append-only one works.
Choose which columns you want back, up to five. Leave it alone and you get the ones the table shows in its own rows list, which is usually the right answer to the same question.
The rows come back ready to loop over:
Today we have:
{{#each menu.rows}}
{{this.name}} for {{money this.price}}
{{/each}}
You can narrow it to rows matching one column, so a flow can offer the mains rather than the whole menu, or only what is still in stock. Pick the column, how to compare it, and what to compare it against, which can be something the conversation just collected.
| Comparison | Matches |
|---|---|
| is | Exactly that value. category is Mains. |
| is not | Anything but that value. |
| contains | Part of it. name contains chicken. Text and dates only, so due contains 2026-08 reads as "in August". |
| is more than | Larger, later, or further down the alphabet. |
| is less than | Smaller, earlier, or further up it. |
Numbers compare as numbers, so 9 is less than 100 rather than after it. A value that comes out empty matches nothing rather than everything, so a question the visitor skipped does not quietly return the lot. Encrypted columns cannot be matched on, for the same reason they cannot be ordered on.
Rows come back newest first unless you order them by a column, which is usually what a menu or a price list wants. Which way round is named for what the column holds: oldest or newest first for a date, smallest or largest for a number, A to Z for words. Without a column you are ordering by when a row was added, so you can ask for oldest first as easily as newest. If that column is later dropped they simply come back newest first again, rather than the step failing: they are still the right rows, only the order is not what you asked for.
It branches on Found, Empty, and Error. An empty table is not a failure: a menu with nothing on it is still a menu, and Empty is where you say so.
Ask for what you will print. A step returns up to 100 rows and 25 by default, and everything it returns is carried in the conversation from then on. A long description you are not going to show is worth leaving out of the columns, and a hundred things is not a list anyone reads in a chat message.
You can also reach {{menu.count}}, which is useful for saying "we have four left" without printing them.
Writes a row. Pick the column each value goes into, and say where the value comes from:
Table: Leads
email <- {{ask_email}}
name <- {{ask_name}}
budget <- {{ask_budget}}
Values are addressed to the column itself rather than to its name, so renaming that column later leaves the step working. If you drop a column a step was writing to, the step keeps saving everything else and tells you the field is gone.
Because the key identifies a row, saving twice against the same key does not make two rows. You choose what happens when the key already exists:
| Setting | What it does |
|---|---|
| Create or update | Writes either way. Updating touches only the columns this step maps. The usual choice. |
| Create only | Leaves an existing row untouched. Use when the first answer is the one that counts. |
| Update only | Changes a row that is already there and adds nothing new. |
Save branches on Saved, Skipped when your setting deliberately left the row alone, and Error.
Removes a row, found by the key the same way Look up finds one. This is how a conversation honours a request to be forgotten, cancels a booking, or releases a slot someone was holding.
It branches on Deleted, Nothing to delete, and Error.
Nothing to delete is not a failure either. Someone asking to be forgotten twice should be told yes both times.
It removes one row or none. There is no way to say "delete everything matching", and a key value that comes out empty takes Nothing to delete rather than guessing. Deleting frees the space immediately.
All four steps work in a trigger as well as in a conversation. None of them sends a message or waits for a reply, so there is nothing for them to wait on when no one is there.
That makes a trigger a good place to tidy up: record how a conversation ended, or clear a row once it has served its purpose.