Your First Pipeline: Kafka To Redis
The first graph is deliberately trivial: JSON order records arrive on a Kafka topic, Nervix decodes them, and an emitter publishes them to a Redis Pub/Sub channel. Along the way you meet every building block that later steps reuse: domains, schemas, codecs, relays, clients, ingestors, and emitters.
Provision The Topic
Nervix never creates external entities, so create the Kafka topic first:
docker exec broker /opt/kafka/bin/kafka-topics.sh \
--create --topic orders --bootstrap-server localhost:9092
Create A Domain
Every entity in Nervix lives in an explicit domain — there is no implicit default. Open
nervix-cli and create one, then switch the session to it:
CREATE UNPACED DOMAIN quickstart;
USE quickstart;
An unpaced domain admits records as they arrive, with no domain clock pacing — the right choice
for a first pipeline. See Domains And Time. USE is a
client-local statement that scopes the rest of your session to the domain.
Open A Transaction
Submitting more than one model statement at a time requires a transaction, and it is also how a
graph is assembled atomically — nothing takes effect until COMMIT;. See
NSPL Overview for the statement surface.
BEGIN;
The graph declarations in the rest of this page run inside this transaction.
Describe The Data
Nervix separates what a record looks like inside the graph from how it is serialized on the wire. The internal schema is exact-typed; the wire schema describes the JSON payload in the wire format’s own type names; the codec bridges the two. See Schemas And Codecs.
CREATE SCHEMA order_record (
order_id STRING,
customer STRING,
status STRING,
amount I64,
quantity I64
);
CREATE WIRE JSON SCHEMA order_wire MODE STRICT (
order_id string,
customer string,
status string,
amount integer,
quantity integer
);
CREATE CODEC order_codec
FROM WIRE JSON SCHEMA order_wire
TO SCHEMA order_record;
STRICT rejects payload fields that are not declared; LOOSE would drop them instead
(Wire Schemas). There are no implicit casts anywhere in
Nervix, so the codec is the only place where wire values become typed internal values. A
DATETIME field would additionally need an explicit encoding such as
ENCODE created_at AS RFC3339 (Codecs).
Create A Relay
Relays are the named streams that connect runtime nodes. Each relay carries records of exactly one schema. See Streams And State.
CREATE RELAY orders SCHEMA order_record UNBRANCHED;
UNBRANCHED means one shared stream. Branched Processing
introduces the alternative.
Declare The External Connections
Connection endpoints never live on ingestors or emitters; they are named CLIENT objects that
nodes reference. Kafka client config is passed through to the Kafka driver, so
bootstrap.servers uses its familiar key.
CREATE CLIENT kafka_local
TYPE KAFKA
CONFIG {
'bootstrap.servers' = '127.0.0.1:9092'
};
CREATE CLIENT redis_local
TYPE REDIS
POOL SIZE MIN 1 MAX 4
CONFIG {
'addr' = 'redis://127.0.0.1:6379/'
};
Ingest From Kafka
An ingestor brings external data into the graph. It decodes each payload with the codec and routes the result to a relay:
CREATE KAFKA TOPIC INGESTOR kafka_orders
FROM kafka_local
TOPIC 'orders'
OFFSET BY CONSUMER GROUP 'quickstart'
MODE ACK SEQUENTIAL ACK TIMEOUT 30s RETRY POLICY BACKOFF 200ms MAX 5s
ON QUIESCE SUSPEND DECODE USING order_codec
TO orders
INHERIT ALL
UNBRANCHED
FLUSH EACH 100ms MAX BATCH SIZE 1MiB
ON MESSAGE ERROR LOG
ON GENERAL ERROR LOG;
Reading top to bottom:
KAFKA TOPIC INGESTORnames the connector and the entity the ingestor reads.FROMnames theTYPE KAFKAclient, and the topic and consumer group are string literals sent to Kafka exactly as written.OFFSET BY CONSUMER GROUPtracks progress in Kafka;MODE ACK SEQUENTIALacknowledges records in order. The full Kafka source grammar is in Supported Ingestor Types.TO ordersstarts a route. Routes begin empty:INHERIT ALLcopies every decoded field into the outgoing record, and later steps showSETconstructing new values. See The Working Message.- Every route needs an explicit flush policy —
FLUSH EACH <duration> MAX BATCH SIZE <bytes>orFLUSH IMMEDIATE. There are no hidden defaults. ON MESSAGE ERROR LOGis the route’s per-record error policy;ON GENERAL ERROR LOGis the node-wide policy (Runtime Node Error Policies).
Emit To Redis
An emitter drains a relay out of the graph. The Redis integration is Pub/Sub (Supported Emitter Sinks), and the same codec that decoded the input encodes it back to JSON:
CREATE EMITTER redis_orders
FROM orders
TO REDIS PUBSUB redis_local CHANNEL orders_out
MODE NO_ACK RETRY POLICY BACKOFF 250ms MAX 30s
ENCODE USING order_codec
INHERIT ALL
FLUSH EACH 100ms MAX BATCH SIZE 1MiB
ON MESSAGE ERROR LOG
ON GENERAL ERROR LOG;
Redis Pub/Sub exposes no delivery confirmation, so MODE NO_ACK names server acceptance as the
publish boundary. The required retry policy paces connection and other infrastructure failures;
it does not turn subscriber presence into an acknowledgment.
Commit And Start
COMMIT; applies the structural changes: the whole graph is validated and installed atomically.
Nothing processes data yet — that is the job of START;, the domain lifecycle command that begins
actual work (Start And Stop):
COMMIT;
START;
The Complete Script
CREATE UNPACED DOMAIN quickstart;
USE quickstart;
BEGIN;
CREATE SCHEMA order_record (
order_id STRING,
customer STRING,
status STRING,
amount I64,
quantity I64
);
CREATE WIRE JSON SCHEMA order_wire MODE STRICT (
order_id string,
customer string,
status string,
amount integer,
quantity integer
);
CREATE CODEC order_codec
FROM WIRE JSON SCHEMA order_wire
TO SCHEMA order_record;
CREATE RELAY orders SCHEMA order_record UNBRANCHED;
CREATE CLIENT kafka_local
TYPE KAFKA
CONFIG {
'bootstrap.servers' = '127.0.0.1:9092'
};
CREATE CLIENT redis_local
TYPE REDIS
POOL SIZE MIN 1 MAX 4
CONFIG {
'addr' = 'redis://127.0.0.1:6379/'
};
CREATE KAFKA TOPIC INGESTOR kafka_orders
FROM kafka_local
TOPIC 'orders'
OFFSET BY CONSUMER GROUP 'quickstart'
MODE ACK SEQUENTIAL ACK TIMEOUT 30s RETRY POLICY BACKOFF 200ms MAX 5s
ON QUIESCE SUSPEND DECODE USING order_codec
TO orders
INHERIT ALL
UNBRANCHED
FLUSH EACH 100ms MAX BATCH SIZE 1MiB
ON MESSAGE ERROR LOG
ON GENERAL ERROR LOG;
CREATE EMITTER redis_orders
FROM orders
TO REDIS PUBSUB redis_local CHANNEL orders_out
MODE NO_ACK RETRY POLICY BACKOFF 250ms MAX 30s
ENCODE USING order_codec
INHERIT ALL
FLUSH EACH 100ms MAX BATCH SIZE 1MiB
ON MESSAGE ERROR LOG
ON GENERAL ERROR LOG;
COMMIT;
START;
Send An Order
Subscribe to the output channel in one terminal:
docker exec redis redis-cli SUBSCRIBE orders_out
Produce an order in another:
docker exec -i broker /opt/kafka/bin/kafka-console-producer.sh \
--bootstrap-server localhost:9092 --topic orders
{"order_id":"o-1001","customer":"acme","status":"new","amount":1500,"quantity":3}
The Redis subscriber prints the record back as JSON. You can also watch any relay from inside Nervix with a read-only session subscription (Sessions):
nervix-cli --domain quickstart subscribe watch orders
The pipeline works end to end. Next, add logic to it in Conditional Routing.