<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Samuel Matioli</title>
    <description>The latest articles on DEV Community by Samuel Matioli (@smatioli).</description>
    <link>https://dev.to/smatioli</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1208972%2F8b92c9b8-6d3c-433d-8d7f-b94d6ef89e17.jpeg</url>
      <title>DEV Community: Samuel Matioli</title>
      <link>https://dev.to/smatioli</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/smatioli"/>
    <language>en</language>
    <item>
      <title>JEV-as-a-Router: What If Kafka Events Could Choose Their Own Topic?</title>
      <dc:creator>Samuel Matioli</dc:creator>
      <pubDate>Mon, 26 Oct 2026 14:23:33 +0000</pubDate>
      <link>https://dev.to/smatioli/jev-as-a-router-what-if-kafka-events-could-choose-their-own-topic-5hi8</link>
      <guid>https://dev.to/smatioli/jev-as-a-router-what-if-kafka-events-could-choose-their-own-topic-5hi8</guid>
      <description>&lt;p&gt;Most event-driven AI use cases do not begin with text generation. They begin with a decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is this event urgent?&lt;/li&gt;
&lt;li&gt;Does it require a person?&lt;/li&gt;
&lt;li&gt;Which team owns it?&lt;/li&gt;
&lt;li&gt;Does it look like fraud?&lt;/li&gt;
&lt;li&gt;Which workflow should run next?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Automating these small decisions is what lets a business process scale.&lt;/p&gt;

&lt;p&gt;That is the problem I wanted to solve by connecting Kafka to JEV.&lt;/p&gt;

&lt;p&gt;In this post, we will build a pipeline that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;consumes support tickets from Kafka;&lt;/li&gt;
&lt;li&gt;classifies each ticket with JEV;&lt;/li&gt;
&lt;li&gt;enriches the original event with the answers;&lt;/li&gt;
&lt;li&gt;routes the result to a topic selected from those answers; and&lt;/li&gt;
&lt;li&gt;measures latency and cost across 20,000 evaluations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All code and configuration are available in the &lt;a href="https://github.com/smatiolids/kafka_jev_demo" rel="noopener noreferrer"&gt;Kafka JEV demo repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Prefer video? &lt;a href="https://www.youtube.com/watch?v=C1CLjmetbmI&amp;amp;t=2s" rel="noopener noreferrer"&gt;An earlier version of this walkthrough is available on YouTube&lt;/a&gt;. It is presented in Portuguese, with subtitles available.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Kafka-JEV connector
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://github.com/smatiolids/kafka-jev-connector" rel="noopener noreferrer"&gt;Kafka-JEV-Connector&lt;/a&gt; as an open-source Kafka Connect sink connector.&lt;/p&gt;

&lt;p&gt;It consumes records from selected topics, builds a state from each record, submits that state and a set of questions to JEV, and publishes an enriched record.&lt;/p&gt;

&lt;p&gt;The destination can be fixed or derived from a JEV answer. In the second case, the connector also acts as a message router.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhgrv39o1qfqfjcyulkj6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhgrv39o1qfqfjcyulkj6.png" alt="Kafka-JEV-Connector architecture" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Failures can be sent to a dead-letter queue. Successful evaluations produce one output record in one destination topic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The example: route support tickets
&lt;/h2&gt;

&lt;p&gt;The demo reads support tickets from a CSV file and publishes them to a &lt;code&gt;messages&lt;/code&gt; topic.&lt;/p&gt;

&lt;p&gt;The connector evaluates each ticket, enriches it, and writes the result to a topic whose name starts with &lt;code&gt;messages_jev&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Before configuring Kafka Connect, we need to define two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Questions:&lt;/strong&gt; what do we want the model to decide?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State:&lt;/strong&gt; which event data does the model need to make those decisions?&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Define the questions
&lt;/h3&gt;

&lt;p&gt;The question set covers human intervention, cancellation risk, department, customer intent, urgency, and dissatisfaction.&lt;/p&gt;

&lt;p&gt;It combines &lt;code&gt;noul&lt;/code&gt;, &lt;code&gt;choice&lt;/code&gt;, and &lt;code&gt;score&lt;/code&gt; questions so the routing examples can use continuous values, named choices, and ordered levels.&lt;/p&gt;

&lt;p&gt;The criteria and answer keys are part of the routing contract. For example, the &lt;code&gt;department&lt;/code&gt; choices become topic suffixes, while urgency scores can be compared with a business-defined threshold.&lt;/p&gt;

&lt;p&gt;The questions live in &lt;code&gt;config/questions.json&lt;/code&gt;. A helper script serializes them into the Kafka Connect configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 config/set_questions.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Build the model state
&lt;/h3&gt;

&lt;p&gt;If questions define what we want to know, state defines what JEV needs to know.&lt;/p&gt;

&lt;p&gt;The connector can send the complete record value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"state.mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FULL_VALUE"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is convenient, but an event may contain irrelevant or sensitive fields. Raw JSON may also provide less context than a short, labeled description.&lt;/p&gt;

&lt;p&gt;The demo therefore uses &lt;code&gt;TEMPLATE&lt;/code&gt; mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"state.mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TEMPLATE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"state.template"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ticket: ${key}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Subject: ${value:/subject:-(no subject)}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Message: ${value:/body:-(no message)}"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a record with key &lt;code&gt;ticket-42&lt;/code&gt;, subject &lt;code&gt;Payment failed&lt;/code&gt;, and body &lt;code&gt;My card was charged twice&lt;/code&gt;, JEV receives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ticket: ticket-42
Subject: Payment failed
Message: My card was charged twice
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives each value a meaning and controls which fields leave Kafka.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F23l1ichduvljhlwgy0yj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F23l1ichduvljhlwgy0yj.png" alt="Building state with a template" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Templates can read the complete key or value, JSON Pointer fields, headers, and Kafka metadata:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Expression&lt;/th&gt;
&lt;th&gt;Content&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;${value}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Complete record value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;${key}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Complete record key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;${value:/customer/name}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Value field selected by JSON Pointer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;${key:/tenant/id:-unknown}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Key field with a default&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;${header:trace-id}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Last header with this name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;${metadata:topic}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Source topic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;${metadata:partition}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Source partition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;${metadata:offset}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Source offset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;${metadata:timestamp}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Record timestamp&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;:-&lt;/code&gt; fragment provides a fallback. Without one, a missing reference is a permanent failure and the connector sends the record to the DLQ.&lt;/p&gt;

&lt;p&gt;Template mode turns each event into focused, dynamic context without requiring another custom consumer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure the connector
&lt;/h2&gt;

&lt;p&gt;With the questions and state defined, the core connector configuration looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"connector.class"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"io.github.smatiolids.kafkajev.JevSinkConnector"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tasks.max"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"topics"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"messages"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output.topic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"messages_jev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"errors.deadletter.topic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"messages_jev_dlq"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output.bootstrap.servers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${env:KAFKA_BOOTSTRAP_SERVERS}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jev.api.key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${env:TYPESAFE_API_KEY}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jev.endpoint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.typesafe.ai/v1/systemone"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jev.model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jev-latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"question.set.id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"message_classification/v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"state.mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TEMPLATE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"state.template"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ticket: ${key}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Subject: ${value:/subject:-(no subject)}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;Message: ${value:/body:-(no message)}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"behavior.on.null.values"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"IGNORE"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;topics&lt;/code&gt; selects the input. &lt;code&gt;errors.deadletter.topic&lt;/code&gt; selects the failure destination. Despite its singular name, &lt;code&gt;output.topic&lt;/code&gt; can also be a template that selects the success destination.&lt;/p&gt;

&lt;p&gt;The connector does not create topics. Provision every possible destination before processing records.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four routing patterns
&lt;/h2&gt;

&lt;p&gt;The same enriched record can support several routing policies.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2yktc7puebrjgv6fakq7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2yktc7puebrjgv6fakq7.png" alt="Four event-routing policies" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Send everything to one topic
&lt;/h3&gt;

&lt;p&gt;The simplest policy uses a fixed topic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output.topic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"messages_jev"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every successful result goes to &lt;code&gt;messages_jev&lt;/code&gt;. The answers enrich the event but do not affect its route.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Route by a choice
&lt;/h3&gt;

&lt;p&gt;We can insert the selected department into the topic name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output.topic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"messages_jev_${answers.department.choice}"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;billing&lt;/code&gt; result goes to &lt;code&gt;messages_jev_billing&lt;/code&gt;; &lt;code&gt;technical_support&lt;/code&gt; goes to &lt;code&gt;messages_jev_technical_support&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here, JEV does more than label the ticket. Its answer selects the workflow that receives it.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Route by a threshold
&lt;/h3&gt;

&lt;p&gt;For a &lt;code&gt;noul&lt;/code&gt; answer, the routing rule can compare its value with a business threshold:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output.topic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"messages_jev_${answers.requires_human.noul &amp;gt; 0.8; &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;to_human&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;to_agent&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Values above &lt;code&gt;0.8&lt;/code&gt; go to &lt;code&gt;messages_jev_to_human&lt;/code&gt;. All other values go to &lt;code&gt;messages_jev_to_agent&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The model produces the value; our rule chooses the cutoff. We can make the policy more conservative when a wrong automated route has greater impact.&lt;/p&gt;

&lt;p&gt;The same pattern works with a continuous score:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output.topic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"messages_jev_${answers.urgency.score &amp;gt; 2 ; &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;urgent&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;normal&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This rule routes tickets to either &lt;code&gt;messages_jev_urgent&lt;/code&gt; or &lt;code&gt;messages_jev_normal&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Route by a score level
&lt;/h3&gt;

&lt;p&gt;We can also preserve each level of the original scale:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output.topic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"messages_jev_dissatisfaction_${answers.customer_dissatisfaction.level}"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With four criteria, the possible topic suffixes are &lt;code&gt;0&lt;/code&gt;, &lt;code&gt;1&lt;/code&gt;, &lt;code&gt;2&lt;/code&gt;, and &lt;code&gt;3&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Each evaluation still produces one publication. The connector selects one destination; it does not fan out the result to several topics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the demo locally
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;.env&lt;/code&gt; from the repository example and set &lt;code&gt;TYPESAFE_API_KEY&lt;/code&gt;. Then start Kafka and Kafka Connect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; compose.kafka.yaml &lt;span class="nt"&gt;-f&lt;/span&gt; compose.yaml up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--build&lt;/span&gt; &lt;span class="nt"&gt;--wait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the connector status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--fail&lt;/span&gt; &lt;span class="nt"&gt;--silent&lt;/span&gt; localhost:8083/connectors/jev/status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a virtual environment, install the helper dependencies, and send ten tickets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
.venv/bin/pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
.venv/bin/python helper/send_message.py &lt;span class="nt"&gt;--limit&lt;/span&gt; 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each ticket triggers one JEV call, so starting with a small batch is useful.&lt;/p&gt;

&lt;p&gt;Follow every output topic with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;.venv/bin/python helper/receive_messages.py &lt;span class="nt"&gt;--from-beginning&lt;/span&gt; &lt;span class="nt"&gt;--pretty&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To try another routing policy, change &lt;code&gt;CONNECTOR_CONFIG_FILE&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt; to one of the &lt;code&gt;connector-local*.json&lt;/code&gt; files and restart the connector environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results from 20,000 evaluations
&lt;/h2&gt;

&lt;p&gt;I ran all 4,000 tickets through each of the five output configurations.&lt;/p&gt;

&lt;p&gt;That produced 20,000 enriched records, plus the 4,000 original input events. Every evaluation used the resolved model &lt;code&gt;jev-1.14.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5pus0dbeghdeqfe7m788.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5pus0dbeghdeqfe7m788.png" alt="Execution results" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Routing distributions
&lt;/h3&gt;

&lt;p&gt;Each policy processed the same dataset but exposed a different operational view:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Policy&lt;/th&gt;
&lt;th&gt;Distribution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fixed&lt;/td&gt;
&lt;td&gt;4,000 in &lt;code&gt;messages_jev&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Department&lt;/td&gt;
&lt;td&gt;3,237 technical support; 414 billing; 341 general support; 7 account security; 1 retention&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human intervention&lt;/td&gt;
&lt;td&gt;2,203 to agent; 1,797 to human&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Urgency&lt;/td&gt;
&lt;td&gt;2,427 normal; 1,573 urgent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dissatisfaction&lt;/td&gt;
&lt;td&gt;927 at level 0; 1,945 at level 1; 1,128 at level 2; none at level 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DLQ&lt;/td&gt;
&lt;td&gt;No records&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each policy ran a fresh evaluation. Because results are not deterministic, small differences between runs are expected.&lt;/p&gt;

&lt;p&gt;The average &lt;code&gt;requires_human&lt;/code&gt; value was &lt;code&gt;0.74&lt;/code&gt;. At a threshold of &lt;code&gt;0.5&lt;/code&gt;, 90.1% of tickets would be true.&lt;/p&gt;

&lt;p&gt;Our stricter &lt;code&gt;0.8&lt;/code&gt; rule sent 1,797 tickets to a person and allowed 2,203 to continue to an agent.&lt;/p&gt;

&lt;p&gt;Urgency shows the same model-versus-policy separation. With &lt;code&gt;score &amp;gt; 2&lt;/code&gt;, 1,573 tickets, or 39.3%, entered the urgent path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Latency and reliability
&lt;/h3&gt;

&lt;p&gt;Across the 20,000 calls, observed JEV latency was:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Average&lt;/td&gt;
&lt;td&gt;210 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p50&lt;/td&gt;
&lt;td&gt;201 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p95&lt;/td&gt;
&lt;td&gt;279 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p99&lt;/td&gt;
&lt;td&gt;329 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maximum&lt;/td&gt;
&lt;td&gt;1,518 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All evaluations succeeded on the first attempt. No records were retried or sent to the DLQ.&lt;/p&gt;

&lt;p&gt;This is one test on one workload, not a universal benchmark. It does show the profile we were looking for: structured per-event decisions with a p95 below 300 ms in this run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost
&lt;/h3&gt;

&lt;p&gt;The run consumed 20,002,245 input tokens and 3,737,920 output tokens, averaging 1,187 total tokens per record.&lt;/p&gt;

&lt;p&gt;At the JEV rate used for this test—USD 0.042 per million input tokens—the complete run cost about USD 0.84.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Projection&lt;/th&gt;
&lt;th&gt;Estimated cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One evaluation&lt;/td&gt;
&lt;td&gt;USD 0.000042&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000 evaluations&lt;/td&gt;
&lt;td&gt;USD 0.042&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 million evaluations&lt;/td&gt;
&lt;td&gt;USD 42.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pricing changes, so treat these figures as a record of the test rather than a current quote.&lt;/p&gt;

&lt;p&gt;We evaluated the same 4,000 tickets five times. A single pass under the same conditions would cost about USD 0.17.&lt;/p&gt;

&lt;p&gt;You can inspect the &lt;a href="https://github.com/smatiolids/kafka_jev_demo/blob/main/docs/results.txt" rel="noopener noreferrer"&gt;complete captured results&lt;/a&gt; or reproduce the summary locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;.venv/bin/python helper/summarize_results.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;JEV can estimate whether a ticket needs a person, how urgent it is, or which department fits best. Kafka Connect can turn that estimate into a repeatable route with explicit thresholds and failure handling.&lt;/p&gt;

&lt;p&gt;This pattern is useful when you need to classify, prioritize, or route events but do not need to generate prose.&lt;/p&gt;

&lt;p&gt;It keeps the decision output structured, the business threshold visible, and the event pipeline composable.&lt;/p&gt;

&lt;p&gt;If you want to try it, clone the &lt;a href="https://github.com/smatiolids/kafka_jev_demo" rel="noopener noreferrer"&gt;demo&lt;/a&gt; and explore the &lt;a href="https://github.com/smatiolids/kafka-jev-connector" rel="noopener noreferrer"&gt;connector source&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If the project is useful, a GitHub star is always appreciated.&lt;/p&gt;

</description>
      <category>jev</category>
      <category>kafka</category>
      <category>genai</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
