Blog

Jev Went Viral, So We Integrated It into Lakehouse

avatarsundyliSep 22, 2026
Jev Went Viral, So We Integrated It into Lakehouse

Jev attracted a lot of attention soon after its release, so I tried it on a few classification tasks. The results were good enough, and the calls cheap enough, that I wanted to see what it would feel like inside a database rather than in another standalone AI script. I added Jev to the Databend Python UDF server and started running it against ordinary SQL queries.

The useful part turned out to be quite specific. Databases contain plenty of records that a person can classify after a quick read, while the equivalent keyword rule becomes brittle almost immediately. This article is a record of that integration: what Jev does, why it fits behind a Databend UDF, how I ran it, and where I would be careful in production.

What Jev is

Jev is the first model released by TypeSafe in a family the company calls System One models. The name comes from the fast, intuitive mode of thinking described in Daniel Kahneman's Thinking, Fast and Slow. The name makes sense once you use the API. Jev is built to make a bounded decision quickly, not to sit there composing an answer.

A request contains two main inputs:

  • Some source material, such as text, JSON, an email, or a support ticket.

  • A question whose answer type is defined in advance.

Jev currently supports three question types:

  • Noul
    returns the probability that a statement is true, as a number from 0 to 1.

  • Choice
    selects one item from a supplied set and returns the probability of every option.

  • Score
    evaluates an item against an ordered set of levels and returns a probability-weighted position.

These operations cover tasks such as classifying articles and media, extracting customer sentiment, making bounded decisions about clothing, food, or travel, and responding to game state in real time.

For example, given an email and three possible categories, Jev returns a typed result instead of an explanation:

{
"choice": "billing",
"probabilities": {
"billing": 0.91,
"technical": 0.07,
"sales": 0.02
},
"confidence": 0.82
}

That response can go straight into application logic. There is no paragraph to parse and no JSON-shaped prose to repair.

Why use a decision model for this work?

I have tried several versions of "make an LLM classify this." The usual recipe is a prompt that demands JSON, followed by parsing, schema validation, and a retry path for malformed output. It works, but it always feels wasteful when all I need is one category and the model still has to write the surrounding JSON one token at a time.

Jev avoids that path because the answer space is fixed before inference. I do not have to persuade it to follow an output format, and the result cannot wander off into an explanation.

The speed difference was immediately noticeable. TypeSafe describes typical calls as taking roughly a few hundred milliseconds or less. From Databend, including the network round trip, I usually saw a few hundred milliseconds rather than the multi-second pause I was used to with generative models. Jev also produces no output tokens, which makes the cost easier to tolerate when the job involves labeling a large number of rows.

What I found most useful, though, was the probability. Once that number is back in SQL, I can set a threshold, let high-confidence records continue automatically, and send uncertain cases to a review queue. Getting a confidence value I would trust from a general-purpose generative model has been much harder.

Jev also stays in its lane. It does not decide what the workflow should do next. SQL or application code still controls the path, and Jev is called only where the path needs a semantic judgment. That behavior is a good fit for a database UDF.

Where Jev does not fit

The narrow interface is also the main limitation. Jev cannot generate articles, code, summaries, or email replies. It only returns answers from a space defined by the caller. I would still use a generative model such as GPT or Claude for open-ended work.

It is also a poor fit for long chains of dependent reasoning. Independent questions can be evaluated together, but a workflow that derives B from A and then C from B should express those dependencies in code and call the model at each appropriate step.

A valid structure does not guarantee a correct decision. When TypeSafe describes the model as calibrated, the claim is statistical: among predictions assigned roughly 80% confidence, about 80% should be correct in aggregate. Individual predictions can still be wrong. High-risk actions need deterministic checks and, where appropriate, human approval.

Question and option design matter as well. I saw confidence drop when categories overlapped or the record lacked context. Jev does not remove the need to design and test the decision boundary.

There are practical constraints too. Jev is available through a hosted API, its weights are not public, and it currently accepts text and text-based structures such as JSON and arrays. It does not process images or audio. Data leaves the database, crosses a network, and incurs a charge.

The working rule I came away with is simple: use a generative model when the output must be open ended, use Jev for a bounded judgment, and keep workflow control in deterministic code.

Why put Jev behind a Databend UDF?

Databases contain many records that a person can interpret quickly but that resist stable keyword rules: customer feedback, support tickets, reviews, logs, product descriptions, and sales leads.

I would normally handle these records by selecting them with SQL, exporting them to a script, calling a model, and writing the results back. Each step is straightforward. The annoying part is owning the glue code and another scheduled job for what is conceptually one transformation.

Databend already supports a Python UDF server, while Jev accepts structured inputs and returns structured results. The interfaces lined up cleanly. Once I connected them, the semantic test looked like an ordinary SQL function:

SELECT *
FROM customer_feedback
WHERE jev(
OBJECT_CONSTRUCT('rating', rating, 'feedback', feedback),
'The customer reports a serious product failure that requires immediate support'
);

This is the boundary I wanted. Databend still scans, filters, joins, aggregates, and writes data. Jev only evaluates what a record means when a deterministic condition runs out of road.

Figure 1. Databend retains control of data execution and the workflow. Jev handles bounded semantic decisions.

The five UDFs

The integration exposes five functions:

FunctionReturn typePurpose
jev
BOOLEAN
Test whether a record satisfies a condition written in natural language
jev_prob
DOUBLE
Return the probability that a condition is true
jev_choice
VARCHAR
Select the best matching category from a list
jev_score
DOUBLE
Return a position on an ordered scale
jev_eval
VARIANT
Return the full result, including probabilities and confidence

The first four return scalar values that fit naturally into SQL expressions.

jev_eval
preserves the complete response for auditing, risk controls, or human review. The implementation is available in
python/example/jev.py
.

Set up the service

Start the UDF server

Create a TypeSafe API key and store it in an environment variable:

export TYPESAFE_API_KEY='<your-api-key>'

Do not put the key in source code, SQL, or a repository.

Clone

databend-udf
, enter the Python directory, and start the example server:

cd python
uv run python example/jev.py

By default, it listens on these addresses:

UDF Server: 0.0.0.0:8815
Metrics: 0.0.0.0:8816

Allow Databend to reach the server

Databend Query nodes do not connect to arbitrary UDF servers by default. Enable the feature and add the address to the allowlist:

[query]
enable_udf_server = true
udf_server_allow_list = ["http://127.0.0.1:8815"]
udf_server_allow_insecure = true

Restart the Query node after changing the configuration.

This configuration is suitable for local development. In production, use an internal hostname with TLS and keep the allowlist narrow. If Databend runs in a container,

127.0.0.1
refers to the container itself, so replace it with an address the Query node can actually reach.

Register the functions

CREATE OR REPLACE FUNCTION jev(VARIANT, VARCHAR)
RETURNS BOOLEAN
LANGUAGE python
HANDLER = 'jev'
ADDRESS = 'http://127.0.0.1:8815';

CREATE OR REPLACE FUNCTION jev_prob(VARIANT, VARCHAR)
RETURNS DOUBLE
LANGUAGE python
HANDLER = 'jev_prob'
ADDRESS = 'http://127.0.0.1:8815';

CREATE OR REPLACE FUNCTION jev_choice(
VARIANT,
VARCHAR,
ARRAY(VARCHAR NOT NULL)
)
RETURNS VARCHAR
LANGUAGE python
HANDLER = 'jev_choice'
ADDRESS = 'http://127.0.0.1:8815';

CREATE OR REPLACE FUNCTION jev_score(
VARIANT,
VARCHAR,
ARRAY(VARCHAR NOT NULL)
)
RETURNS DOUBLE
LANGUAGE python
HANDLER = 'jev_score'
ADDRESS = 'http://127.0.0.1:8815';

CREATE OR REPLACE FUNCTION jev_eval(
VARIANT,
VARCHAR,
VARCHAR,
ARRAY(VARCHAR NOT NULL)
)
RETURNS VARIANT
LANGUAGE python
HANDLER = 'jev_eval'
ADDRESS = 'http://127.0.0.1:8815';

Run

SHOW USER FUNCTIONS;
to confirm that all five functions are registered.

Figure 2. Databend performs deterministic filtering. The UDF server batches remote semantic decisions and sends them to Jev.

Try it on customer feedback

Create a small table:

CREATE OR REPLACE TABLE customer_feedback (
id UINT64,
customer VARCHAR,
product VARCHAR,
rating UINT8,
feedback VARCHAR
);

INSERT INTO customer_feedback VALUES
(1, 'Alice', 'Cloud Warehouse', 5,
'The query engine is fast and setup was easy.'),
(2, 'Bob', 'Cloud Warehouse', 2,
'Queries are useful, but the dashboard often times out.'),
(3, 'Carol', 'Data Pipeline', 4,
'Reliable ingestion. I would like better monitoring.'),
(4, 'Dave', 'Data Pipeline', 1,
'The connector stopped working and we cannot load data.');

Each function accepts a

VARIANT
as its first argument. Use
OBJECT_CONSTRUCT
to assemble the fields the model should see:

OBJECT_CONSTRUCT(
'product', product,
'rating', rating,
'feedback', feedback
)

I would avoid passing only the feedback string. Including the product name and rating gives the model the context a person would naturally use when reading the record, and I found that the decisions improved with it.

Filter with
jev

Suppose the support team needs feedback that describes a serious failure requiring immediate attention. A keyword filter might begin like this:

WHERE feedback ILIKE '%error%'
OR feedback ILIKE '%failed%'
OR feedback ILIKE '%stopped%'

This list starts small and then grows forever. As soon as users write "not working," "stopped working," or another equivalent phrase, somebody has to update the rule. With

jev
, I can describe the intended meaning directly:

SELECT id, customer, product, rating, feedback
FROM customer_feedback
WHERE jev(
OBJECT_CONSTRUCT(
'product', product,
'rating', rating,
'feedback', feedback
),
'The customer reports a serious product failure that requires immediate support'
);

On this sample, Dave's connector failure is selected while Bob's dashboard timeout is not. That distinction is worth inspecting with real data, but it makes the example concrete: the condition is about a serious product failure, not every negative comment.

Because

jev
returns
BOOLEAN
, it can appear in
WHERE
,
CASE WHEN
, or an aggregate expression. The default threshold is 0.5. Raise it when false positives cost more than missed records:

export JEV_THRESHOLD='0.8'
uv run python example/jev.py

Rank with
jev_prob

A Boolean result works for filtering, but many workflows need a value they can rank. The following query estimates churn risk:

SELECT
id,
customer,
rating,
feedback,
jev_prob(
OBJECT_CONSTRUCT(
'product', product,
'rating', rating,
'feedback', feedback
),
'The customer is at high risk of churn'
) AS churn_probability
FROM customer_feedback
ORDER BY churn_probability DESC;

Once the probability is available in SQL, the policy no longer has to live in the model wrapper. An operations team can require at least 0.8:

WITH scored AS (
SELECT
*,
jev_prob(
OBJECT_CONSTRUCT(
'product', product,
'rating', rating,
'feedback', feedback
),
'The customer is at high risk of churn'
) AS churn_probability
FROM customer_feedback
)
SELECT *
FROM scored
WHERE churn_probability >= 0.8
ORDER BY churn_probability DESC;

A sales team might prefer the top ten results. Both can work from the same UDF without changing the model integration.

Classify with
jev_choice

Customer feedback often needs to be routed to the right team:

SELECT
id,
feedback,
jev_choice(
OBJECT_CONSTRUCT(
'product', product,
'rating', rating,
'feedback', feedback
),
'Which category best describes this customer feedback?',
[
'Performance',
'Reliability',
'Usability',
'Feature request'
]
) AS category
FROM customer_feedback;

I would use the same pattern for ticket routing, review topics, product categorization, or lead segmentation. One lesson from testing it is to keep the candidate categories distinct. When two options mean nearly the same thing, the probability spreads between them and confidence falls.

Score with
jev_score

Some judgments have degrees rather than a binary answer. Sentiment is one example:

SELECT
id,
feedback,
jev_score(
OBJECT_CONSTRUCT(
'rating', rating,
'feedback', feedback
),
'How positive is this customer feedback?',
[
'Very negative',
'Negative',
'Neutral',
'Positive',
'Very positive'
]
) AS sentiment_score
FROM customer_feedback
ORDER BY sentiment_score;

The five levels occupy positions 0 through 4. The function returns a weighted position based on the probability of each level, so values such as 2.5 and 3.7 are possible.

The result is numeric and can be aggregated directly:

SELECT
product,
AVG(
jev_score(
OBJECT_CONSTRUCT('rating', rating, 'feedback', feedback),
'How positive is this customer feedback?',
['Very negative', 'Negative', 'Neutral', 'Positive', 'Very positive']
)
) AS avg_sentiment
FROM customer_feedback
GROUP BY product
ORDER BY avg_sentiment DESC;

Jev supplies the fuzzy judgment, while Databend does the grouping and averaging it was built for. This query was the point where the integration started to feel genuinely useful to me: the semantic result behaves like any other numeric column.

Inspect the full result with
jev_eval

A category may be enough for an automated path. Auditing, risk controls, and human review often need the confidence and full probability distribution:

SELECT jev_eval(
PARSE_JSON('{"text":"A SQL query engine written in Rust"}'),
'Which category best describes this text?',
'choice',
['Database', 'Cooking', 'Sports']
) AS result;

The response is:

{
"choice": "Database",
"confidence": 1.0,
"probabilities": {
"Cooking": 0.0,
"Database": 1.0,
"Sports": 0.0
},
"type": "choice"
}

Databend stores this as

VARIANT
, so the query can extract individual fields:

WITH evaluated AS (
SELECT
id,
jev_eval(
OBJECT_CONSTRUCT('feedback', feedback),
'Which category best describes this feedback?',
'choice',
['Performance', 'Reliability', 'Usability', 'Feature request']
) AS result
FROM customer_feedback
)
SELECT
id,
result['choice']::VARCHAR AS category,
result['confidence']::DOUBLE AS confidence,
result['probabilities'] AS probabilities
FROM evaluated;

The pattern I use most often is to send high-confidence results downstream, put low-confidence results in a review queue, and retain the full probability distribution so I can inspect the decisions later.

The third argument to

jev_eval
specifies the question type:

kind
Meaning
options
noul
Probability of a Boolean statementPass
[]
choice
Classification among candidatesPass the candidate categories
score
Position on an ordered scalePass the ordered levels

Persist results that will be reused

This is the first operational trap to keep in mind: these functions call a remote API. They may look like

LOWER()
or
SUBSTR()
in a query, but they do not have the same cost. Recomputing an unchanged result every time a dashboard refreshes adds both latency and API spend.

If dashboards, applications, or downstream jobs will read the result repeatedly, compute it once and write it to a table:

CREATE OR REPLACE TABLE feedback_enriched AS
SELECT
id,
product,
rating,
feedback,
jev_eval(
OBJECT_CONSTRUCT(
'product', product,
'rating', rating,
'feedback', feedback
),
'Which category best describes this customer feedback?',
'choice',
['Performance', 'Reliability', 'Usability', 'Feature request']
) AS category_result,
NOW() AS processed_at
FROM customer_feedback;

Subsequent queries can read the stored result:

SELECT
category_result['choice']::VARCHAR AS category,
COUNT(*) AS feedback_count
FROM feedback_enriched
GROUP BY category
ORDER BY feedback_count DESC;

Figure 3. The pipeline evaluates customer feedback once and persists the result. High-confidence records proceed automatically; low-confidence records enter a human review queue.

Production considerations

Reduce the input set with SQL first

Apply deterministic filters for date, status, partition, and numeric conditions before calling Jev:

SELECT
id,
jev_prob(
OBJECT_CONSTRUCT('rating', rating, 'feedback', feedback),
'The customer needs immediate support'
) AS probability
FROM customer_feedback
WHERE rating <= 2
AND feedback IS NOT NULL;

I would not point a remote model at an unfiltered large table. Let SQL remove everything it can first.

Send only the fields required for the decision

I construct a minimal object instead of passing an entire row. This keeps the request smaller and reduces the amount of sensitive data leaving the database.

Keep questions consistent within a batch

The UDF implementation batches rows that share the same question type, wording, and options. In practice, keeping those values fixed across a batch gives the server much more room to improve throughput.

Batch size and concurrency are configurable:

export JEV_BATCH_SIZE='20'
export JEV_CONCURRENCY='6'

The server also implements timeouts, retries, and an in-process LRU cache to absorb transient network failures and repeated requests.

Define the data boundary before deployment

Every field used in a decision is sent to a remote API. Before I would ship this to production, I would want clear answers to these questions:

  • Which fields may leave the database?

  • Do they contain personal or other sensitive information?

  • How will the API key be injected and rotated?

  • Should traffic between Databend and the UDF server use TLS?

  • Do the allowlist and network egress rules permit only trusted destinations?

  • Must results be retained for auditing?

Putting an AI call behind a SQL function does not relax data governance. The call now sits directly in the data path, so the boundary needs to be more explicit.

Databend executes; Jev decides

After building the integration, the division of work felt natural. Databend stores, filters, joins, aggregates, and executes at scale. Jev answers the semantic questions I cannot express cleanly as rules. Natural-language filters, probabilities, categories, ordered scores, and confidence-based review all remain inside the SQL workflow.

That means a data team can keep working with familiar pieces. The input is still a table, the call is still a SQL expression, and the result can feed

WHERE
,
ORDER BY
,
GROUP BY
,
INSERT INTO
, or another pipeline.

I am still cautious about Jev. The product is early, its evaluations were designed by its own team, and the weights are not public. Those claims need independent scrutiny. Still, the integration convinced me of one narrower point: the small, ambiguous decisions inside a database do not always need a model built to write an essay. Sometimes SQL just needs a bounded answer and a probability it can act on.

Resources:

Function signatures:

jev(row, condition) -- Natural-language condition; returns BOOLEAN
jev_prob(row, condition) -- Probability that the condition is true; returns DOUBLE
jev_choice(row, question, options) -- Candidate classification; returns VARCHAR
jev_score(row, question, levels) -- Position on an ordered scale; returns DOUBLE
jev_eval(row, question, kind, options) -- Complete result; returns VARIANT
Share this post

Subscribe to our newsletter

Stay informed on feature releases, product roadmap, support, and cloud offerings!