Generate orders and write them to the console, a Kafka topic, a PostgreSQL table or a JSON Lines file. The model is the same for every sink, and only the egress changes.
Before You Start
The commands run from the root of a clone of the repository, where the files are in docs/snippets/generate-data/. The Full Files block of this page holds the same files, for a reader who saves them by hand.
The console needs nothing. The Kafka sink needs pip install "desflow[kafka]" and a broker (odctl up kafka-lite), the PostgreSQL sink needs pip install "desflow[postgres]" and a database (odctl up postgres), and the file sink needs pip install "desflow[parquet]". The Builder and Low-level files import all four sinks, so they need the kafka and postgres extras even for the console, and the parquet extra for the file sink.
Steps
Describe the Arrival
An arrival starts one order about every second: dist: exponential with rate: 1 draws gaps that average 1 second, and spawn: order names the task each arrival starts.
sampler=Sampler(seed=42)customer=sampler.generator("tasks.order.payload.customer_id")total=sampler.generator("tasks.order.payload.total")orders_gap=DistributionConfig(dist="exponential",rate=1)# The random sequence the blueprint's draw usesorders_gap.stream="arrival.orders"
Sampler(seed=42) supplies every draw: a random generator for each payload field and for the gap. The stream line names the random sequence the blueprint uses for this arrival, so the three forms draw the same gaps.
Generate the Payload
The payload of a task maps each field to a generator. sequence numbers the orders from 1, integer draws a customer from 1 to 100, and uniform draws a total from 5 to 200 with 2 decimals.
order_payload=Template({"order_id":{"gen":"sequence","start":1},"customer_id":{"gen":"integer","bounds":[1,100]},"total":{"gen":"uniform","low":5,"high":200,"decimals":2},},name="tasks.order",)@app.task(name="order")deforder(task_id):"""Gives one order record."""returnorder_payload
A Low-level script has no payload mapping. It draws each value and passes the finished dictionary to env.publish_event.
Choose the Sink
The egress decides where the records go. send_data_to_kafka.yaml, send_data_to_postgres.yaml and send_data_to_a_file.yaml are the console file with another egress section, and each Python file holds the four sinks as functions.
defconsole_sink()->ConsoleEgress:"""Prints each record."""returnConsoleEgress()defkafka_sink()->KafkaEgress:"""Writes the records to the topic `orders`."""returnKafkaEgress(bootstrap_servers=os.environ.get("KAFKA_BOOTSTRAP_SERVERS","localhost:9092"),event_topic="orders",)defpostgres_sink()->PostgresEgress:"""Writes each record as a row of the table `orders`."""returnPostgresEgress(DSN,tables={"orders":{"columns":{"order_id":"INT","customer_id":"INT","total":"REAL"},"primary_key":"order_id",}},)deffile_sink()->JsonlStorageEgress:"""Writes the records to a JSON Lines file."""returnJsonlStorageEgress(default_path=f"{os.environ.get('DEST_PATH','data')}/orders.jsonl")
build(sink) takes one of these functions and uses the console when it gets none, so build(kafka_sink).run(until=UNTIL) writes to Kafka. The SINK environment variable of Check the Result picks one of them when the file runs as a script.
defconsole_sink()->ConsoleEgress:"""Prints each record."""returnConsoleEgress()defkafka_sink()->KafkaEgress:"""Writes the records to the topic `orders`."""returnKafkaEgress(bootstrap_servers=os.environ.get("KAFKA_BOOTSTRAP_SERVERS","localhost:9092"),event_topic="orders",)defpostgres_sink()->PostgresEgress:"""Writes each record as a row of the table `orders`."""returnPostgresEgress(DSN,tables={"orders":{"columns":{"order_id":"INT","customer_id":"INT","total":"REAL"},"primary_key":"order_id",}},)deffile_sink()->JsonlStorageEgress:"""Writes the records to a JSON Lines file."""returnJsonlStorageEgress(default_path=f"{os.environ.get('DEST_PATH','data')}/orders.jsonl")
run(sink) takes one of these functions in the same way and calls env.setup_egress with it. The SINK environment variable of Check the Result picks one of them when the file runs as a script.
Check the Result
Run the console model for 10 simulated seconds and print its first 5 records. Orders count from 1, every customer is from 1 to 100 and every total is from 5 to 200.
# Kafka: the first three records of the topic ordersdockerexeckafka/opt/kafka/bin/kafka-console-consumer.sh\--bootstrap-serverlocalhost:9092--topicorders--from-beginning--max-messages3# PostgreSQL: the number of rows of the table ordersdockerexecpostgrespsql-Uuser-dodctl-c"SELECT count(*) FROM orders;"# File: the number of records in the files of the folder datacatdata/orders_*.jsonl|wc-l
A run of 10 simulated seconds writes 10 records, so the PostgreSQL count and the file count are 10 after one run. A second run appends to the topic and adds a new file to the folder, and PostgreSQL skips an order number the table already holds, so the count stays at 10 until you drop the table.
SINK names one of the four functions of the Steps section, and the console is the default. The console run prints every record of the run as an [EVT] line with the same fields and values. The other three print log lines only.
Read the records back from the sink. Run these after the run, with the service from Before You Start still up for Kafka and PostgreSQL:
# Kafka: the first three records of the topic ordersdockerexeckafka/opt/kafka/bin/kafka-console-consumer.sh\--bootstrap-serverlocalhost:9092--topicorders--from-beginning--max-messages3# PostgreSQL: the number of rows of the table ordersdockerexecpostgrespsql-Uuser-dodctl-c"SELECT count(*) FROM orders;"# File: the number of records in the files of the folder datacatdata/orders_*.jsonl|wc-l
A run of 10 simulated seconds writes 10 records, so the PostgreSQL count and the file count are 10 after one run. A second run appends to the topic and adds a new file to the folder, and PostgreSQL skips an order number the table already holds, so the count stays at 10 until you drop the table.
SINK names one of the four functions of the Steps section, and the console is the default. The console run prints every record of the run as an [EVT] line with the same fields and values. The other three print log lines only.
Read the records back from the sink. Run these after the run, with the service from Before You Start still up for Kafka and PostgreSQL:
# Kafka: the first three records of the topic ordersdockerexeckafka/opt/kafka/bin/kafka-console-consumer.sh\--bootstrap-serverlocalhost:9092--topicorders--from-beginning--max-messages3# PostgreSQL: the number of rows of the table ordersdockerexecpostgrespsql-Uuser-dodctl-c"SELECT count(*) FROM orders;"# File: the number of records in the files of the folder datacatdata/orders_*.jsonl|wc-l
A run of 10 simulated seconds writes 10 records, so the PostgreSQL count and the file count are 10 after one run. A second run appends to the topic and adds a new file to the folder, and PostgreSQL skips an order number the table already holds, so the count stays at 10 until you drop the table.
The Kafka, PostgreSQL and file models write these same records to their sink. Kafka receives them on the topic orders and PostgreSQL writes a row of the table orders for each, with order_id as the primary key. The file model writes the records as flat rows of JSON Lines files named data/orders_<8 hex digits>.jsonl, one file for each batch, as Egress, Batching and Delivery sets out for file names. DEST_PATH sets another folder.
# Send data to a sink: one arrival, one generated payload, written to the console.## An order arrives about once a second. Its record holds an order number, a customer# and a total. The Console egress prints each record. The files send_data_to_kafka.yaml,# send_data_to_postgres.yaml and send_data_to_a_file.yaml are this file with another# egress.## Needs: nothing.# Run it with: desflow run docs/snippets/generate-data/send_data_to_a_sink.yamlsimulation:sim_id:Shopfactor:0random_seed:42logical_start_time:2026-01-01T00:00:00egress:-{type:Console}arrivals:orders:{dist:exponential, rate:1, spawn:order}tasks:order:payload:order_id:{gen:sequence, start:1}customer_id:{gen:integer, bounds:[1,100]}total:{gen:uniform, low:5, high:200, decimals:2}run:{until:10}
# Send data to a sink: the orders of send_data_to_a_sink.yaml, written to a Kafka topic.## Only the egress differs from that file.## Needs: a broker, with odctl up kafka-lite, and the kafka extra.# KAFKA_BOOTSTRAP_SERVERS sets the broker address.# Run it with: desflow run docs/snippets/generate-data/send_data_to_kafka.yamlsimulation:sim_id:Shopfactor:0random_seed:42logical_start_time:2026-01-01T00:00:00egress:-type:Kafkaconfig:bootstrap_servers:${KAFKA_BOOTSTRAP_SERVERS:-localhost:9092}event_topic:ordersarrivals:orders:{dist:exponential, rate:1, spawn:order}tasks:order:payload:order_id:{gen:sequence, start:1}customer_id:{gen:integer, bounds:[1,100]}total:{gen:uniform, low:5, high:200, decimals:2}run:{until:10}
# Send data to a sink: the orders of send_data_to_a_sink.yaml, written to a PostgreSQL# table.## Only the egress differs from that file.## Needs: a database, with odctl up postgres, and the postgres extra.# Run it with: desflow run docs/snippets/generate-data/send_data_to_postgres.yamlsimulation:sim_id:Shopfactor:0random_seed:42logical_start_time:2026-01-01T00:00:00egress:-type:Postgresconfig:connection_dsn:postgresql://user:password@127.0.0.1:5432/odctltables:orders:columns:{order_id:INT, customer_id:INT, total:REAL}primary_key:order_idarrivals:orders:{dist:exponential, rate:1, spawn:order}tasks:order:payload:order_id:{gen:sequence, start:1}customer_id:{gen:integer, bounds:[1,100]}total:{gen:uniform, low:5, high:200, decimals:2}run:{until:10}
# Send data to a sink: the orders of send_data_to_a_sink.yaml, written to a JSON# Lines file.## Only the egress differs from that file.## Needs: the parquet extra, which holds the file writers. DEST_PATH sets the folder# (data by default).# Run it with: desflow run docs/snippets/generate-data/send_data_to_a_file.yamlsimulation:sim_id:Shopfactor:0random_seed:42logical_start_time:2026-01-01T00:00:00egress:-type:Jsonlconfig:default_path:${DEST_PATH:-data}/orders.jsonlarrivals:orders:{dist:exponential, rate:1, spawn:order}tasks:order:payload:order_id:{gen:sequence, start:1}customer_id:{gen:integer, bounds:[1,100]}total:{gen:uniform, low:5, high:200, decimals:2}run:{until:10}
"""Send data to a sink: one arrival, one generated payload, builder API.The builder version of `send_data_to_a_sink.yaml`: an order about every second,written to the console, and to Kafka, PostgreSQL or a JSON Lines file when `sink` isanother function of the file.Needs: the kafka and postgres extras, because the file imports every sink, and theparquet extra for the file sink. The console needs no service. The Kafka sink needs abroker (odctl up kafka-lite) and the PostgreSQL sink a database (odctl up postgres).Run it with: python docs/snippets/generate-data/send_data_to_a_sink_builder.pySINK sets the sink: console (the default), kafka, postgres or file."""importloggingimportosfromdatetimeimportdatetime,timezonefromdesflowimport(ConsoleEgress,JsonlStorageEgress,KafkaEgress,PostgresEgress,SimulationContext,)fromdesflow.valuesimportTemplateUNTIL=10DSN="postgresql://user:password@127.0.0.1:5432/odctl"defconsole_sink()->ConsoleEgress:"""Prints each record."""returnConsoleEgress()defkafka_sink()->KafkaEgress:"""Writes the records to the topic `orders`."""returnKafkaEgress(bootstrap_servers=os.environ.get("KAFKA_BOOTSTRAP_SERVERS","localhost:9092"),event_topic="orders",)defpostgres_sink()->PostgresEgress:"""Writes each record as a row of the table `orders`."""returnPostgresEgress(DSN,tables={"orders":{"columns":{"order_id":"INT","customer_id":"INT","total":"REAL"},"primary_key":"order_id",}},)deffile_sink()->JsonlStorageEgress:"""Writes the records to a JSON Lines file."""returnJsonlStorageEgress(default_path=f"{os.environ.get('DEST_PATH','data')}/orders.jsonl")SINKS={"console":console_sink,"kafka":kafka_sink,"postgres":postgres_sink,"file":file_sink,}defbuild(sink=console_sink)->SimulationContext:"""Builds the model, with the egress that `sink` makes."""app=SimulationContext(sim_id="Shop",factor=0,random_seed=42,logical_start_time=datetime(2026,1,1,tzinfo=timezone.utc),)app.add_egress(sink())app.add_arrival("orders","exponential",rate=1,spawn="order")order_payload=Template({"order_id":{"gen":"sequence","start":1},"customer_id":{"gen":"integer","bounds":[1,100]},"total":{"gen":"uniform","low":5,"high":200,"decimals":2},},name="tasks.order",)@app.task(name="order")deforder(task_id):"""Gives one order record."""returnorder_payloadreturnappif__name__=="__main__":logging.basicConfig(level=logging.INFO,format="%(levelname)s%(message)s")build(SINKS[os.environ.get("SINK","console")]).run(until=UNTIL)
"""Send data to a sink: one arrival, one generated payload, low-level API.The low-level version of `send_data_to_a_sink.yaml`: plain SimPy on a`DynamicRealtimeEnvironment`, which publishes each record with `publish_event`. Eachdraw is made with `Sampler`, from the random sequence the blueprint's generator at thatplace uses, so the draws are the blueprint's. The four sinks are functions, and `run`takes one of them.Needs: the kafka and postgres extras, because the file imports every sink, and theparquet extra for the file sink. The console needs no service. The Kafka sink needs abroker (odctl up kafka-lite) and the PostgreSQL sink a database (odctl up postgres).Run it with: python docs/snippets/generate-data/send_data_to_a_sink_low_level.pySINK sets the sink: console (the default), kafka, postgres or file."""importloggingimportosfromdatetimeimportdatetime,timezonefromdesflowimport(ConsoleEgress,DistributionConfig,DynamicRealtimeEnvironment,JsonlStorageEgress,KafkaEgress,PostgresEgress,Sampler,)UNTIL=10DSN="postgresql://user:password@127.0.0.1:5432/odctl"defconsole_sink()->ConsoleEgress:"""Prints each record."""returnConsoleEgress()defkafka_sink()->KafkaEgress:"""Writes the records to the topic `orders`."""returnKafkaEgress(bootstrap_servers=os.environ.get("KAFKA_BOOTSTRAP_SERVERS","localhost:9092"),event_topic="orders",)defpostgres_sink()->PostgresEgress:"""Writes each record as a row of the table `orders`."""returnPostgresEgress(DSN,tables={"orders":{"columns":{"order_id":"INT","customer_id":"INT","total":"REAL"},"primary_key":"order_id",}},)deffile_sink()->JsonlStorageEgress:"""Writes the records to a JSON Lines file."""returnJsonlStorageEgress(default_path=f"{os.environ.get('DEST_PATH','data')}/orders.jsonl")START=datetime(2026,1,1,tzinfo=timezone.utc)ENVIRONMENT:dict={"logical_start_time":START,"timezone":"UTC"}defbuild(env:DynamicRealtimeEnvironment)->None:"""Starts the arrivals on `env`."""sampler=Sampler(seed=42)customer=sampler.generator("tasks.order.payload.customer_id")total=sampler.generator("tasks.order.payload.total")orders_gap=DistributionConfig(dist="exponential",rate=1)# The random sequence the blueprint's draw usesorders_gap.stream="arrival.orders"deforders_source():number=0whileTrue:yieldenv.timeout(sampler.sample(orders_gap))number+=1payload={"order_id":number,"customer_id":int(customer.integers(1,100+1)),"total":round(float(total.uniform(5,200)),2),}env.publish_event(f"orders-{number}",payload,task="order",instance=number-1)env.process(orders_source())SINKS={"console":console_sink,"kafka":kafka_sink,"postgres":postgres_sink,"file":file_sink,}defrun(sink=console_sink)->None:"""Runs the model unpaced into the egress that `sink` makes."""env=DynamicRealtimeEnvironment(factor=0,**ENVIRONMENT)env.setup_egress([sink()])build(env)try:env.run(until=UNTIL)finally:env.teardown()if__name__=="__main__":logging.basicConfig(level=logging.INFO,format="%(levelname)s%(message)s")run(SINKS[os.environ.get("SINK","console")])