Hacker News gives out its 500 newest submissions from one API endpoint. Most of them are not for me. Which ones are depends entirely on who I am, and I can’t write that down as a LIKE pattern.
So I wrote it down as a sentence, and put the sentence in the query.
The whole program
ATTACH 'typesafe' (TYPE vgi, LOCATION 'uvx --from git+https://github.com/Query-farm/vgi-typesafe vgi-typesafe');
ATTACH 'hackernews' (TYPE vgi, LOCATION 'uvx --from git+https://github.com/Query-farm/vgi-hackernews vgi-hackernews');
CREATE SECRET (TYPE typesafe, api_key 'ts-...');
SELECT title, url, interesting.noulFROM (SELECT title, url FROM hackernews.new_stories LIMIT 500) hn_stories, typesafe.ask(hn_stories, questions => { 'interesting': { 'type': 'noul', 'instructions': 'Is this story interesting to someone who in data and databases (i.e. DuckDB) but also appreciates distributed systems, python, apache arrow' } } )WHERE interesting.noul > 0.50ORDER BY interesting.noul DESCLIMIT 20;That’s it. Two ATTACH statements, a secret, one query.
I’d read most of that. And look at the instructions again — there’s a verb missing in the first line, a stray “i.e.”, and no capital letters worth speaking of. I typed it once and never went back. Prompt engineering did not enter into it.
There is no install step
LOCATION is a command, not a URL. DuckDB runs it. uv fetches the worker from GitHub, builds it into an environment, and starts talking VGI to it over stdio. Second run, it’s already built.
ATTACH statements build all of this. The workers are subprocesses, not services.This is why a two-worker query is worth typing at all. No service to stand up, no container, nothing left running when the query ends — vgi-hackernews and vgi-typesafe are subprocesses that speak Arrow, and they exit with the query.
Let’s be honest about “nothing installed”, though, because it isn’t quite true. You never run an install command, but uv keeps everything it builds — uv cache dir will tell you where, and if you have been using uv for a while that directory is not nothing. What you get for it is a warm start of under a second. uv cache clean is the uninstaller.
I ran this in haybarn-cli (uvx haybarn-cli), which autoloads the vgi extension when you ATTACH. In the DuckDB CLI, install it yourself first:
INSTALL vgi FROM community;LOAD vgi;Hang on, what’s Jev?
Fair question, since it shipped about a week ago. Jev is TypeSafe AI’s first System One model, and it is not a chatbot. It doesn’t write. It can’t write. There is no prose in it anywhere.
Their framing is the Kahneman one. System Two is the deliberate, slow, show-your-working reasoning that frontier LLMs do so well. System One is the fast judgement you make before you’ve finished reading the sentence. Jev only does the second kind, and gives up text generation entirely to do it. TypeSafe’s own summary: “Think of Jev as a frontier-intelligence function call: unstructured state in, typed probabilistic decisions out.”
You hand it some state and a typed question. It hands back a typed answer with a probability attached. Nothing in between.
interesting.noul is already a DOUBLE.The type safety isn’t a validation layer bolted on afterwards — every possible answer is enumerated before the model runs, because you supplied the list. There’s no room in the output space for a value outside the declared type. Nothing to strip, nothing to parse, nothing to retry when it returns ```json with an apology attached.
It comes in three flavours, and you can mix them in one call:
- choice — pick one of these options. Returns the pick, a confidence, and the full probability distribution over your options.
- noul — is this claim true? Returns a number from 0 to 1.
- score — put this on the rubric I described. Returns a position on your scale.
The name is a joke about William Stanley Jevons, the Victorian economist who worked out in The Coal Question that making steam engines more efficient made Britain burn more coal, not less. Cheaper fuel, more uses for it. That’s the Jevons paradox, and TypeSafe’s version is: “Every order of magnitude drop in the cost of intelligence unlocks orders of magnitude more use cases.” We’ll come back to it, because I proved it on myself before the day was out.
It’s fast enough that the demo is playing Doom at ten decisions a second. I’m using it to sort a reading list, which is a bit like buying a Formula One car to get to the shops.
noul gives you a probability, not a verdict
This is the part that matters for SQL.
A classifier that answers true or false has already made your decision for you, at a threshold some stranger picked. noul hands back a number and stays out of it. Where you cut is your business — which means it’s an ordinary comparison operator, and it can go in ORDER BY too.
Here’s how the 500 stories spread out:
noul > 0.50 keeps 134 stories — about a quarter of the feed. Twelve land on exactly 0.50 and get cut by the strict inequality.Nothing scored above 0.88 and nothing below 0.16, which is what calibration looks like when the question is “would this person enjoy this” rather than “is this a cat”. The bottom of the list is the best part:
| title | noul |
|---|---|
| Hjfjhg | 0.16 |
| ICE-style immigration raids won't be used in Australia, minister says | 0.17 |
| Free Unlimited AI Text to Speech – No Sign Up | 0.18 |
| iPhone 18 Pro Camera Review: Dunton, Colorado | 0.19 |
| Extreme Alarm Clocks (2025) | 0.20 |
| PPPlayer – An open-source music player built with Flutter | 0.20 |
| She's Leaving Me Because I Never Called Her Beautiful | 0.20 |
| Nestbalm | 0.21 |
Somebody submitted a story called Hjfjhg. It scored 0.16 — not zero. Even keyboard mash gets a little benefit of the doubt, which is either very generous or very Bayesian. Meanwhile “She’s Leaving Me Because I Never Called Her Beautiful” ranks below a Flutter music player, and I can’t argue.
The interesting zone is the middle, where twelve stories landed on exactly 0.50. That’s the model shrugging:
| title | noul |
|---|---|
| Auditing in the age of (good enough) AI | 0.54 |
| Fingerprinting Network Honeypots with Weighted Behavioral Scoring Engine | 0.54 |
| Gyazo screen capture tool confirms data breach | 0.54 |
| Eight language models and the 2026 Berlin state election | 0.54 |
| Ask HN: Which course or book changed you as a programmer and software engineer? | 0.54 |
Those really could go either way for me, and a boolean would have had to pick one and pretend. Here the uncertainty survives into the result set, and moving 0.50 to 0.65 is a product decision I get to make later.
The state is the whole row
Look at the call again:
typesafe.ask(hn_stories, questions => { ... })hn_stories is the subquery’s alias. Not a column — the whole row. Jev gets {"title": "...", "url": "..."} with the field names intact, so arxiv.org and lwn.net are part of what it’s judging. No string concatenation, no 'Title: ' || title || ' URL: ' || url.
ask() takes state as an ANY argument and converts by Arrow type: VARCHAR goes as text, a STRUCT or a whole row goes as JSON. Want one column instead? typesafe.ask(hn_stories.title, ...).
That’s a LATERAL join in the comma spelling — DuckDB spots the correlation, so the keyword is optional. The function is registered as a blended table-in-out function, which means the same call works on a literal, on a column, and inside an explicit LATERAL, with no separate syntax for each. VGI hands the worker batches of left-hand rows rather than one at a time, which is how 500 correlated calls overlap instead of queueing; the architecture page has the details.
Look before you spend
Your result columns are named after your own questions, so the schema depends on what you asked. You can see it for free, because binding validates the questions and works out the schema without sending anything:
DESCRIBE SELECT * FROM typesafe.ask('x', questions => {'interesting': {'type': 'noul', 'instructions': 'Is this interesting?'}});| column_name | column_type |
|---|---|
| interesting | STRUCT(noul DOUBLE) |
| usage | STRUCT(model VARCHAR, input_tokens BIGINT, output_tokens BIGINT) |
What it cost
17.8 seconds end to end. Fetching the stories from Hacker News is 1.8 of those, so about 16 seconds is Jev.
TypeSafe has no batch endpoint — one request carries one state — so that’s 500 HTTPS requests, eight at a time (concurrency defaults to 8, goes to 64). ask() also collapses duplicate (state, questions) pairs into one request, which saved me exactly nothing here: all 500 title-and-url pairs were distinct.
The usage column says 173,148 input tokens and 10,000 output tokens against jev-1.13.0. TypeSafe charges $0.042 per million input tokens and nothing at all for output, so scoring 500 stories cost $0.0073.
Three quarters of a cent. I spent longer deciding whether to run it than it cost to run. The 10,000 output tokens — exactly 20 per story, which is what it takes to say one number — were free.
Also — and Jevons is somewhere laughing — I ran the whole thing twice, because the first set of numbers was fine but I wanted a snapshot I could query repeatedly for this post. Making the question cheap didn’t make me ask it less. It made me ask it a thousand times. The model is named after the man who predicted exactly that, which I’d call a warning if it weren’t so obviously a sales pitch.
Five questions cost the same as one
What Jev does batch is questions. Five questions about one story is one request, not five, because they’re all answered in the same parallel pass. That’s why ask() takes a map:
SELECT title, urgent.noul, team.choice, depth.scoreFROM (SELECT title, url FROM hackernews.new_stories LIMIT 100) hn_stories, typesafe.ask(hn_stories, questions => { 'urgent': {'type': 'noul', 'instructions': 'Is this breaking news rather than evergreen writing?'}, 'team': {'type': 'choice', 'instructions': 'Which area does this belong to?', 'criteria': MAP {'data': 'Databases, analytics, storage formats', 'infra': 'Distributed systems, networking, operations', 'other': 'Everything else'}}, 'depth': {'type': 'score', 'instructions': 'How technically deep is this?', 'criteria': ['a headline', 'an overview', 'an engineering deep dive']}});| title | urgent | team | depth |
|---|---|---|---|
| Warren Buffett steps down as Berkshire Hathaway chairman | 0.85 | other | 0.00 |
| Startup Fluxnium found a way to tap 50k years' worth of nuclear fuel | 0.73 | other | 0.02 |
| Prediction markets are becoming a national security threat | 0.46 | other | 0.42 |
| Reconstructed Jurassic insect calls (165M-year-old) [video] | 0.31 | other | 0.55 |
| Think Like an Attacker: CI/CD Security in the AI Era | 0.13 | infra | 0.90 |
| You can create any workflow or pipeline you want with DSCI and LLM | 0.12 | infra | 0.43 |
Buffett resigning: maximally breaking, zero engineering depth. The CI/CD security piece: not news at all, deepest thing in the batch. Both correct, both from the same request, three typed columns each.
Take it somewhere else
The pattern isn’t about Hacker News. Any table with text in it — support tickets, commit messages, reviews, RFP responses — can be scored against a sentence and filtered with >.
A few things worth stealing:
- The score is just data. Join it, average it, sort by it.
avg(interesting.noul) GROUP BY domaintells you which sites reliably publish things you like, which is a more useful answer than any single story. - Pick your own threshold. 0.8 if a machine acts on it unsupervised. 0.5 for a reading list. 0.3 for a triage queue a human still reads.
DESCRIBEis free. Check the shape before you run 500 rows.- For one yes/no, skip the join.
typesafe.is_true(body, 'Is this an angry complaint?') > 0.5is a scalar and goes straight intoWHERE.
Both workers are MIT-licensed and one ATTACH away: vgi-typesafe and vgi-hackernews. TypeSafe’s docs cover the three primitives properly, and VGI is what gets any of it into DuckDB in the first place.
Every number here comes from one run against the live APIs on 18 September 2026. Hacker News moves. Your Hjfjhg will differ.