CREATE PAYLOAD TRANSFORM

Creates a payload transform that defines how incoming HTTP payloads are parsed, transformed, and inserted into a target table. Once created, data is ingested by POSTing to the /ingest endpoint.

Syntax

CREATE [ OR REPLACE ] PAYLOAD TRANSFORM transformName
INTO targetTable
[ DLQ dlqTable [ PARTITION BY ( YEAR | MONTH | WEEK | DAY | HOUR ) ] [ TTL n timeUnit ] ]
AS [ DECLARE [ OVERRIDABLE ] @var := value [, [ OVERRIDABLE ] @var2 := value2 ... ] ]
SELECT ...

Where:

  • timeUnit: HOURS | DAYS | WEEKS | MONTHS | YEARS
  • The SELECT must not reference existing tables - it can only use functions and expressions, including CTEs

Parameters

ParameterDescription
transformNameName for the payload transform
OR REPLACEReplace existing transform with the same name
targetTableTable to insert rows into
DLQ dlqTableRoute failed payloads to a dead-letter queue table
PARTITION BYPartitioning unit for the DLQ table (if QuestDB creates it)
TTLRetention period for DLQ rows
DECLAREDefine variables used in the SELECT
OVERRIDABLEAllow variable to be overridden via URL query parameters

Column mapping

SELECT output column names must match column names in the target table. Columns are matched by name, not position. You do not need to produce all columns - any columns not included in the SELECT receive their default values.

Examples

Basic transform

Create a table and a transform
CREATE TABLE order_book (
ts TIMESTAMP,
symbol SYMBOL,
bids DOUBLE[][],
asks DOUBLE[][]
) TIMESTAMP(ts) PARTITION BY DAY WAL;

CREATE PAYLOAD TRANSFORM binance_depth
INTO order_book
AS DECLARE OVERRIDABLE @symbol := 'BTCUSDT'
SELECT
now() AS ts,
@symbol AS symbol,
json_extract(payload(), '$.bids')::DOUBLE[][] AS bids,
json_extract(payload(), '$.asks')::DOUBLE[][] AS asks;

With dead-letter queue

Transform with DLQ and 7-day retention
CREATE PAYLOAD TRANSFORM binance_depth
INTO order_book
DLQ dlq_errors PARTITION BY DAY TTL 7 DAYS
AS DECLARE OVERRIDABLE @symbol := 'BTCUSDT'
SELECT
now() AS ts,
@symbol AS symbol,
json_extract(payload(), '$.bids')::DOUBLE[][] AS bids,
json_extract(payload(), '$.asks')::DOUBLE[][] AS asks;

Replace an existing transform

Replace a transform definition
CREATE OR REPLACE PAYLOAD TRANSFORM binance_depth
INTO order_book
AS SELECT
now() AS ts,
'BTCUSDT' AS symbol,
json_extract(payload(), '$.bids')::DOUBLE[][] AS bids,
json_extract(payload(), '$.asks')::DOUBLE[][] AS asks;

Multiple overridable variables

Two overridable variables with defaults
CREATE PAYLOAD TRANSFORM sensor_ingest
INTO sensor_data
AS DECLARE OVERRIDABLE @source := 'default', OVERRIDABLE @region := 'us-east'
SELECT
now() AS ts,
@source AS source,
@region AS region,
json_extract(payload(), '$.temperature')::DOUBLE AS temperature;

Validation

QuestDB validates the transform at creation time:

CheckDescription
Column namesEvery SELECT output column must exist in the target table
Column typesEach output type must be convertible to the target column type, following INSERT AS SELECT rules
DLQ schemaIf the DLQ table already exists, its schema must match the expected DLQ layout

Validation errors report the position of the offending column expression in the SELECT.

Dead-letter queue schema

When a DLQ is configured and a transform error occurs, QuestDB writes a row with the following columns:

ColumnTypeDescription
tsTIMESTAMPWhen the error occurred (designated timestamp)
transform_nameSYMBOLName of the transform that failed
payloadVARCHARThe original HTTP body
queryVARCHARThe transform's SELECT SQL
stageSYMBOLProcessing stage where the error occurred
errorVARCHARError message

Multiple transforms can share the same DLQ table. The HTTP response still returns an error so the caller knows the request failed.

Permissions

ContextRequirement
Target tableThe /ingest caller must have INSERT permission, checked at request time
DLQ tableThe DDL caller must have INSERT permission, checked at creation time. Runtime DLQ writes use the system security context
Enterprise

In QuestDB Enterprise deployments with RBAC enabled, the user creating the transform must hold the CREATE PAYLOAD TRANSFORM grant. When using OR REPLACE on a transform that already exists, the user must also hold DROP PAYLOAD TRANSFORM. The /ingest caller must hold the HTTP endpoint grant and INSERT permission on the target table (and on the DLQ table, if configured).

-- Create-only
GRANT CREATE PAYLOAD TRANSFORM TO ingest_admin;

-- If using OR REPLACE
GRANT CREATE PAYLOAD TRANSFORM, DROP PAYLOAD TRANSFORM TO ingest_admin;

GRANT HTTP TO ingest_service;
GRANT INSERT ON order_book TO ingest_service;
GRANT INSERT ON dlq_errors TO ingest_service;