Skip to main content
Available from Alginte 0.14.0.
Enrich from a service asks a service about each record and adds its answer to the record, under a field you name. The key is kept; the value comes out as a map with the answer in it. The node never filters or routes: put a Filter or a Split after it to act on the answer. It is for answers that are computed: a model’s decision about a record, a fraud score, a provider’s status. Reference data that changes slowly — a customer’s tier, a catalogue, a rate — is better streamed in as a table and joined, with no call per record at all.

Declare the service

A design names a service and never carries its address or its credential: those belong to the console that runs the stream, declared in its configuration. One service, in an application.properties file:
or as environment variables, the container’s way — ALGINTE_SERVICES_JEV_KIND=http-json, ALGINTE_SERVICES_JEV_BASEURL=https://api.example.com, and so on. The name after services. is what a design refers to.
To try the node without a service of your own, use the playground: its console declares a stand-in service, severity, that answers like a classifier, so a design asking it deploys and runs there. Its answer’s shape is the one in the example above.
The credential is never a property value. auth-value-env names the environment variable that holds it, and the console reads it when it makes a call, so the key stays out of every design, download and upload. The console checks every declaration when it starts and refuses to start on one that could never be called: an unknown kind, a missing or relative base-url, a timeout that is not positive.

The node

It is in the palette under Stateless, as Enrich from a service (HTTP).
The Enrich from a service drawer with the service jev chosen, a request expression, a path suffix and a simulated answer

The node's drawer: the service chosen from the console's list, the request it sends, and the simulated answer the preview adds instead of calling.

For example, with a service that classifies a CVE’s description:
a record {"id": "CVE-2026-0001", "description": "heap overflow in the parser"} comes out as {"id": "CVE-2026-0001", "description": "heap overflow in the parser", "decision": {"severity": "high", "confidence": 0.8}}.

The preview never calls the service

The sample preview and the expression editor never make a call. A preview that called would spend money on every keystroke, need the credential in the authoring console, and could answer the same record differently twice. Instead the node carries a simulated answer, and the preview adds it exactly as the running stream adds a real one — through the same mapping, into the same field — labelled simulated answer so it is never mistaken for the service’s.
  • Choosing a service fills it in. When the service declares an answer-example and the node has no simulated answer yet, the example is copied into the node. It is saved with the design and travels with a download, as test data.
  • Change it to walk every branch. A high score, a low confidence, an answer the mapping does not expect, the fallback: paste each one and watch what the nodes after it do. The editor checks the answer mapping against the simulated answer too, so a mapping that reaches into a field the answer does not have is flagged while you write it.
  • The request is built, only not sent. It is evaluated on the sample record like every other expression, so a request that would fail on a record fails in the preview.
  • An agent gets the same preview. In sandbox mode the MCP tool evaluate_expression evaluates an Enrich step with the simulated answer the agent passes, and the step’s note says the answer was simulated. Without one, the answer is null, and the note says that instead.
The preview can only show what the stream does given an answer. Whether the real service answers well is not something it can tell you.

What the record becomes

The value always comes out as a map, so write it with the JSON value serde:
  • a map-like value — JSON, JSON Schema, an Avro record, a Protobuf message — has its fields copied and the answer field added. Avro and Protobuf values are copied as their JSON, the same rendering the preview shows;
  • any other value becomes {"value": …, "<answer field>": …}.

When a call fails

The service’s retry-statuses (by default 429, 500, 502, 503 and 504) are retried with exponential backoff, from 100 ms up to 1 s between attempts, inside the timeout. Any other status — a 401 or a 422, say — fails the call at once, and so does a connection that cannot be made. An answer that is not JSON is a failure too.
  • Fail the record stops the stream: it shows ERROR, and its Info tab names the failure with the service and what went wrong, for example service 'jev': answered 422. Fix the cause and Stop/Start it (Deploy & lifecycle); the record is asked about again, since nothing after the last commit was kept.
  • Add a fallback answer adds the answer you give instead, and the stream carries on. Write it in the service’s own shape: it goes through the same answer mapping as a real answer and as the simulated one, so one mapping serves all three. To see the fallback’s branch in the preview, paste it as the simulated answer.

Throughput, and the poll loop

The call blocks the stream thread until the service answers: a thread makes one call at a time, so at a call of L seconds it handles at most 1/L records a second. More calls in flight need more tasks and more threads: the partitions of what reaches the node, which a repartition before it sets, and num.stream.threads on the stream. A thread also has to poll the broker at least every max.poll.interval.ms (300 seconds by default), and it polls only after it has worked through what the last poll returned. With slow calls, that whole batch has to fit: keep max.poll.records times the node’s timeout under half of max.poll.interval.ms. The whole interval is the limit itself; the half leaves room for what the arithmetic leaves out, such as the commit’s own time. With the defaults, 300 seconds and 1,000 records, and a 2-second timeout, that means max.poll.records at 75 or less: the interval divided by twice the timeout. A thread that falls outside it is taken out of the group and its work since the last commit is done again. Both settings go on the stream’s properties. The node’s drawer does this arithmetic for you. Under Throughput it shows the calls the node can have in flight at once, and where the number comes from: the partitions of the repartition before it, or of its input when there is none, up to num.stream.threads. With no repartition before the node, Add a repartition before this node puts one there, with no partition count yet, and opens it so you can set one. Below the number come the node’s warnings, each with a button that sets the stream property it names.
The Throughput section reading calls in flight up to 1, with an Add a repartition button and a warning whose button sets max.poll.records to 375

The drawer's Throughput section: one call in flight, and a warning that 1,000 records a poll at a 400 ms timeout is too much for the poll interval, with its fix.

There are two warnings:
  • The poll interval: max.poll.records times the node’s timeout is above half of max.poll.interval.ms. The fix sets max.poll.records to the interval divided by twice the timeout.
  • The stream threads: the repartition before the node has more partitions than num.stream.threads, so fewer calls are in flight than it allows. The fix sets num.stream.threads to its partition count.

What Submit refuses

Besides the fields the drawer marks as required, Submit refuses an Enrich node whose service this console does not declare — a design uploaded from another console is the usual case — and one whose service declares modifies-data=true, naming the node and the service. The services this console declares are also listed at GET /api/kafka-streams/enrich-services. The Submit Stream step checks the design as soon as you reach it, before you press Submit, and again after each fix. It lists what Submit would refuse and the throughput warnings, each with its fix. A warning never blocks the deploy.
The Submit Stream step showing a warning about max.poll.records with a button that sets it to 375, the note that warnings never block the deploy, and the Submit button

The Submit Stream step, checked on arrival: a throughput warning with its fix, above the design and the Submit button.

Limits

  • Services that modify nothing. A stream repeats a call after an error, and repeats the calls since its last commit after a restart, so a call that wrote would write again.
  • HTTP with JSON is the only kind of service so far.
  • On a stream, not a table. The node takes a KStream.
  • One call per node. A step that needs two services is two nodes, each with its own simulated answer and its own timeout.