Skip to content

Build1 publisher3 min readPublished

n8n's Postgres node inlines $1 parameters as SQL literals

n8n 2.41.6's Postgres node writes $1 values into SQL text via pg-promise 11.9.1, so Postgres never gets a bind parameter. A psql PREPARE dry-run tests a path the node never uses, and Postgres types inlined literals before any CASE guard runs.

The Engineer · Build desk

Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

What happened

  • A CASE query guarding an empty string returned NULL under psql PREPARE and EXECUTE, but in n8n the same query failed with invalid input syntax for type integer.
  • Because the node's On Error setting was Continue (using regular output), the run ended green: no retry, no alert, and no saved callback.
  • pg-promise formats values on the client by default unless the pgFormatting option is set, and n8n creates its pg-promise instance without that option.
  • In the same workflow, a comma inside a free-text value shifted every later parameter one column over, and the row saved without an error.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • exposure Any Postgres node set to Continue can drop a write while the execution log shows success. A green run does not confirm the row was written.
  • contradiction The docs promise prepared statements and injection safety; the author found the injection promise holds, while the prepared-statement wording describes binding the node never performs.
  • decision Teams that pass free text through Query Parameters have to build the field as an array expression, since only non-array strings are split on their commas.

The failure comes down to when Postgres assigns a type. On a real bind parameter, $1::int is a run-time cast, and it runs only when its branch runs [12]. pg-promise instead escapes the value and writes it into the SQL text, so the server receives make_interval(mins => ''::int) inside one plain string [8]. The Postgres documentation says a cast on an unadorned string literal "represents the initial assignment of a type to a literal constant value" [13]. According to the post, Postgres hands the empty string to the integer input routine while it is still analysing the statement, and the query fails before the CASE is evaluated [14].

The same documentation warns that CASE "is not a cure-all" and "does not prevent early evaluation of constant subexpressions" [15]. Under client-side formatting, every parameter reaches the server as a constant [8].

The author's reproduction also works as a test method. Against a local Postgres 16.15 with pg-promise 11.9.1, pgp.as.format produced the inlined query, and running it returned error 22P02 [9]. Sent as a real parameter through pg-promise's ParameterizedQuery, the same SQL returned {"delay": null}, matching the psql result [10]. A psql PREPARE exercises the bind path. n8n's node does not take that path, so formatting the query with pgp.as.format and executing the output is the closer dry-run [1].

The escaping itself is sound: the author checked the docs' claim that the node "sanitizes data in query parameters, which prevents SQL injection" and reported that it holds up [11]. The typing failure is a separate issue. The docs' "prepared statements" wording [1] describes server-side binding that, per the post, never happens [8].

Query Parameters has a second client-side transformation. From node version 2.5 onward (2.7 is what new nodes get), the code in executeQuery.operation.ts evaluates every {{ }} expression on its own [17]. Arrays are taken item by item. Any other result becomes a string and, unless it parses as JSON, goes through stringToArray [17]. That function splits on commas, drops empty pieces and trims the rest [18]. The docs call the field "a comma-separated list of values" [19]. "They don't mention that the split happens after your expressions are evaluated, inside your data," the author wrote [20].

All of this comes from one practitioner's post, backed by code excerpts and a local reproduction against the n8n release that was current stable when it was written [6]. The post does not include a response from n8n. The author caught the callback failure only because they were stepping through the test by hand [21]. In my setup I would treat any Postgres node set to Continue as unverified until a later step confirms the row was written.

What to watch

  • An n8n release after 2.41.6 that sets pgFormatting on its pg-promise instance or rewords the 'prepared statements' line in the Postgres node docs.
  • A change to stringToArray in executeQuery.operation.ts so evaluated values are no longer split on commas inside the data.
  • A response from n8n confirming or disputing the client-side formatting path the post describes.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories