Postgres
Declare PostgreSQL connections and table schemas for use in workflow nodes.
A postgres block describes an external PostgreSQL database you operate. Declare its connection and table row schemas, then use type: postgres nodes to run parameterized SELECT, INSERT, UPDATE, or DELETE queries. Store connection credentials in a secret block.
For a database reachable only through a tailnet, reference a Project Network with network: <name>. The connection fails closed until that network has an execution gateway binding.
Use a stream to persist workflow output, or a managed database when Swirls should provision and migrate PostgreSQL for you.
Declaring a postgres block
There is no type: field: the keyword postgres identifies the block.
| Field | Required | Description |
|---|---|---|
label | No | Human-readable label. |
secrets | No | References a top-level secret block; bare connection identifiers are validated against that block’s vars. |
connection | Yes | Connection string: a bare identifier (secret ref) or a quoted literal for testing (the validator warns on literals in production-oriented workflows). |
table <name> { } | Yes (≥1) | Each nested block declares a table name and a JSON Schema for one row. |
postgres my_db {
label: "CRM database"
secrets: project_secrets
connection: SHARED_DATABASE_URL
table leads {
schema: @json {
{
"type": "object",
"properties": {
"id": { "type": "string" },
"email": { "type": "string" },
"score": { "type": "number" }
},
"required": ["id", "email"]
}
}
}
}Table names in the DSL are unqualified (for example leads); runtime targets your database's public schema.
Three connection modes work:
- Secret-block reference:
secrets: my_secretsplusconnection: DATABASE_URL. The identifier is validated against the block'svars. - Project secret:
connection: DATABASE_URLwith nosecrets:. The bare identifier resolves as a project secret. - Literal:
connection: "postgresql://...". The validator warns; use it for testing only.
Declared table schemas drive validation and editor tooling only. They are never enforced against the live database, so a drifted schema surfaces as a query-time error, not a deploy-time one. Postgres nodes run in deployed projects on Swirls Cloud.
Table declarations and tooling
Each table block’s schema is JSON Schema for a single row. The language service uses it to:
- Validate that explicit
INSERT INTO t (col1, col2)column names exist on the declared table. - Infer TypeScript types for
paramsreturn objects and for{{key}}hovers from SQL (for example, which column a placeholder maps to).
Select nodes (select:)
Use exactly one of select:, insert:, update:, or delete: on a type: postgres node. These are the only four statement kinds: there is no arbitrary-statement node. For reads, use select: with a SELECT (or WITH) statement.
| Field | Required | Description |
|---|---|---|
postgres | Yes | Name of a top-level postgres block. |
select | Yes | @sql block: must read as a SELECT (or WITH) statement. |
params | Optional; required when the SQL contains {{key}} placeholders | @ts block returning a plain object whose keys match those placeholders. |
schema / outputSchema | Recommended | JSON Schema for the array of result rows. |
node load_leads {
type: postgres
label: "Active leads"
postgres: my_db
select: @sql {
SELECT id, email, score
FROM leads
WHERE score >= {{min_score}}
}
params: @ts {
return {
min_score: context.nodes.root.output.threshold
}
}
schema: @json {
{
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"email": { "type": "string" },
"score": { "type": "number" }
}
}
}
}
}Node output is an array of row objects (query result rows).
Insert nodes (insert:)
For writes, use insert: with an INSERT statement. params: is required.
| Field | Required | Description |
|---|---|---|
postgres | Yes | Name of a top-level postgres block. |
insert | Yes | @sql block: an INSERT statement. Upsert via ON CONFLICT is allowed. |
params | Yes | @ts block returning a plain object whose keys match {{key}} in the SQL. |
condition | No | @ts block returning a boolean; if false, the insert is skipped. |
Wrap row values in VALUES (...): the validator requires VALUES to be followed by parentheses around the value list.
node upsert_lead {
type: postgres
postgres: my_db
condition: @ts {
return context.nodes.classify.output.score > 20
}
insert: @sql {
INSERT INTO leads (name, email, score)
VALUES ({{name}}, {{email}}, {{score}})
}
params: @ts {
return {
name: context.nodes.root.output.name,
email: context.nodes.root.output.email,
score: context.nodes.classify.output.score
}
}
}Typical output shape is { executed: true } or, when condition is false, { skipped: true }.
Update nodes (update:)
To modify existing rows, use update: with an UPDATE statement. params: is required when the SQL contains {{key}} placeholders; a static statement may omit it.
| Field | Required | Description |
|---|---|---|
postgres | Yes | Name of a top-level postgres block. |
update | Yes | @sql block: an UPDATE statement. |
params | When the SQL has {{key}} placeholders | @ts block returning a plain object whose keys match those placeholders. |
condition | No | @ts block returning a boolean; if false, the update is skipped. |
node refresh_score {
type: postgres
postgres: my_db
update: @sql {
UPDATE leads
SET score = {{score}}
WHERE email = {{email}}
}
params: @ts {
return {
score: context.nodes.classify.output.score,
email: context.nodes.root.output.email
}
}
}An UPDATE without a WHERE clause modifies every row in the table, so the validator warns on it. Explicit SET col = ... column names are validated against the declared table schema, the same way explicit INSERT column lists are.
Delete nodes (delete:)
To remove rows, use delete: with a DELETE statement. The same params: and condition: rules as update: apply.
| Field | Required | Description |
|---|---|---|
postgres | Yes | Name of a top-level postgres block. |
delete | Yes | @sql block: a DELETE statement. |
params | When the SQL has {{key}} placeholders | @ts block returning a plain object whose keys match those placeholders. |
condition | No | @ts block returning a boolean; if false, the delete is skipped. |
node purge_stale_leads {
type: postgres
postgres: my_db
condition: @ts {
return context.nodes.root.output.confirmed === true
}
delete: @sql {
DELETE FROM leads
WHERE score < {{min_score}}
}
params: @ts {
return { min_score: context.nodes.root.output.threshold }
}
}A DELETE without a WHERE clause removes every row in the table, so the validator warns on it.
Like insert nodes, update and delete nodes produce { executed: true } or { skipped: true }; they return no rows.
Parameter interpolation ({{key}})
Values are not concatenated into the SQL string. At runtime, each {{key}} is replaced with a positional placeholder ($1, $2, …) and the corresponding value is sent as a bound parameter: so user data never becomes raw SQL text.
In the DSL, params: returns a named object. Placeholder names in SQL and keys in that object must match when placeholders exist. Those names do not need to match JSON Schema property names on the row: types are inferred from SQL position (for example, which column appears in INSERT INTO t (a,b) VALUES ({{x}},{{y}})) or from simple comparison patterns (for example WHERE col = {{key}} on SELECT, UPDATE, or DELETE, and SET col = {{key}} on UPDATE).
Type safety in the editor
When table row schemas are declared and SQL is parseable by the tooling, you get:
- Hover types on
{{key}}inside@sqlblocks. - Type checking on the object returned from
params@tsblocks. - An index signature on the inferred params type so extra keys remain valid when you map names freely.
The Node types reference lists all postgres node fields in one place.