Delete from Database
Actionaction.database_delete
Removes the rows that match your conditions from a local SQLite database. Rows that don't match are left exactly as they were.
Delete from Database is the node behind every retention rule and cleanup job: prune what's old, remove what's stale, keep a table from growing forever. It only ever removes rows your conditions match, and it will not guess at "the rest of the table" for you.
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 of the database itself 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 |
No conditions means the node refuses to run. A blank condition list would reach every row, so this node stops instead of guessing what you meant. If that really is what you want, turn on No conditions — reach EVERY row; it is a deliberate switch, and the node is badged destructive while it is on.
Conditions
A list of conditions (a column, an operator, and usually a value) decides which rows are removed. The ALL / ANY switch beside the Where heading decides whether every condition has to match or just one.
| The eighteen operators | The value cell holds |
|---|---|
| equals · does not equal | one value |
| contains · does not contain · starts with · ends with | one value |
| is greater than · is at least · is less than · is at most | one value |
| older than (days) · within last (days) | a number of days, see below |
| is one of · is not one of | a list, see below |
| is null · is not null · is empty · is not empty | nothing; 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:
- equals finds rows holding nothing when you compare against nothing. Plain SQL
=never matches NULL, so "equals {{maybe}}" would silently match no row at all. - contains treats
%and_as ordinary characters. Searching for100%finds "100% done", not everything. - is not one of keeps rows whose value is empty. Plain
NOT INdrops exactly the rows you meant to keep.
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, greenas 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 readsred, 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.
Dates, and keeping a table from growing forever
Two conditions read a date column against a window of time: older than (days) and within last (days). The value is a number of days (30, or a {{variable}} holding one), never a date. Together they split a table in two: every row is in exactly one of the windows.
That is what a retention policy is, without a line of SQL. A Schedule trigger every night into a Delete from Database whose only condition is created_at older than 30 keeps a clipboard history at a month, forever, and reports how many rows it removed as rowsAffected.
created_atworks out of the box. It is the managed stamp every table already has, so a retention rule needs nothing set up first.- A date column of your own works too, as long as it holds an ISO-8601 timestamp; store
{{timestamp}}from a trigger, or any2026-08-20T21:04:11Z-shaped text, and the same two conditions read it. Both sides of the comparison go through SQLite's date parser, so the stored stamp and the window are compared as moments, not as text. - A value that isn't a date simply never matches. Point one of these at a column of names or numbers and it selects no rows, in either direction. Nothing is guessed at.
- A window that isn't a positive number of days stops the run and names the condition.
last week,30 days,0and a variable the input doesn't have are all refused rather than turned into some other window, the same rule every condition follows, for the same reason: this one decides which rows are removed.
Anything more particular than "N days" (a calendar month, a range between two dates, a time of day) belongs in Run SQL, where SQLite's whole date() and datetime() vocabulary is available.
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 a delete reaches. 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.
What this node hands downstream
| Key | Type | Description |
|---|---|---|
| rowsAffected | Number | How many rows were removed. 0 is a normal answer; it means the conditions matched nothing. |
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, 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 deleting from 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.
- 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 removes real rows from the wrong place and reports success.