Find in Database

Action action.database_query

Reads rows from a local SQLite database that match the conditions you set. Read-only: nothing is ever written.

Find in Database is the read half of the pair, the node that answers "what did I already store?" Pair it with Save to Database: write a row today, and read it back next week.

You don't need the table to exist yet. A query against a table nobody has written to reports zero records instead of failing, so the first run of a flow that queries before it ever inserts still completes cleanly.

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

The clipboard-history flow

Write it down, then read it back. This works on the very first run, because a query against a table that doesn't exist yet reports zero records instead of failing.

Conditions

A list of conditions (a column, an operator, and usually a value) decides which rows come back. Leave the list empty and every row in the table matches. The ALL / ANY switch beside the Where heading decides whether every condition has to match or just one.

The eighteen operatorsThe value cell holds
equals · does not equalone value
contains · does not contain · starts with · ends withone value
is greater than · is at least · is less than · is at mostone value
older than (days) · within last (days)a number of days, see below
is one of · is not one ofa list, see below
is null · is not null · is empty · is not emptynothing; the cell shows a

There is no matches regex and no between: the first belongs in a Condition node on the payload, the second is two conditions. Three operators behave more helpfully than plain SQL would:

A list is a list, however it got there

is one of and is not one of compare against several values at once, and a list reaches them by exactly three routes:

  • You type them. The cell reads red, blue, green as three values: commas separate, spaces around them don't matter, and blanks are dropped.
  • One variable holds them. Write {{tags}} on its own and the variable's real array is unpacked, the parts a Split produced, an array a JSON Parse pulled out, a list an AI node returned. A variable holding a single plain value counts as a list of one. What does not work is a variable holding the text "red, blue": that is one value with a comma in it, and it matches a row only if a cell literally reads red, blue.
  • The AI Flow Builder writes them. A drafted flow supplies a real JSON array, for the same reason: a comma-joined string would match nothing.

An empty list is an answer rather than a mistake: is one of nothing matches no row, is not one of nothing matches every row.

Comparisons against a number compare numerically even when the column holds text; imported data is full of numbers stored as strings, and a text comparison would rank "10" below "9". Match capitalisation exactly is off by default, so equals, contains and friends ignore case.

Date conditions

older than (days) and within last (days) read a date column against a window of time. The value is a number of days (30, or a {{variable}} holding one), never a date. created_at, the managed stamp every table already has, works out of the box; a column of your own works too, as long as it holds an ISO-8601 timestamp. A column that isn't a date simply never matches, in either direction, and a window that isn't a positive number of days stops the run and names the condition.

See Delete from Database for the common pattern this pair enables: a nightly job that prunes anything past a retention window.

Narrowing a query

FieldWhat it does
ColumnsNothing picked means every column. Once Watchflows has seen the table it offers its columns as a checklist; before that, type the names you want, name, city.
Order ByOne column, with the / control for direction; created_at desc is the usual "newest first". Deliberately no richer than that: a multi-column or expression sort belongs in Run SQL. A column that isn't in the table is an error, not a silent no-sort.
Max Rows100 unless you change it. Ask for more than 5,000 and you get 5,000, with a line in the run log saying so.
Skip RowsHow many matching rows to step over first; Max Rows and Skip Rows together are how you page through a big table.

What this node hands downstream

KeyTypeDescription
recordsArray of objectsThe rows. A real array; Loop over it, or index it: {{records.0.body}}. Empty array when nothing matched, never a missing key.
firstObject or nullThe leading row, so a downstream field can just say {{first.body}}. Explicitly null on an empty result.
countNumberHow many records came back. Always the length of records; it never reports what the table holds.
columnsArray of stringsColumn names in selection order. A name that appears twice (a join) is suffixed _2, and that suffixed name is also the key inside each record.
columnTypesArray of stringstext, integer, real, blob or null, one per column.
truncatedBooleantrue when the result hit the ceiling below. truncated_reason then says row_limit or byte_limit.
truncated_reasonString or nullrow_limit, byte_limit, or null when nothing was cut.

Caps. This node returns 100 rows unless you raise Max Rows, and it never returns more than 5,000 rows or 8 MB. When a result is cut, it says so, in truncated, in the run log, and under the results table. Reading a whole table into a payload is not what this node is for; narrow it with conditions instead.

A run that returned rows also draws them as a table in the run log, under the step; sortable by clicking a header, with copy-as-TSV/CSV/JSON on the right-click menu.

A condition that doesn't resolve stops the run

A where-rule that cannot be worked out stops the run outright, because a condition that quietly vanished would change which rows come back. That covers three ways a rule can be unfinished: a missing variable, a value like clip-{{n}} where n isn't there (never rendered as clip-), and a rule left without a column or without a value.

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 conditions and their operators. It refers to a database by name, the way you do (for example "read from Clipboard History"), and Watchflows turns that name into the id. It is never handed an id, and it never invents one.