Skip to main content

Pipeline specification (pipeline.yaml)

This page is the field-by-field reference for the pipeline YAML — the single artifact that describes a replication job. urutau run -f, urutau-coordinator run -f, and the definition.inline field of a CDCPipeline all consume the same format, and it is validated by the same server-side rules (spec.Validate) — there is no second, looser path.

Shape

Pipeline shape
pipeline: orders # required; names the run and its worker groups
source: {} # required; where rows come from
sink: {} # required; where rows go
tables: [] # required; at least one — unless source.postgres.discover is set

source

FieldRequiredNotes
kindyesRegistered driver: mysql, postgres, kafka
uriyes*Connection string: a MySQL/Postgres DSN, or the Kafka broker list. On Kubernetes, filled from URUTAU_SOURCE_URI. *Mutually exclusive with postgres
snapshotUrinoRead-only URI for the snapshot SELECT; lets a worker run as a SELECT-only user. Falls back to uri
serverIdmysqlReplication server id (string holding a uint32). Must be unique per MySQL instance
maxReconnectAttemptsnoMySQL binlog reader reconnect budget (default 3). Without a bound a permanently broken stream retries forever
slotNamepostgresLogical replication slot; required for Postgres
snapshotModenonone disables the snapshot. Must be none for Kafka
groupIdkafkaConsumer group
partitionedByPrimaryKeykafkaAssert the topics are key-partitioned; required for upsert
formatkafkadebezium (default), raw, avro
schemaRegistryavroConfluent-compatible registry base URL
postgrespostgresStructured connection fields (alternative to uri). See below

source.postgres

Structured PostgreSQL connection config. Mutually exclusive with source.uri. When present, uri is ignored for connection building.

FieldRequiredNotes
hostyesDatabase hostname
portnoPort (default 5432)
databaseyesDatabase name
usernamenoConnection user
passwordnoConnection password
paramsnoExtra DSN key-value pairs (e.g. application_name)
ssl.modenodisable (default), require, verify-ca, verify-full
ssl.canoServer CA certificate PEM path
ssl.certnoClient certificate PEM path (mutual TLS)
ssl.keynoClient private key PEM path (mutual TLS)
ssh.hostnoSSH bastion hostname
ssh.portnoSSH port (default 22)
ssh.usernamenoSSH user
ssh.passwordnoSSH password
ssh.privateKeynoSSH private key path
ssh.passphrasenoPrivate key passphrase
maxThreadsnoMax concurrent snapshot connections and row-normalization workers (1..32, default runtime.NumCPU())
retryCountnoTransient-error retries with backoff for snapshot queries and replication reconnect (default 3; 0 means "use the default")
cdc.pluginnoLogical decoding plugin: pgoutput (default) or wal2json
cdc.initialWaitTimenoSeconds the CDC reader waits for the first WAL message before failing with a non-retryable error (minimum 30, default 300)
tables[].modenocdc (default, log-based) or incremental (cursor column, no slot; Postgres, collapsed runner)
tables[].cursornoCursor column, required when mode: incremental
schemasnoLimits discover to these schemas (default: every accessible schema)
discovernoReplicates every table the user may SELECT (base tables and partitioned parents) instead of an explicit tables list. Mutually exclusive with tables; targets derive as <sink.namespace>.<table>. See Table discovery

In distributed mode the worker opens the snapshot SELECT from a DSN rendered from this block. ssl.ca/ssl.cert/ssl.key travel as paths, so every worker must mount those files at the same paths as the coordinator; ssh is not supported in distributed mode — set snapshotUri to a directly reachable read-only URI. See Distributed mode.

See Sources for driver-specific behavior — e.g. the MySQL uri accepts timezone and TLS (tls, ssl-ca, ssl-cert, ssl-key, ssl-server-name) query parameters.

sink

FieldRequiredNotes
typenoiceberg+rest (default), clickhouse, couchbase
uriyesCatalog/endpoint URI; on Kubernetes, filled from URUTAU_SINK_URI
namespaceyesDefault namespace for bare targets
warehouseicebergCatalog warehouse name
clientId / clientSecret / scopeicebergOAuth2 credentials; filled from URUTAU_SINK_* on Kubernetes
commitModecouchbasefast (default) or atomic
defaults.writeModenoPipeline-wide writeMode default
defaults.targetFileSizenoTarget data-file size
maintenance.enablednoBackground Iceberg table maintenance (Iceberg sink only — rejected on any other sink type). false by default — omitting the whole maintenance block, or leaving enabled unset, is the same as false
maintenance.compaction.interval / .targetFileSize / .minInputFilesnoSmall-file compaction. Defaults 5m / 512Mi / 5
maintenance.snapshotExpiry.interval / .retainLast / .maxAgenoSnapshot history pruning. Defaults 10m / 1 / 168h. maxAge is a safety window, not just a retention count — see Sinks
maintenance.orphanCleanup.interval / .olderThannoUnreferenced-file deletion. Defaults 1h / 72h

See Sinks for each sink's semantics and limits.

tables[]

FieldRequiredNotes
sourceyesschema.table in the source
targetyesnamespace.table in the sink
primaryKeyupsertRequired for writeMode: upsert
writeModenoupsert (default), append, append-idempotent
onDeleteappendrecord (default) or skip; a DELETE on append needs filterImmutable
filternoStructured row filter, applied at the source (snapshot WHERE + CDC). See filter
filterImmutablenoRequired for append + filter
partitionBynoIceberg partition transform
createIfNotExistsnoCreate the target table
workers.numbernoPartition the table across N workers by key range
workers.cpu / workers.memorynoPer-table Kubernetes resources
identityappend-idempotentTransport-metadata columns making the table idempotent
metadatanoPipeline metadata columns (op, commit_ts, …)
castnoOverride a source column's canonical type
columnFilternoSource column subset to read and emit. Must include every primary-key column
chunkColumnnoSnapshot chunking column for SQL sources. Must be a primary-key column; empty uses the source default (Postgres: CTID)
columnskafkaExplicit schema (no introspection)
bootstrapnosnapshot (default), adopt, adopt-verify
enrichnoBroadcast reference joins

filter

A structured predicate tree, not a raw SQL string. Each node is exactly one of all, any, not, or where:

Filter example
filter:
all:
- where: { col: status, op: eq, value: active }
- where: { col: amount, op: gt, value: 100 }

Operators: eq, neq, lt, lte, gt, gte, in, not_in, is_null, is_not_null. It is applied at the source boundary, before the Arrow hot-path: the snapshot composes it into the chunk WHERE, and CDC evaluates it on each decoded row. On a row that leaves the filter, an update emits a delete so an upsert target drops the stale row. A NULL column never satisfies a comparison (SQL three-valued logic). Literal types must match the column type — a JSON number for a numeric column, a string for a text column (no implicit coercion).

columnFilter

A subset of source columns to read and emit; the excluded columns are absent from the target schema. It applies to the snapshot SELECT list and to the CDC projection. Every primary-key column (declared, or introspected when not declared) must be included — the sink resolves the key and sort order by column name.

chunkColumn

Selects the snapshot chunking strategy for a SQL source. Empty uses the source's default — for Postgres, physical CTID block ranges, which need no primary key and give uniform chunks regardless of key skew. When set, it must name a primary-key column: an integer/float column splits by value range (batch-size), any other type by cursor stepping (next-query).

A table with workers: {number: N > 1} is always chunked by its (single-column) key, so each chunk range is routable to the same worker as the live stream; CTID is not routable and is used only for the single-worker snapshot.

writeMode

  • upsert — a change replaces the row at primaryKey (update/delete reflected as state). Requires primaryKey.
  • append — every change is a new row; nothing is updated. Requires filterImmutable when a filter is set.
  • append-idempotent — append, but a transport-metadata identity makes replay a no-op.

workers

workers: {number: N} splits the table's primary-key range into N contiguous ranges and derives the group names <pipeline>-<target>-<index>. N <= 1 or absent means one worker, no partitioning. A sink that cannot handle concurrent writers rejects N > 1 at boot. See Distributed mode.

enrich[]

Joins each event against a small reference table held in memory (broadcast hash join). Declared in order; an inner-join miss drops the event. See Enrichment.

Validation

The same rules run in three places, all server-side:

  1. At booturutau run / urutau-coordinator run load and validate before opening any connection.
  2. At admission — the Kubernetes validating webhook validates the definition.inline spec on kubectl apply, with the credential/URI fields exempted (spec.WithoutCredentials), because the webhook cannot read the Secrets the coordinator will mount. See Deploy on Kubernetes.
  3. In tests — the e2e suite runs every example spec through the same validator, so the docs cannot drift from the code.

A spec that fails validation is rejected with a list of problems, each naming the exact field path:

Validation error example
spec: tables[0].primaryKey: required when writeMode is upsert

Environment fallback

On the Kubernetes path the URI/credential fields are left empty and filled from the environment at load time (spec.LoadYAML):

EnvironmentField
URUTAU_SOURCE_URIsource.uri
URUTAU_SINK_URIsink.uri
URUTAU_SINK_CLIENT_IDsink.clientId
URUTAU_SINK_CLIENT_SECRETsink.clientSecret
URUTAU_SINK_SCOPEsink.scope

An inline value always wins; the environment only fills what is empty.