Skip to main content

Kafka Activities

Two activities: kafka.publish sends messages to a topic, and kafka.consume subscribes to one and feeds each message into the running workflow as an event.

Setup

The broker comes from the broker input field or, when that is unset, from MOCO_KAFKA_BROKER. There is no secret-store integration; SASL/SSL settings go in kafka_config on kafka.consume.

The worker needs the confluent_kafka package (and its bundled librdkafka).

Defaults

ActivityTimeoutMax attemptsHeartbeat
kafka.publish60 s3
kafka.consume86400 s (1 day)120 s interval / 60 s timeout

kafka.publish

Publishes one or more messages to a topic. Each message is a [key, value] pair; the value is a JSON-serializable object.

By default the activity does not wait for broker acknowledgement (no_wait: true) — it enqueues and returns. Set no_wait: false when you need to know each message landed.

Input

FieldTypeRequiredDefaultDescription
messageslist[[key, value]]yesMessages as two-element lists: a string key (or null) and an object value
topicstrnonullDestination topic
brokerstrnoMOCO_KAFKA_BROKERBootstrap server list
no_waitboolnotrueReturn without waiting for delivery acknowledgement

Output

With no_wait: true (the default): null.

With no_wait: false: a list with one entry per message —

FieldTypeDescription
resultstr"success" or "fail"
messagestrDelivery details, or the error when the result is fail

Example

- activity:
name: publish-orders
type: kafka.publish
input_data:
topic: "orders.enriched"
broker: "{{ kafka_broker }}"
no_wait: false # wait for acknowledgement so failures surface here
messages: "{{ [ [o['order_id'], o] for o in enriched_orders ] }}"
output_name: publish_result # -> [{result, message}, ...]

- abort:
condition: "{{ any(r['result'] == 'fail' for r in publish_result) }}"
type: raise
message: "Kafka delivery failed: {{ publish_result }}"
Fire-and-forget is the default

With no_wait: true a broker outage is invisible to the workflow — the activity succeeds and the messages are lost. Use no_wait: false for anything you cannot afford to drop.


kafka.consume

Subscribes to a topic and relays each message into the running workflow as an event, which the workflow picks up with wait_for. It runs until cancelled.

Input

FieldTypeRequiredDefaultDescription
topicstryesTopic to subscribe to
consumer_groupstryesConsumer group id; offsets are tracked per group as usual
brokerstrnoMOCO_KAFKA_BROKERBootstrap server list
kafka_configdictnonullExtra librdkafka settings — SASL, SSL, auto.offset.reset, …
relay_topicstrno"default"Event topic the workflow listens on
relay_event_typestrnonullevent_type stamped on each relayed event
target_workflow_idstrnocurrent workflowWorkflow the events are delivered to

Output

Returned when the activity is cancelled:

FieldTypeDescription
stoppedboolAlways true
last_offsetsdict[str, int]Last offset seen, keyed "<topic>:<partition>"

Relayed event data

Each message arrives at wait_for as an event whose data is:

FieldTypeDescription
keyanyMessage key — parsed JSON, else a string, else raw bytes
valueanyMessage value, decoded the same way
topicstrSource topic
partitionintSource partition
offsetintMessage offset

Example

Start the consumer in the background, then handle messages as they arrive:

- activity:
name: start-consumer
type: kafka.consume
async_mode: true # required: otherwise this blocks for a day
input_data:
topic: "orders.raw"
consumer_group: "moco-enricher"
broker: "{{ kafka_broker }}"
relay_topic: "kafka.msg"
relay_event_type: "kafka_message"
target_workflow_id: "{{ __sys_info__['workflow_id'] }}"

- iteration:
input_data: "{{ range(max_messages) }}"
body:
sequence:
elements:
- wait_for:
event_source_topic: "kafka.msg"
event_type: "kafka_message"
timeout_sec: 300
output_name: msg
- activity:
type: sql.execute
input_data:
connection_string_secret_key: "ORDERS_DB_CONN"
statement: "INSERT INTO order_raw (order_id, payload) VALUES (:id, :payload)"
parameters:
id: "{{ msg['data']['key'] }}"
payload: "{{ msg['data']['value'] }}"
Start it asynchronously

kafka.consume blocks for its whole timeout — a full day. Set async_mode: true, or run it in a parallel branch alongside the statements that consume its events. Unlike rabbit.receive, it does not default to async mode.

Every message costs workflow history

On the Temporal runtime each relayed message becomes a workflow signal and a history entry, and the engine keeps at most 1000 unmatched events per topic before evicting the oldest. This activity suits low-rate control topics, not high-throughput data streams — for those, consume outside the workflow and start a workflow per batch.

Offsets and delivery

Offsets are committed by the consumer group normally, but relayed events are independent of that: a workflow that crashes can lose messages already consumed and relayed. The last_offsets in the output, and the same values in the activity's heartbeat details, are what a retry would use to reason about where it got to.