Configure HTTP Reaction
Send webhooks and HTTP requests when query results change
9 minute read
The HTTP Reaction Reaction A component that receives query result changes and takes action on them. Learn more sends HTTP requests when query results change Result Change Event A notification from a Continuous Query describing changes to its result set. Learn more . Use it for webhooks, REST API integrations, serverless functions, and other systems that accept HTTP requests.
The reaction supports two delivery modes:
- Single notifications (default) - each added, updated, or deleted result row is delivered as its own HTTP request.
- Adaptive batching - result changes are coalesced into batches and POSTed to one batch endpoint. Batch size adjusts automatically with incoming load.
Basic Configuration
With no outputTemplates configured, each change is sent as a default JSON envelope to:
POST {baseUrl}/changes/{queryId}
reactions:
- kind: http
id: webhook
queries: [my-query]
baseUrl: https://api.example.com
To customize routes, methods, headers, or bodies, add outputTemplates:
reactions:
- kind: http
id: webhook
queries: [my-query]
baseUrl: https://api.example.com
outputTemplates:
routes:
my-query:
added:
url: /events
method: POST
template: '{{json after}}'
headers:
Content-Type: application/json
Configuration Reference
| Field | Type | Default | Description |
|---|---|---|---|
kind |
string | Required | Must be http |
id |
string | Required | Unique reaction identifier |
queries |
array | Required | Query IDs to subscribe to |
autoStart |
boolean | true |
Start reaction automatically |
baseUrl |
string | http://localhost |
Base URL for HTTP requests. Must be http or https, include a host, and not include a query string or fragment. |
token |
string | None | Bearer token sent on every request |
timeoutMs |
integer | 5000 |
Per-request timeout in milliseconds. Must be greater than 0. |
priorityQueueCapacity |
integer | 10000 |
Capacity of the inbound queue that buffers query results before processing. Must be greater than 0 when set. |
outputTemplates |
object | None | Default and per-query templates for single notifications and batch items |
adaptive |
object | None | Enables adaptive batching when set. Requires batchEndpoint. |
batchEndpoint |
string | None | Absolute path appended to baseUrl for adaptive batch POSTs. Requires adaptive. |
recoveryPolicy |
string | strict |
Sustained delivery failure policy: strict or auto_skip_gap. |
Output Templates and Routing
outputTemplates controls how changes are rendered. It contains:
| Field | Type | Description |
|---|---|---|
routes |
object | Per-query overrides keyed by query ID or by the query ID’s last dotted segment |
defaultTemplate |
object | Fallback used when no matching routes entry exists |
For each query, configure one or more change types:
| Change Type | When Triggered | Data Available |
|---|---|---|
added |
New item in results | {{after}} |
updated |
Item changed | {{before}}, {{after}}, {{data}} |
deleted |
Item removed | {{before}} |
For each change type, use an HTTP call spec:
| Field | Type | Default | Description |
|---|---|---|---|
template |
string | Empty | Handlebars template for the request body. When empty, the default change notification envelope is sent. |
url |
string | Empty | Handlebars template for the URL. Relative paths are appended to baseUrl. Absolute http(s) URLs are allowed only when their scheme, host, and port match baseUrl. |
method |
string | POST |
HTTP method: GET, POST, PUT, PATCH, or DELETE (case-insensitive). |
headers |
object | {} |
Additional HTTP headers. Header values support Handlebars templates. |
The reaction resolves a template for each result in this order:
outputTemplates.routes[<queryId>]outputTemplates.routes[<last dotted segment>]- for example,routes.orderscan matchsource.ordersoutputTemplates.defaultTemplate- Built-in default:
POST {baseUrl}/changes/{queryId}with the default change notification envelope
Route keys are validated against the subscribed queries.
reactions:
- kind: http
id: rest-sync
queries: [orders, inventory.stock]
baseUrl: https://inventory.example.com/api
outputTemplates:
routes:
orders:
added:
url: /orders
method: POST
template: '{{json after}}'
updated:
url: /orders/{{after.id}}
method: PUT
template: '{{json after}}'
deleted:
url: /orders/{{before.id}}
method: DELETE
stock:
updated:
url: /stock/{{after.sku}}
method: PATCH
template: '{"quantity": {{after.quantity}}}'
defaultTemplate:
added:
url: /events
method: POST
template: |
{
"query": "{{query_id}}",
"operation": "{{operation}}",
"row": {{json after}}
}
Handlebars Templates
The HTTP reaction uses Handlebars templates for request bodies, URLs, and header values.
| Variable | Available On | Description |
|---|---|---|
{{after}} |
added, updated |
The new/current result row |
{{after.property}} |
added, updated |
A property from the new/current row |
{{before}} |
updated, deleted |
The previous result row |
{{before.property}} |
updated, deleted |
A property from the previous row |
{{data}} |
updated |
Raw update payload |
{{query_id}} |
All | Query ID that produced the change |
{{query_name}} |
All | Alias of query_id |
{{operation}} |
All | Operation value: ADD, UPDATE, or DELETE |
{{timestamp}} |
All | RFC 3339 timestamp of the query emission |
{{metadata}} |
All | Query result metadata object, or an empty object |
{{json value}} |
All | JSON-serializes value for embedding in JSON bodies |
Example:
template: |
{
"query": "{{query_name}}",
"operation": "{{operation}}",
"order": {{json after}},
"metadata": {{json metadata}}
}
Default Output Payload
When no body template applies, the reaction sends a DefaultChangeNotification JSON object.
| Field | Type | Required | Description |
|---|---|---|---|
operation |
string | Yes | ADD, UPDATE, or DELETE |
queryId |
string | Yes | Query that produced the change |
sequenceId |
integer | Yes | Monotonic per-query sequence number identifying this emission |
timestamp |
string | Yes | RFC 3339 timestamp of the query emission |
before |
object | No | Row state before the change. Omitted for ADD and for the first emission of an aggregation group. |
after |
object | No | Row state after the change. Omitted for DELETE. |
metadata |
object | No | Source/query metadata. Omitted when empty. |
Diffs are mapped as follows:
| Result diff | operation |
before |
after |
HTTP request |
|---|---|---|---|---|
| Add | ADD |
Omitted | Added row | Yes |
| Update | UPDATE |
Previous row | Current row | Yes |
| Delete | DELETE |
Deleted row | Omitted | Yes |
| Aggregation | UPDATE |
Previous aggregate, omitted on first emission | Current aggregate | Yes |
| Noop | N/A | N/A | N/A | No request |
Example default request:
POST /changes/orders HTTP/1.1
Content-Type: application/json
{
"operation": "ADD",
"queryId": "orders",
"sequenceId": 42,
"timestamp": "2026-01-01T00:00:00+00:00",
"after": {
"id": 1,
"total": 125.5
}
}
Adaptive Batching
Set adaptive and batchEndpoint to enable batched delivery. In batch mode, the reaction groups result changes and sends each batch as one POST request to {baseUrl}{batchEndpoint}.
reactions:
- kind: http
id: batched-webhook
queries: [orders, shipments]
baseUrl: https://api.example.com
adaptive:
adaptiveMinBatchSize: 50
adaptiveMaxBatchSize: 2000
adaptiveWindowSize: 100
adaptiveBatchTimeoutMs: 500
batchEndpoint: /events/batch
Adaptive Settings
| Field | Type | Default | Description |
|---|---|---|---|
adaptiveMinBatchSize |
integer | 1 |
Lower bound on batch size, used during idle or low traffic. Must be greater than 0. |
adaptiveMaxBatchSize |
integer | 100 |
Upper bound on batch size, used during bursts. Must be greater than 0 and at least adaptiveMinBatchSize. |
adaptiveWindowSize |
integer | 10 |
Throughput sample window in 100 ms units. 10 is 1 second, 50 is 5 seconds, and 100 is 10 seconds. Must be between 1 and 255. |
adaptiveBatchTimeoutMs |
integer | 1000 |
Maximum time to wait before flushing a partial batch. Must be greater than 0. |
Batch Endpoint
batchEndpoint must be an absolute path that starts with a single /, for example /events/batch. It cannot be an absolute URL and cannot contain Handlebars templates.
Each batch is sent as a BatchEnvelope:
{
"batch": [
{
"operation": "ADD",
"queryId": "orders",
"sequenceId": 100,
"timestamp": "2026-01-01T00:00:00+00:00",
"after": { "id": 1 }
},
{
"operation": "UPDATE",
"queryId": "shipments",
"sequenceId": 101,
"timestamp": "2026-01-01T00:00:01+00:00",
"before": { "id": 2, "status": "pending" },
"after": { "id": 2, "status": "shipped" }
}
]
}
In adaptive mode:
- Per-query body
templatevalues still apply to each item inbatch. - Per-item
url,method, andheadersdo not apply, because the whole batch is onePOSTtobatchEndpoint. - If a body template is not configured, fails to render, or renders invalid JSON, that batch item falls back to the default change notification envelope.
- A batch may contain items from multiple subscribed queries.
Example with custom batch items:
reactions:
- kind: http
id: batched-events
queries: [orders]
baseUrl: https://events.example.com
adaptive:
adaptiveMinBatchSize: 25
adaptiveMaxBatchSize: 500
adaptiveWindowSize: 50
adaptiveBatchTimeoutMs: 1000
batchEndpoint: /bulk
outputTemplates:
routes:
orders:
added:
template: |
{
"type": "order_created",
"order": {{json after}}
}
updated:
template: |
{
"type": "order_updated",
"before": {{json before}},
"after": {{json after}}
}
Authentication and Headers
Set token to send a bearer token on every request:
reactions:
- kind: http
id: api-webhook
queries: [events]
baseUrl: https://api.example.com
token: ${API_TOKEN}
Use headers on individual output templates for additional headers:
reactions:
- kind: http
id: custom-auth
queries: [events]
baseUrl: https://api.example.com
outputTemplates:
routes:
events:
added:
url: /webhook
method: POST
template: '{{json after}}'
headers:
X-API-Key: ${API_KEY}
X-Query: "{{query_id}}"
X-Operation: "{{operation}}"
Content-Type: application/json
Failure Handling and Recovery
The HTTP reaction retries transient delivery failures up to three times with exponential backoff. Transient failures include network send errors, HTTP 5xx responses, and HTTP 408, 409, 425, and 429 responses.
After the retry budget is exhausted, the configured recoveryPolicy is applied:
| Policy | Behavior |
|---|---|
strict |
Fail-stops the reaction without advancing the checkpoint. Unacknowledged work can replay from the query outbox after restart. |
auto_skip_gap |
Skips the failed item or batch, advances past it, and keeps the reaction running. |
Authentication and permission rejections (401, 403, 407) are treated as sustained failures and use the recovery policy. They are not silently dropped, because refreshing credentials may allow replay to succeed.
Permanent failures that cannot succeed on replay are dropped and the checkpoint advances past them. Examples include most other 4xx responses, invalid HTTP methods, invalid auth-token header values, and rejected rendered URLs.
Template render failures do not drop a change:
- URL render failures fall back to
/changes/{queryId}. - Body render failures fall back to the default change notification envelope.
- Header render failures or invalid rendered header values drop only that header.
Validation Rules
The reaction validates configuration at startup:
baseUrlmust be a validhttporhttpsURL with a host and no query string or fragment.timeoutMsandpriorityQueueCapacitymust be greater than0when set.batchEndpointandadaptivemust be configured together.batchEndpointmust be a path such as/events/batch, not an absolute URL, and cannot contain templates.adaptiveMinBatchSizemust be less than or equal toadaptiveMaxBatchSize.adaptiveWindowSizemust be in the range1to255.- Body, URL, and header templates must compile as valid Handlebars templates.
- Route keys must match a subscribed query ID or the query ID’s last dotted segment.
Examples
GitHub Integration
Create GitHub issues from alert results:
reactions:
- kind: http
id: github-issues
queries: [critical-alerts]
baseUrl: https://api.github.com
token: ${GITHUB_TOKEN}
outputTemplates:
routes:
critical-alerts:
added:
url: /repos/{{after.repo}}/issues
method: POST
template: |
{
"title": "Alert: {{after.type}}",
"body": "{{after.message}}\n\nSeverity: {{after.severity}}"
}
headers:
Accept: application/vnd.github+json
X-GitHub-Api-Version: "2022-11-28"
Slack Notification
reactions:
- kind: http
id: slack-alerts
queries: [important-events]
baseUrl: https://hooks.slack.com
outputTemplates:
routes:
important-events:
added:
url: /services/XXX/YYY/ZZZ
method: POST
template: |
{
"text": "New event: {{after.title}}",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*{{after.title}}*\n{{after.description}}"
}
}
]
}
headers:
Content-Type: application/json
High-Throughput Event Ingestion
reactions:
- kind: http
id: event-ingestion
queries: [orders, inventory, customers]
baseUrl: https://events.example.com
token: ${EVENT_TOKEN}
timeoutMs: 10000
priorityQueueCapacity: 50000
recoveryPolicy: strict
adaptive:
adaptiveMinBatchSize: 100
adaptiveMaxBatchSize: 1000
adaptiveWindowSize: 100
adaptiveBatchTimeoutMs: 2000
batchEndpoint: /events/batch
Testing
Using webhook.site
Test webhooks without a real endpoint:
- Go to https://webhook.site
- Copy your unique URL
- Configure the reaction:
reactions:
- kind: http
id: test-webhook
queries: [my-query]
baseUrl: https://webhook.site
outputTemplates:
routes:
my-query:
added:
url: /your-unique-id
method: POST
template: '{{json after}}'
Local Testing
Use a local HTTP server:
# Python
python -m http.server 9999
# Node.js
npx http-server -p 9999
reactions:
- kind: http
id: local-test
queries: [my-query]
baseUrl: http://localhost:9999
The local test above sends default notifications to http://localhost:9999/changes/my-query.
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: http
id: order-notifications
queries: [high-value-orders]
baseUrl: ${WEBHOOK_URL}
token: ${WEBHOOK_TOKEN}
timeoutMs: 10000
outputTemplates:
routes:
high-value-orders:
added:
url: /orders/high-value
method: POST
template: |
{
"event": "high_value_order",
"order_id": "{{after.id}}",
"customer_id": "{{after.customer_id}}",
"total": {{after.total}}
}
headers:
Content-Type: application/json
updated:
url: /orders/{{after.id}}/status
method: PUT
template: |
{
"status": "{{after.status}}",
"previous_status": "{{before.status}}"
}
headers:
Content-Type: application/json
Documentation resources
HTTP Reaction README
Routing, templates, batching, and payload behavior
HTTP Reaction Output Schema
JSON schema for default notification and batch payloads
Next steps
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.