Skip to content
Expedify
6 min

Database Query

Ask your organisation's own database a question no node quite answers. Read versus write mode, how results come back, and the safety rails worth keeping on.

The CRM Manager node answers most questions about your data: find a contact, list deals, search tasks. What it cannot do is the question with a shape — contacts with no activity in sixty days who still have an open deal, counted by owner.

Database Query runs SQL against your organisation's own database and hands the rows back to the workflow.

Try CRM Manager first, every time. A search there is safer, readable by anyone, and survives a schema change. Reach for SQL when the question genuinely needs a join, an aggregate, or a filter on a custom field — which crm_manager search cannot do.

A worked example

Monday morning, a list

The builder composes the SQL; this node runs it.

Scroll for all 4 steps →

The query here is not typed into the node — it is {{sqlbuilder_1.final_sql}}, coming from the node before it. That pairing is the subject of the next lesson; this one is about what happens when the SQL runs.

The fields worth setting deliberately

Eight of the seventeen. The rest cover external connections, transactions and retries.

use_current_org_db

What it holds
Use current organization's database Defaults to true.

query

What it holds
SQL query to execute (supports template variables)

query_mode

What it holds
Query execution mode: read (SELECT only) or write (INSERT/UPDATE/DELETE) One of: read · write Defaults to read.

parameters

What it holds
Query parameters for safe binding

result_format

What it holds
Format for query results One of: array_of_objects · array_of_arrays · single_value · raw Defaults to array_of_objects.

max_rows

What it holds
Maximum number of rows to return Defaults to 1000.

empty_result_handling

What it holds
How to handle empty query results One of: null · empty_array · error Defaults to empty_array.

timeout

What it holds
Query timeout in seconds Defaults to 30.

query_mode

Why it matters
Read is SELECT only and is the default. Write is a deliberate act, and the moment a workflow can change data it cannot un-change.

max_rows

Why it matters
A ceiling, 1000 by default. A query that quietly returns 40,000 rows will take everything downstream with it.

empty_result_handling

Why it matters
What “nothing matched” looks like: an empty array, null, or a raised error. Pick the one your next node can actually handle.

timeout

Why it matters
Thirty seconds. A slow query holds the run open, so this is worth lowering for anything on a busy trigger.

Results arrive at {{databasequery_1.data}} — an array of objects in the default format, which is the shape a Loop wants. So “run a query, do something per row” is these two nodes and nothing else.

Parameters, not string-building

The query field takes templates, so it is tempting to paste a value straight into the SQL. Use parameters instead — it binds values safely rather than splicing them into the statement.

two ways to write the same filter
-- Fragile: whatever the trigger holds becomes part of the statement
SELECT id, email FROM contacts WHERE lead_status = '{{trigger.new_data.status}}'

-- Bound: the value stays a value
SELECT id, email FROM contacts WHERE lead_status = :status

The risk is not only the classic injection one. A value containing an apostrophe — a company called O'Brien & Co — breaks a spliced query outright, and that is the version you will actually meet.

Write mode

Write mode bypasses everything the CRM does around a change. An UPDATE here does not fire database triggers the way the app does, does not update the vector index, and does not leave the audit trail a CRM Manager write leaves. Use crm_manager for changes to CRM records, and keep write mode for the rare thing that genuinely has no node — a bulk correction, a table the CRM does not own.

If you do use it, set use_transaction so a half-finished statement rolls back rather than leaving the data halfway.

Try it

  1. Add a Database Query with use_current_org_db on and a SELECT that returns a handful of rows.
  2. Read {{databasequery_1.data}} in a Loop and print one field per row.
  3. Change the filter so nothing matches, and see what empty_result_handling gives your next node.
  4. Set max_rows to 2 against a query you know returns more, and confirm it truncates rather than failing.

Next: SQL Builder — composing that query without writing SQL by hand.

Related lessons