Skip to content

Streaming Worker Config Reference

Reference for the YAML config consumed by pycharter stream run. Fields are grouped by location in the document.

CLI companions: pycharter stream validate <config> runs the config-side preflight (contract resolution, sink/source key checks — no Kafka I/O); stream run --var KEY=VALUE overrides variables:; stream run --dry-run [--max-records N] previews validation without writing sinks (ephemeral consumer group for kafka sources); stream list / stream inspect discover and summarise configs. See the streaming validation guide.

api_version: pycharter.io/v2
kind: StreamingWorker
metadata:
  name: orders
  namespace: example
  version: "1.0.0"
  title: orders
spec:
  variables: {...}
  streams: [...]
  observability: {...}

Worker files carry the unified manifest envelope (KM): api_version + kind + identity in metadata (namespace/name/version) + the body under spec:. Files at the older formats (the TH envelope with top-level format_version/id/ version, or the pre-TH streaming: wrapper) upgrade on read or via pycharter migrate, which rewrites them in place. API payloads and store documents remain section-based (streams, variables, observability, metadata) — the envelope is the file format.

The schema is validated by pycharter.streaming.config.StreamingConfig (source). Unknown keys are rejected; stream names must be unique within a config.

Envelope: api_version, kind, and metadata

Field Type Default Notes
api_version string — pycharter.io/v2 — the manifest envelope version (KM-2). Required.
kind string — StreamingWorker. Required.
metadata.name string — Identity slug (^[a-z0-9_-]+$). With metadata.namespace it forms the stored artifact's name key (namespace/name). Required.
metadata.namespace string none Identity scope.
metadata.version string — Config content version (semver); the registry keys on (namespace/name, version). Required.
variables mapping[string, string] {} Default values for ${VAR} placeholder substitution, declared under spec:. Declared values take priority over the worker's environment; ${VAR:-default} and ${VAR:?error} modifiers behave identically to batch. Resolved at worker load time and by the API's source-tap; editable in the UI's Variables tab.
metadata mapping none Cataloguing metadata, mirroring a batch pipeline config's metadata block. Optional; the store derives title/namespace defaults from id when absent.

metadata fields (when present):

Field Type Default Notes
title string — Human-readable config title. Required within metadata.
description string none Longer description of the config's purpose.
owner string none Owning team or person.
tags list[string] [] Free-form tags for search/filtering.
namespace string none Slash-delimited browse folder (e.g. market-data/massive).

metadata.version was deleted in format 2 (TH-1): the artifact version lives at the top level only. A document still carrying it is rejected with a pointer to pycharter migrate.

format_version: "2"
id: market-data/massive
version: "1.0.0"
metadata:
  title: Massive market data
  description: Normalize and validate massive.com market-data events.
  owner: data-platform
  tags: [streaming, market-data, massive]
  namespace: market-data/massive
variables:
  KAFKA_BROKERS: localhost:9092   # default for ${KAFKA_BROKERS}; wins over the env
streams: [...]

streams[]

One stream = one source-to-sink validation pipeline. A worker can run any number concurrently.

streams:
  - name: orders-validator
    source: {...}
    contract: {...}        # batch-aligned: ref + validate/coerce/annotate
    transform: {...}       # optional: rename / convert / add / select / drop
    mode: enforce
    delivery: at_least_once
    load: { target: {...} }
    dlq:  { target: {...} }
    backpressure: {...}
    quality: {...}

The contract / transform / load blocks mirror a batch pipeline step so the "contract specification" reads the same in both engines.

Required fields

Field Type Default Notes
name string — Used in metrics labels (stream=...), logs, and the readiness probe. Must be unique within a worker.
source mapping — See source.
contract mapping — See contract.
load mapping — { target: {...} }. See Sinks.
dlq mapping — { target: {...} }. See Sinks.

Optional fields

Field Type Default Notes
transform mapping none Per-message transform applied before validation (rename / convert / add / select / drop, same operators as the batch transform step). A transform error routes the record to the DLQ.
mode enum enforce detect / enforce / shadow. See Operating modes.
delivery enum at_least_once at_least_once / at_most_once / exactly_once. exactly_once is a transactional Kafka read-process-write path: it requires kafka source and sinks, supports transform and the contract toggles (incl. annotate), but rejects window — window state cannot participate in the Kafka transaction; use at_least_once with window.checkpoint for durable windows.
window mapping none Windowed aggregation (tumbling / sliding / session). When set, the stream emits window rows instead of individual records. See window.
backpressure mapping defaults shown below See below.
quality mapping {windows: [1m, 5m, 1h]} See below.

streams[].source

The source carries connection details only — the contract lives in the contract block, not on the source. kafka is the first-class type (auth fields mirror KafkaExtractor's constructor); every other registered extractor type is built via the shared extractor factory with its own keys: websocket, sse, rabbitmq, sqs, redis (Redis Streams: stream + consumer_group), mongodb, file_watcher, plus the batch-ish http / graphql / file / database / cloud_storage. pycharter stream validate reports an unregistered type with the full list.

source:
  type: kafka
  topic: orders
  bootstrap_servers: kafka:9092
  consumer_group: orders-validator
  auto_offset_reset: latest             # latest | earliest
  poll_timeout: 1.0                     # seconds per poll
  security_protocol: PLAINTEXT          # PLAINTEXT | SASL_PLAINTEXT | SASL_SSL | SSL
  sasl_mechanism: SCRAM-SHA-512
  sasl_username: ...
  sasl_password: ...                    # never inline; read via env interpolation
  ssl_cafile: /etc/certs/ca.pem
Field Type Default Notes
type string kafka kafka, or any registered extractor type (websocket / sse / rabbitmq / sqs / redis / mongodb / file_watcher / …) via the extractor factory.
topic string — Source topic (kafka).
bootstrap_servers string — Comma-separated broker addresses.
consumer_group string stream name Kafka group id.
auto_offset_reset enum latest Where to start when no committed offset exists.
poll_timeout float 1.0 Seconds the consumer blocks per poll cycle.
security_protocol enum PLAINTEXT PLAINTEXT / SASL_PLAINTEXT / SASL_SSL / SSL.
sasl_mechanism string none e.g. SCRAM-SHA-256, SCRAM-SHA-512.
sasl_username string none SASL username.
sasl_password string none Never inline. Use env interpolation.
ssl_cafile path none CA bundle for TLS.

Sinks: load and dlq

Each sink is a { target: {...} } block (same shape as a batch load step's target). The valid sink writes successfully validated records, the DLQ sink writes the §3.7 envelope.

load:
  target:
    type: kafka
    topic: orders-validated
    bootstrap_servers: kafka:9092
    # plus the same auth fields as source

dlq:
  target:
    type: kafka
    topic: orders-dlq
    bootstrap_servers: kafka:9092
target.type Required keys Optional keys Notes
kafka topic, bootstrap_servers same auth fields as source The valid sink re-stamps pycharter-contract / pycharter-contract-version headers; the DLQ sink stamps pycharter-dlq-reason: <reason>. Idempotent producer, acks=all.
file path — Records appended as JSONL, flushed per write.
postgres / database / sqlite connection_string (or url / database.url), table schema (default public), valid sink: record_column (default record), contract_column (contract_id), version_column (contract_version); DLQ sink: envelope_column (envelope) Records inserted as JSON, committed per write. Table/schema/column identifiers are allowlist-validated.

streams[].contract

Batch-aligned contract specification — the same shape as a pipeline step's contract: block, plus the streaming-only resolution knobs.

contract:
  ref:
    type: store                          # store | file
    name: orders                         # store: contract name
    version: "2"                         # store: version (omit for latest)
    # path: /app/contracts/orders        # file: directory of split artifacts
  store:                                 # required when ref.type=store
    type: postgres                       # sqlite | postgres | mongodb | redis | in_memory
    connection_string: ${DATABASE_URL}
  resolution: static                     # static | header
  on_missing_header: strict              # strict | fallback_to_static
  validate: true
  coerce: true
  annotate: false
  privacy:                               # optional — see docs/guides/privacy-flags.md
    mode: detect                         # off | detect | enforce
    clearance: []                        # flags this stream's sink may receive in clear
Field Type Default Notes
ref.type enum store store (registry lookup) or file (single contract from ref.path).
ref.name / ref.version string — Store lookup key. name required for static / fallback resolution.
ref.path path — Directory of split artifacts (schema/coercion/validation). Required when ref.type=file.
store mapping none Contract-store connection; required when ref.type=store.
resolution enum static static uses ref; header reads the pycharter-contract headers per message (requires ref.type=store).
on_missing_header enum strict When resolution=header: strict → DLQ; fallback_to_static → use ref (requires ref.name).
validate / coerce / annotate bool true / true / false Mirror the batch toggles with batch semantics (order coerce → validate → annotate). validate: false passes every record through without validation-failure DLQ routing (decode / unknown-contract routing is unchanged). coerce: false skips the contract's coercion_rules. annotate: true stamps _concept_annotations from the contract's field bindings. For identical coercion across modes, author rules with the shared coerce_to_* operation names.
privacy mapping {mode: detect} How contract-flagged fields (field_bindings entries with pii: true / restricted: true) are treated at the valid sink and the DLQ — the same block as the batch step contract. detect never mutates and counts flagged values reaching the sink in clear; enforce tokenizes pii (salted HMAC, equal to the KB match_token) and redacts restricted to null immediately before each valid-sink write (after the KB-emitter / extraction-sink tees) unless the flag is listed in clearance; under enforce a quarantined payload has every flagged field redacted and an unclassifiable payload (no contract / undecodable) is withheld as {"_withheld": true, "sha256", "bytes"}; if tokenization is needed and no PYCHARTER_KB_ORG_SECRET is set, records are withheld to the DLQ (privacy_error) instead of written in clear. off restores pre-feature behaviour. Requires ref.type: store for enforce: the split-directory ref.type: file form carries no field_bindings, so the worker refuses enforce at build time and warns that detect reports nothing.

streams[].window

Windowed aggregation: validated records are aggregated per key and the stream emits window rows (one dict per closed window) to the valid sink instead of individual records.

window:
  type: tumbling                # tumbling | sliding | session
  size: 60                      # tumbling/sliding; seconds
  slide: 30                     # sliding only
  gap: 300                      # session only
  key: [symbol]                 # group-by fields (string or list)
  time_field: ts                # event-time field (epoch seconds); omit for processing time
  allowed_lateness: 0           # seconds a late record may still join its window
  aggregates:                   # at least one required
    - {name: trades, op: count}
    - {name: total, op: sum, field: qty}
  checkpoint:                   # optional durable state (survives restarts)
    type: file                  # file | sqlite (default file)
    path: /var/lib/pycharter/{stream_name}.json   # file: JSON path; sqlite: db path
    interval_seconds: 30        # persist at most this often (plus on shutdown)
    on_mismatch: discard        # discard | fail — see below

Aggregate op values: count, sum, min, max, mean, first, last, vwap (requires weight). Non-count ops require field.

The window operates on the validated record — fields referenced by key, time_field, or aggregates must be part of the contract's schema (validation output drops fields the schema doesn't declare), or records silently land in a keyless window.

window.checkpoint

Without a checkpoint, window state is in-memory and lost on restart. With one, open-window state is persisted at most every interval_seconds (triggered on the record path — state cannot change while idle) plus a final persist at shutdown, and restored at startup. {stream_name} in path is substituted; multiple streams may share one sqlite database (one row per stream). Run one worker per stream name — two workers sharing a checkpoint race.

Every checkpoint is stamped with a fingerprint of the window spec. If the window: config changed since the checkpoint was written, on_mismatch: discard (default) logs a warning and starts with empty state; fail refuses to start so an operator can decide.

Durability contract: with delivery: at_least_once + a checkpoint, window rows are emitted at-least-once. After a crash, replayed records may double-count into aggregates restored from a checkpoint that is up to interval_seconds stale — pair with idempotent sinks or tolerate approximate aggregates. exactly_once delivery rejects window entirely (state cannot participate in the Kafka transaction).

streams[].backpressure

Engine-wide tunables.

backpressure:
  max_in_flight: 1000                       # bound on the internal queue
  registry_outage_timeout_seconds: 60.0     # circuit breaker open-after threshold
  registry_retry:                           # in-loop retry for transient registry errors
    max_attempts: 3
    base_delay_seconds: 0.2
    max_delay_seconds: 5.0
Field Type Default Notes
max_in_flight int 1000 Bound on the queue between source and validator. Source pauses partition assignment when this saturates.
registry_outage_timeout_seconds float 60.0 How long to tolerate consecutive registry failures before tripping the breaker. Once tripped, /readyz returns 503.
registry_retry.max_attempts int 3 Total tries per record, including the first (1 disables retry). Covers both contract resolution and validator lookup: a store outage in either stage retries, then dead-letters the record as registry_unavailable and the stream keeps consuming. A store that answers "contract absent" dead-letters immediately as unknown_contract (no retry).
registry_retry.base_delay_seconds float 0.2 Delay before the first retry; doubles per attempt with full jitter. Retries block the stream's consume loop (head-of-line), so worst-case added latency per record is bounded by the sum of delays (~0.2+0.4=0.6s at defaults).
registry_retry.max_delay_seconds float 5.0 Ceiling for the exponential backoff.

streams[].quality

Windows for the in-process WindowedQualityReducer. Lower-cardinality than Prometheus histograms; useful for dashboards that show "violation rate over the last 5 minutes".

quality:
  windows: ["1m", "5m", "1h"]

Recognised window labels: 1m, 5m, 15m, 1h. Custom labels are rejected at config-load time.

observability

Worker-level settings shared by every stream.

observability:
  health_port: 8080
  metrics_port: 9090       # reserved; metrics currently surface on health_port
  log_format: json         # json | text
Field Type Default Notes
health_port int 8080 Port for /healthz, /readyz, /metrics.
metrics_port int 9090 Reserved for split metrics-vs-health ports; unused in v1.
log_format enum json json for production (one JSON object per record), text for local development.

Full example

format_version: "2"
id: example/orders
version: "1.0.0"

streams:
  - name: orders-validator
    source:
      type: kafka
      topic: orders
      bootstrap_servers: kafka.prod.svc.cluster.local:9092
      consumer_group: orders-validator
      auto_offset_reset: latest
      security_protocol: SASL_SSL
      sasl_mechanism: SCRAM-SHA-512
      sasl_username: ${KAFKA_USERNAME}
      sasl_password: ${KAFKA_PASSWORD}
      ssl_cafile: /etc/certs/ca.pem
    contract:
      ref: { type: store, name: orders, version: "2" }
      store: { type: postgres, connection_string: ${DATABASE_URL} }
      resolution: header
      on_missing_header: fallback_to_static
      validate: true
      coerce: true
    mode: enforce
    delivery: at_least_once
    load:
      target:
        type: kafka
        topic: orders-validated
        bootstrap_servers: kafka.prod.svc.cluster.local:9092
        security_protocol: SASL_SSL
        sasl_mechanism: SCRAM-SHA-512
        sasl_username: ${KAFKA_USERNAME}
        sasl_password: ${KAFKA_PASSWORD}
        ssl_cafile: /etc/certs/ca.pem
    dlq:
      target:
        type: kafka
        topic: orders-dlq
        bootstrap_servers: kafka.prod.svc.cluster.local:9092
        security_protocol: SASL_SSL
        sasl_mechanism: SCRAM-SHA-512
        sasl_username: ${KAFKA_USERNAME}
        sasl_password: ${KAFKA_PASSWORD}
        ssl_cafile: /etc/certs/ca.pem
    backpressure:
      max_in_flight: 5000
      registry_outage_timeout_seconds: 30
    quality:
      windows: ["1m", "5m", "1h"]
observability:
  health_port: 8080
  log_format: json