> ## Documentation Index
> Fetch the complete documentation index at: https://docs.alginte.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Enrich from a service

> Add a service's answer to every record — a model's decision, a score, a provider's status. The service is declared by the console; the preview simulates it.

> [!NOTE]
>
> 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](https://docs.alginte.com/operate/configuration.md#enrich-services).
One service, in an `application.properties` file:

```properties theme={null}
alginte.services.jev.kind=http-json
alginte.services.jev.base-url=https://api.example.com
alginte.services.jev.auth-scheme=Bearer
alginte.services.jev.auth-value-env=JEV_API_KEY
alginte.services.jev.timeout=2s
alginte.services.jev.retry-statuses=429,500,502,503,504,529
alginte.services.jev.answer-example={"answers":{"severity":{"choice":"high","confidence":0.8}}}
```

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.

> [!TIP]
>
> To try the node without a service of your own, use the [playground](https://docs.alginte.com/installation/playground.md):
> 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)**.

| Field | What it does |
| - | - |
| **Service** | One of the services this console declares, chosen from a list. The list shows each service's kind and timeout, never its address. |
| **Request** | A SpEL expression over `key` and `value` that builds what is sent: the service receives it as a JSON body. A map or a list is written as JSON exactly as a JSON sink would write it; a String is sent as written, for when you build the JSON yourself. |
| **Path suffix** | Appended to the service's address, for example `/v1/classify`. It must start with a single `/`, and it cannot point the call at another host. |
| **Simulated answer** | What the preview adds instead of calling: raw JSON, in the service's own shape. See [below](#the-preview-never-calls-the-service). |
| **Answer mapping** | Optional. A SpEL expression over `answer`, `key` and `value` that shapes the service's answer before it is added. Empty adds the answer as it came. |
| **Answer field** | The name the answer is added under. |
| **Timeout (ms)** | The whole budget for one record, retries included. Empty uses the service's own timeout. |
| **When the call fails** | **Fail the record**, the default, or **Add a fallback answer**. See [below](#when-a-call-fails). |

![The Enrich from a service drawer with the service jev chosen, a request expression, a path suffix and a simulated answer](https://mintcdn.com/alginte-9b6e883f/TESThHfrph7SKr2F/images/streams/enrichDrawer.png?fit=max\&auto=format\&n=TESThHfrph7SKr2F\&q=85\&s=3f8daf3d63a399228e4d6921715cbf7a)

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:

```text theme={null}
Request:         {'state': value.get('description')}
Answer mapping:  {'severity':   answer.get('answers').get('severity').get('choice'),
                  'confidence': answer.get('answers').get('severity').get('confidence')}
Answer field:    decision
```

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](https://docs.alginte.com/streams/spel.md#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](https://docs.alginte.com/operate/mcp.md) 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](https://docs.alginte.com/streams/deploy.md#when-a-stream-shows-error));
  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](https://mintcdn.com/alginte-9b6e883f/TESThHfrph7SKr2F/images/streams/enrichThroughput.png?fit=max\&auto=format\&n=TESThHfrph7SKr2F\&q=85\&s=f696719c2cc8587123f02a8f50ed7664)

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](#throughput-and-the-poll-loop), 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](https://mintcdn.com/alginte-9b6e883f/TESThHfrph7SKr2F/images/streams/enrichSubmitCheck.png?fit=max\&auto=format\&n=TESThHfrph7SKr2F\&q=85\&s=9e5821d37ed0768db1a0867fa32c2ebf)

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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.