Save to Database
Actionaction.database_insert
Adds a row to a local SQLite database, creating the table and any missing columns automatically the first time it runs. No schema step, ever.
You do not need to design anything first. Point Save to Database at a table name that doesn't exist yet, and the table appears. Give it a field the table has never seen, and the column appears too. There is no schema step: you never write CREATE TABLE, never declare a column type, and never run a migration by hand.
Most flows are stateless: something happens, something else happens, and nothing remembers. Save to Database is how a flow remembers, with none of the setup a database normally asks for first.
Each database is one ordinary .sqlite file under Watchflows' Application Support folder. Open it in any SQLite tool you like; it is your file. Naming, privacy and deletion live in Databases.
Ports
| Direction | Name | Data Type | Description |
|---|---|---|---|
| Input | Input | Any | Incoming payload, passed through, and the source of every {{variable}} the node resolves |
| Output | Output | Any | The incoming payload merged with this node's result keys |
Your first row, start to finish
Nothing has to exist before you begin: no database, no table, no columns.
- Drop a Save to Database node on the canvas and open the Database field. On a Mac with none yet it reads No databases yet; choose New Database…, and a name is already filled in from the flow you're building. Press Create.
- Type a Table name. Any name works:
clips,readings,prices. It does not exist yet, and that is fine. - Under Fields to store, add a column and its value: the column is a name you invent, the value is a literal like
42or a{{variable}}from an earlier node. - Run the flow. The table, its columns, and the row all appear together.
Point a Find in Database node at the same database, name the same table, and you can read back what you just stored. Both nodes appear in Settings → Databases from the moment the database exists.
Example
The two columns Watchflows manages
Every table this node creates gets two columns you don't have to think about:
| Column | Type | What it holds |
|---|---|---|
| id | INTEGER PRIMARY KEY | An auto-numbering row id. This node reports the one it just wrote as rowId. |
| created_at | TEXT NOT NULL | A UTC ISO-8601 stamp (2026-08-20T21:04:11.238Z), written by SQLite itself. Sort on it with Order By created_at desc in Find in Database. |
They are deliberately unprefixed, so the database reads naturally in any SQLite tool. The cost is that those two names are reserved: a field called id or created_at (in any capitalisation) is refused, and the message tells you to rename it, for example item_id.
Values, and how they get their type
A field's value is either a variable or a literal, and the two are read differently on purpose.
| You write | Stored as | Why |
|---|---|---|
| {{text}} | whatever type it already was | A variable on its own keeps the upstream value's real type; a number stays a number. |
| {{first}} {{last}} | TEXT | A mixed template renders to text, like every other value field in the app. |
| 42 | INTEGER | A literal that reads as a whole number is one. |
| 3.5 | REAL | …and one with a decimal point is a real number. |
| "00123" | TEXT | Quotes are the escape hatch. A serial number that happens to be all digits keeps its leading zero. |
| true | INTEGER 1 | See the note below. |
SQLite has no boolean type. true is stored as 1 and reads back downstream as the number 1, not as true. That asymmetry is real and permanent, so match a flag with the condition equals 1, and test it downstream with {{done}} equals 1.
A field that doesn't resolve is skipped
If a field is exactly {{something}} and that variable isn't in the node's input, the field is left out: no column is created and no empty value is stored. The run log says which fields were skipped. If nothing resolves there is no row to write, and the node fails rather than storing a blank one.
How a table grows
This node shapes the table around what you actually store, in the order you listed the fields; that order is the column order you will see in every grid afterwards.
- The first run creates the table. Create the table if it doesn't exist is on by default. Turn it off and a missing table becomes an error instead, worth doing once a table matters, so a typo in the Table field can't quietly start a second one.
- A later run adds a missing column. Add a field to the node next month and the column appears on the next run. It is added without a declared type, because the first value it happens to see is a bad guess at every value after it; SQLite stores each cell as whatever it is.
- A table only auto-grows to 64 of your own columns (the managed pair doesn't count). Past that the node stops and points you at Run SQL, where you can shape a wide table deliberately.
Under More options, On a conflicting row decides what happens when the row you write collides with a unique index: Stop with an error (the default), Skip the row, or Replace the row. Nothing Watchflows creates for you is unique, so this only comes into play once you have added a UNIQUE index yourself with Run SQL; when a row is skipped, the node reports inserted as false with a null rowId rather than failing. Replace the row deletes the colliding row before writing the new one, so a node set to it is badged destructive, the same as Update in Database or Delete from Database.
What this node hands downstream
| Key | Type | Description |
|---|---|---|
| inserted | Boolean | Whether a row was actually written. false when a conflict rule skipped it. |
| rowId | Number or null | The managed id of the row just written, or null when nothing was. Store it and you can come back to that exact row later. |
| rowsAffected | Number | How many rows were written. 0 is a normal answer when a conflict rule skipped the row. |
Sharing a flow that uses a database
This node finds its database by id, not by name, so renaming a database in Settings never breaks a flow. That id is per-machine, though: when a flow arrives from somebody else (a Community install, an emailed .watchflow), its Database field is blanked and marked needs setup, and the flow can't be enabled until you pick a database. It keeps the author's name for it, so you can see what it was. Watchflows never invents a database for an id it doesn't recognise.
Asking the Flow Builder for one
The AI Flow Builder can author this node, including the table and the field list. It refers to a database by name, the way you do (for example "log it in Clipboard History"), and Watchflows turns that name into the id. It is never handed an id, and it never invents one.
- The match is exact, ignoring capitalisation; near misses are not guessed at, because the cost of writing to the wrong database is far higher than the cost of asking.
- Private and deleted databases are never listed to it and never resolve. Marking a database private hides it from the AI as thoroughly as from the picker.
- A name that matches nothing, including on a Mac with no databases yet, leaves the node marked needs setup. The flow is built; you pick the database. That is the same state an imported flow's node arrives in.
- Once a node is pointed at a database, chat cannot move it. Naming a different one in a later turn leaves the binding where it is and says so in the checklist; the node's own Database field is what changes it, because a node aimed at the wrong database writes real rows into the wrong place and reports success.