Configure PostgreSQL Stored Procedure Reaction
Invoke PostgreSQL stored procedures when query results change
6 minute read
The PostgreSQL Stored Procedure
Reaction
Reaction
A component that receives query result changes and takes action on them.
Learn more
invokes PostgreSQL stored procedures when query
results change
Result Change Event
A notification from a Continuous Query describing changes to its result set.
Learn more
. Use it to synchronize query result changes into a PostgreSQL database by calling a different, configurable stored procedure for added, updated, and deleted changes.
Row data is bound as safe positional SQL parameters, so untrusted values can never alter the command structure. The reaction supports per-query template overrides, a shared default template, SSL, a configurable command timeout, and automatic retries with exponential backoff.
Basic Configuration
reactions:
- kind: storedproc-postgres
id: user-sync
queries: [user-changes]
hostname: localhost
port: 5432
database: mydb
user: postgres
password: ${DB_PASSWORD}
defaultTemplate:
added:
template: "CALL add_user({{param after.id}}, {{param after.name}}, {{param after.email}})"
updated:
template: "CALL update_user({{param after.id}}, {{param after.name}}, {{param after.email}})"
deleted:
template: "CALL delete_user({{param before.id}})"
Configuration Reference
| Field | Type | Default | Description |
|---|---|---|---|
kind |
string | Required | Must be storedproc-postgres |
id |
string | Required | Unique reaction identifier |
queries |
array | Required | Query IDs to subscribe to |
autoStart |
boolean | true |
Start reaction automatically |
hostname |
string | localhost |
Database hostname or IP address |
port |
integer | 5432 |
Database port |
user |
string | Required | Database user |
password |
string | Required | Database password |
database |
string | Required | Database name |
ssl |
boolean | false |
Enable SSL/TLS |
defaultTemplate |
object | None | Fallback templates applied to all queries |
routes |
object | {} |
Per-query template overrides |
commandTimeoutMs |
integer | 30000 |
Command timeout in milliseconds |
retryAttempts |
integer | 3 |
Number of retries on failure |
Template Configuration
A template configuration (defaultTemplate or a routes entry) supplies a stored-procedure command for each change type:
| Change Type | When Triggered | Data Available |
|---|---|---|
added |
New item in results | after |
updated |
Item changed | before, after |
deleted |
Item removed | before |
Each change type accepts a template spec:
| Field | Type | Required | Description |
|---|---|---|---|
template |
string | Yes | Handlebars template for the stored procedure command |
If no template is configured for an operation, the event for that operation is skipped.
Command Templates
Stored-procedure commands are Handlebars templates. Every template is compiled when the reaction starts, so an invalid template fails fast rather than at runtime.
Template Context
Each template is rendered against a context with these keys:
| Key | Description | Available On |
|---|---|---|
after |
The post-change row | added, updated |
before |
The pre-change row | updated, deleted |
data |
The raw data payload of an update diff | updated |
query_id |
The ID of the query that produced the result | all |
query_name |
Alias of query_id (same value; provided for symmetry with other reactions) |
all |
operation |
ADD, UPDATE, or DELETE |
all |
timestamp |
RFC3339 result timestamp | all |
metadata |
Result metadata map | all |
Binding Values with {{param}}
Reference a field with the {{param <expr>}} helper. Instead of inlining the value into the SQL text, the helper appends the resolved value to the command’s positional bind parameters and emits a $N placeholder. Untrusted row data therefore can never alter the command structure — it is always sent to PostgreSQL as a bound parameter.
For example, with this template:
template: "CALL add_user({{param after.id}}, {{param after.name}}, {{param after.email}})"
and this added result row:
{
"id": 1,
"name": "Alice",
"email": "alice@example.com"
}
the reaction executes the SQL:
CALL add_user($1, $2, $3)
with $1 = 1, $2 = "Alice", and $3 = "alice@example.com" bound as parameters.
Nested Field Access
Access nested fields using dot notation:
template: "CALL add_address({{param after.user.id}}, {{param after.address.city}})"
Binding a Whole Object as JSONB
To pass an entire object to a jsonb argument, reference it with {{param}} directly. The object is bound as a single positional JSONB parameter, so it is safe even for untrusted row data:
template: "CALL ingest_record({{param after}})"
SQL Safety
Only `{{param Render Failures
If a template references a missing field, or otherwise fails to render, the reaction logs the error and skips that event rather than executing partial or unsafe SQL.
Template Resolution Order
For each (query_id, operation) the reaction resolves the command template in this order:
- A
routesentry keyed by the full query ID. - A
routesentry keyed by the query ID’s last dotted segment (sosource.my_querycan be routed via amy_querykey). - The shared
defaultTemplate.
If none of these supplies a template for the operation, the event is skipped. Every routes key must match a subscribed query ID (or its last dotted segment); an unmatched key is rejected when the reaction is built.
Examples
Basic Single-Query Sync
Keep a users_sync table in step with a continuous query by calling a stored procedure for each change type:
reactions:
- kind: storedproc-postgres
id: user-sync
queries: [user-changes]
hostname: localhost
port: 5432
database: mydb
user: postgres
password: ${DB_PASSWORD}
ssl: true
commandTimeoutMs: 30000
retryAttempts: 3
defaultTemplate:
added:
template: "CALL add_user({{param after.id}}, {{param after.name}}, {{param after.email}})"
updated:
template: "CALL update_user({{param after.id}}, {{param after.name}}, {{param after.email}})"
deleted:
template: "CALL delete_user({{param before.id}})"
Multi-Query with Per-Query Route Overrides
Subscribe to multiple queries, apply a shared defaultTemplate to most of them, and override only the product-changes query with custom procedures. Because the product-changes route omits deleted, delete events for that query fall back to the default template.
reactions:
- kind: storedproc-postgres
id: multi-query-sync
queries: [user-changes, product-changes, order-changes]
hostname: localhost
port: 5432
database: mydb
user: postgres
password: ${DB_PASSWORD}
commandTimeoutMs: 5000
retryAttempts: 3
defaultTemplate:
added:
template: "CALL log_entity_added({{param after.id}}, {{param after.type}})"
updated:
template: "CALL log_entity_updated({{param after.id}}, {{param after.type}})"
deleted:
template: "CALL log_entity_deleted({{param before.id}}, {{param before.type}})"
routes:
product-changes:
added:
template: "CALL sync_product_added({{param after.product_id}}, {{param after.name}}, {{param after.price}}, {{param after.inventory}})"
updated:
template: "CALL sync_product_updated({{param after.product_id}}, {{param after.price}}, {{param after.inventory}})"
How this resolves:
user-changesandorder-changes→ use thedefaultTemplatefor all operations.product-changes→added:CALL sync_product_added(...)(custom route)updated:CALL sync_product_updated(...)(custom route)deleted:CALL log_entity_deleted(...)(falls back to the default template)
Error Handling
The reaction retries failed procedure calls automatically with exponential backoff:
- Initial retry: 100ms delay
- Subsequent retries: 200ms, 400ms, 800ms, and so on
- Max retries: configurable with
retryAttempts(default3) - Timeout: configurable per command with
commandTimeoutMs(default30000)
Enable debug logging for troubleshooting:
logLevel: debug
Complete Example
host: 0.0.0.0
port: 8080
logLevel: info
sources:
- kind: postgres
id: orders-db
host: ${DB_HOST}
database: ecommerce
user: ${DB_USER}
password: ${DB_PASSWORD}
tables:
- public.orders
queries:
- id: high-value-orders
query: |
MATCH (o:orders)
WHERE o.total > 500
RETURN o.id, o.customer_id, o.total, o.status
sources:
- sourceId: orders-db
reactions:
- kind: storedproc-postgres
id: order-sync
queries: [high-value-orders]
hostname: ${SYNC_DB_HOST}
port: 5432
database: reporting
user: ${SYNC_DB_USER}
password: ${SYNC_DB_PASSWORD}
ssl: true
commandTimeoutMs: 10000
retryAttempts: 3
defaultTemplate:
added:
template: "CALL add_high_value_order({{param after.id}}, {{param after.customer_id}}, {{param after.total}}, {{param after.status}})"
updated:
template: "CALL update_order_status({{param after.id}}, {{param after.status}})"
deleted:
template: "CALL remove_high_value_order({{param before.id}})"
Documentation Resources
Feedback
Was this page helpful?
Glad to hear it! Please tell us what you found helpful.
Sorry to hear that. Please tell us how we can improve.