- loadExecution no longer reads trigger_outputs. It runs for every step,
but only the execution start reads the trigger payload. The start now
reads it with the new loadTriggerOutputs, in parallel with
loadExecution, so the start has no extra sequential round trip.
ExecutionRecord no longer carries triggerOutputs; NewExecutionRecord
still does.
- loadAllSteps becomes loadCompletedStepOutputs. The v1 step data loader
used only the node id, the iteration and the outputs of completed
steps, but it read all 12 columns of every step. The query now filters
on status = 'completed' and reads the three columns.
- These queries run as static named prepared statements. The
prepared-statement helper moves to database/prepared-query.ts and
borrows a pooled connection when it runs outside a transaction.
Durability and ordering guarantees do not change.
Co-authored-by: Cursor <cursoragent@cursor.com>
The TypeORM fork sent `SET search_path` (and `SET statement_timeout`,
when configured) every time it took a connection from the pool. That is
one extra round trip before every repository call and every transaction.
These settings last for the whole session, and no n8n code changes them
during a session. The driver now sends them once for each new pooled
connection and remembers that connection in a WeakSet.
This change is in the shared driver, so it applies to every n8n process
that uses PostgreSQL, not only engine v2. A trace of webhook -> 3 steps on
the engine data plane goes from 111 to 68 statements, and the SET runs
2 times instead of 45.
Co-authored-by: Cursor <cursoragent@cursor.com>
createSteps built its INSERT with one VALUES tuple per record, so the
statement text changed with the batch size. Now it passes one array for
each column through unnest, so the text is the same for every call.
Both createSteps statements (the execution-row lock and the insert) now
run as named prepared statements on the transaction's connection.
PostgreSQL parses and plans each one once per connection.
WITH ORDINALITY and ORDER BY keep the insert in input order, so
RETURNING still lists the created rows in the order of the records.
Named prepared statements need a direct connection or a session-mode
pooler. A transaction-mode PgBouncer before 1.21 does not support them.
Co-authored-by: Cursor <cursoragent@cursor.com>
Each queue on the data plane handles one message at a time, so every
sequential round trip adds to the time per execution.
- createSteps: move the failed-step test into the INSERT. The test still
runs as a statement after the execution-row lock, so it sees a failure
that commits while the insert waits for the lock. 5 round trips become 4.
- step:settled: read hasFailedSteps in parallel with loadStep and
loadExecution. A failure that lands after the probe still stops
planning, because createSteps refuses to insert after a failure.
- step:settled: skip refreshLiveStatus when it cannot change the status:
after the handler finishes the execution, and after it queues steps for
an execution that is running.
- v1 step data loader: load the execution and its steps in parallel.
Durability and ordering guarantees do not change. One visible effect:
if a branch suspends while another branch queues steps, the execution
can show 'waiting' instead of 'running' until the next settlement. The
engine treats both statuses as live, so only the reporting API sees it.
Webhook -> 3 NoOps: about 12 fewer sequential round trips on the
orchestrator and 3 fewer on the step worker per execution.
Co-authored-by: Cursor <cursoragent@cursor.com>