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:
-
returns the probability that a statement is true, as a number from 0 to 1.Noul
-
selects one item from a supplied set and returns the probability of every option.Choice
-
evaluates an item against an ordered set of levels and returns a probability-weighted position.Score
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:
| Function | Return type | Purpose |
|---|---|---|
| | Test whether a record satisfies a condition written in natural language |
| | Return the probability that a condition is true |
| | Select the best matching category from a list |
| | Return a position on an ordered scale |
| | Return the full result, including probabilities and confidence |
The first four return scalar values that fit naturally into SQL expressions.
jev_eval
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
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
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;

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
OBJECT_CONSTRUCT
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
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
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
BOOLEAN
WHERE
CASE WHEN
export JEV_THRESHOLD='0.8'
uv run python example/jev.py
Rank with jev_prob
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
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
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
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
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
| Meaning | |
|---|---|---|
| Probability of a Boolean statement | Pass |
| Classification among candidates | Pass the candidate categories |
| Position on an ordered scale | Pass 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()
SUBSTR()
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
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:
Databend UDF: github.com/databendlabs/databend-udf
Function signatures:
jev(row, condition) -- Natural-language condition; returns BOOLEANjev_prob(row, condition) -- Probability that the condition is true; returns DOUBLEjev_choice(row, question, options) -- Candidate classification; returns VARCHARjev_score(row, question, levels) -- Position on an ordered scale; returns DOUBLEjev_eval(row, question, kind, options) -- Complete result; returns VARIANT
Subscribe to our newsletter
Stay informed on feature releases, product roadmap, support, and cloud offerings!



