At a glance
- Takes
- Any payload — its variables become the statement's bound values
- Emits
- A read's keys for a SELECT, rowsAffected and rowId for a write
- Good for
- Joins, GROUP BY, indexes, views, a table you want to shape by hand
Set it up
The other four Database nodes cover the everyday shapes with no SQL at all. Reach for this one when you want the real thing. Write the statement, use {{variables}} wherever a value goes, and click ⇲ on the SQL field to open it in the Workbench with your results beside it.

SELECT body, created_at
FROM clips
WHERE source = {{app}}
ORDER BY created_at DESC
Configure
| Field | Default | What it does |
|---|---|---|
| Database | — | Which database the statement runs against. The picker fills in the id. |
| SQL | — | One statement. A trailing semicolon is fine; two statements are not. |
Because values are bound, there are two places a variable cannot go. A name is one: SELECT * FROM {{table}} can't work, so type the table name. Inside quotes is the other: LIKE '%{{q}}%' would store the braces as text. Write a contains-search outside the quotes instead — instr(lower(body), lower({{q}})) > 0. If a variable is read as a name where you meant a value, wrap it in its own parentheses: ({{x}}) always means "this is a value".
Output
The shape depends on what you wrote: a SELECT hands on the read keys, a write hands on rowsAffected and rowId.
| Key | Type | Value |
|---|---|---|
records | Array | SELECT: the rows, as a real array of objects — {{records.0.body}} |
first | — | Not emitted here. A write has no leading row, so first stays a Find in Database promise |
count | Number | SELECT: how many records came back |
columns | Array | SELECT: column names in selection order; a name selected twice is suffixed _2 |
columnTypes | Array | SELECT: text, integer, real, blob or null, one per column |
truncated | Boolean | SELECT: true when the result hit the 5,000-row or 8 MB ceiling |
truncated_reason | String | row_limit or byte_limit; null when nothing was cut |
rowsAffected | Number | How many rows the statement changed. 0 is a normal answer |
rowId | Number | The managed id of the row an INSERT wrote, or null when it wrote none |
Example: count the clippings
Input runs the flow. Run SQL groups clips by source and counts each one. Log prints the rows.

Paste this into the Flow Builder to get the same flow:
Run a SQL statement against my Clipboard History database that counts rows in clips grouped by source, then log the results.
Good to know
Run SQL is always badged destructive. The statement is yours to write, including UPDATE, DELETE and DROP TABLE, none of which can be undone.
A handful of statements are turned away before SQLite sees them, each with a plain-English reason: ATTACH / DETACH, writing pragmas (read-only ones such as table_info stay), triggers, virtual-table modules, BEGIN / COMMIT / ROLLBACK / SAVEPOINT, rewriting SQLite's own sqlite_ tables, and the functions that would reach the rest of your Mac. Everything else you would expect is there — CREATE TABLE, ALTER TABLE, CREATE INDEX, views, DROP.
A hand-typed ? or :name is refused, because every value comes from a {{variable}}. A variable the input doesn't have stops the run and names the keys that are available — in SQL, an empty string would change which rows the statement touches.
A statement stops on its own after 30 seconds, so a runaway join can't hold a flow open. A variable inside a -- or /* */ comment is left alone.