Save to Database

Action action.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

DirectionNameData TypeDescription
InputInputAnyIncoming payload, passed through, and the source of every {{variable}} the node resolves
OutputOutputAnyThe 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.

  1. 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.
  2. Type a Table name. Any name works: clips, readings, prices. It does not exist yet, and that is fine.
  3. Under Fields to store, add a column and its value: the column is a name you invent, the value is a literal like 42 or a {{variable}} from an earlier node.
  4. 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:

ColumnTypeWhat it holds
idINTEGER PRIMARY KEYAn auto-numbering row id. This node reports the one it just wrote as rowId.
created_atTEXT NOT NULLA 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 writeStored asWhy
{{text}}whatever type it already wasA variable on its own keeps the upstream value's real type; a number stays a number.
{{first}} {{last}}TEXTA mixed template renders to text, like every other value field in the app.
42INTEGERA literal that reads as a whole number is one.
3.5REAL…and one with a decimal point is a real number.
"00123"TEXTQuotes are the escape hatch. A serial number that happens to be all digits keeps its leading zero.
trueINTEGER 1See 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.

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

KeyTypeDescription
insertedBooleanWhether a row was actually written. false when a conflict rule skipped it.
rowIdNumber or nullThe managed id of the row just written, or null when nothing was. Store it and you can come back to that exact row later.
rowsAffectedNumberHow 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.