Wvlet for AI Agents
Wvlet is a flow-style query language that compiles to SQL for DuckDB, Trino, Snowflake, and Hive. A query starts with from and lists one operator per line in the order the data is processed. That makes Wvlet the query language agents edit well: adding a filter, a column, or an aggregation is a one-line append, not a rewrite of nested SQL.
This page is written for AI coding agents and the people who prompt them. The whole documentation is also available as plain text at llms.txt (index) and llms-full.txt (all pages). Every user-facing doc page has a Markdown copy at the same URL with a .md suffix.
When should I choose Wvlet?
Choose Wvlet when:
- You build a query step by step, or keep changing one: each step is a line you can add, remove, or reorder without touching the rest.
- You need the same logic on more than one engine. Wvlet generates engine-specific SQL for DuckDB, Trino, Snowflake, and Hive.
- You want reusable, testable query modules:
modeldefinitions,valconstants, andtestassertions live next to the query in.wvfiles. - You explore local files (Parquet, CSV, JSON) with DuckDB, which is built in.
Plain SQL is fine when the project already standardizes on SQL files, a tool only accepts SQL, or the query is a single short SELECT. Wvlet can still help there: wvlet compile turns a .wv file into SQL you can hand to any tool.
Why do flow-style queries suit agents?
- Line-local edits. Each line is one operator (
where,add,group by,agg,order by, ...). A change touches the lines it is about, which keeps diffs small and reviews easy. - Append-only growth. The output of each line feeds the next one, so you can extend a working query by appending a line and check the result after every step.
- Reading order is execution order.
fromcomes first andselectcomes last (or not at all), so there is no need to jump betweenSELECT,FROM,GROUP BY, andHAVINGto understand a query.
from orders
where status = 'paid'
group by customer_id
agg amount.sum as total
where total > 100 -- filter groups (SQL HAVING)
order by total desc
limit 10
Syntax cheat sheet
Keywords are lower-case. Each operator takes the output of the previous line as its input.
| Operator | What it does | Example |
|---|---|---|
from | Read a table, a file, or inline values | from orders, from 'data.parquet' |
where | Filter rows; repeat it as often as needed | where status = 'paid' |
select | Choose output columns (optional) | select id, amount |
select distinct | Unique rows of the selected columns | select distinct status |
add | Append a computed column | add amount * 1.1 as with_tax |
exclude | Drop columns | exclude status, amount |
rename | Rename a column | rename amount as price |
shift | Move columns to the left | shift amount |
group by | Group rows by keys | group by customer_id |
agg | Add aggregations to the group keys | agg _.count as n, amount.sum as total |
where after agg | Filter groups | where total > 100 |
order by | Sort rows | order by total desc |
limit | Keep the first n rows | limit 10 |
count | Count all rows | from orders count |
dedup | Remove duplicate rows | select status dedup |
sample | Random sample | sample 1000 |
join / left join | Join another relation | join customers on orders.customer_id = customers.id |
concat | Union all rows of another query | concat { from archived_orders } |
with | Name a subquery | with paid as { from orders where status = 'paid' } |
model | Define a reusable query | model paid_orders = { from orders where status = 'paid' } |
val | Define a constant or an inline table | val t(id, name) = [[1, 'a'], [2, 'b']] |
test | Assert on the result | test _.size should be 10 |
Expressions:
- Strings use single or double quotes:
'paid'and"paid"are both string literals. Quote identifiers with back quotes:`order id`. - Comparison:
=,!=,<,<=,>,>=,is null,is not null,in (...),between ... and ...,like. Logic:and,or,not. - Aggregations:
_.countcounts rows in a group;column.sum,.avg,.min,.max,.count_distinctaggregate a column. SQL functions such ascount(*)andsum(x)also work. - Casts:
amount::int,'2026-01-01'::date. - Conditionals:
if x > 0 then 'pos' else 'neg', orcase when x > 0 then 'pos' else 'neg'(noend). - String interpolation:
s"order-${id}". - Window functions:
row_number() over (partition by customer_id order by amount desc). - Comments:
-- line comment.---starts a Markdown doc comment block.
See the Query Syntax reference for every operator.
SQL to Wvlet mapping
| SQL | Wvlet |
|---|---|
SELECT * FROM t | from t |
SELECT a, b FROM t WHERE c > 0 | from t where c > 0 select a, b |
SELECT *, x * 2 AS y FROM t | from t add x * 2 as y |
SELECT DISTINCT a FROM t | from t select distinct a |
SELECT COUNT(*) FROM t | from t count |
SELECT k, COUNT(*) AS n FROM t GROUP BY k | from t group by k agg _.count as n |
... GROUP BY k HAVING SUM(v) > 10 | group by k agg v.sum as s where s > 10 |
SELECT COUNT(DISTINCT u) FROM t | from t agg u.count_distinct as users |
... ORDER BY n DESC LIMIT 10 | order by n desc limit 10 |
FROM a JOIN b ON a.id = b.a_id | from a join b on a.id = b.a_id |
FROM a LEFT JOIN b ON ... | from a left join b on ... |
q1 UNION ALL q2 | q1 concat { q2 } |
WITH x AS (q) SELECT ... FROM x | with x as { q } from x ... |
CAST(x AS INT) | x::int |
CASE WHEN c THEN a ELSE b END | if c then a else b |
VALUES (1, 'a'), (2, 'b') | val t(id, name) = [[1, 'a'], [2, 'b']] then from t |
To convert an existing SQL query, run wvlet to_wvlet "<sql>". For example, wvlet to_wvlet "select status, count(*) as n from orders group by status having count(*) > 1" prints:
from orders
group by status
where count(*) > 1
select status, count(*) as n
How do I run Wvlet?
Install the CLI with Homebrew (brew install wvlet/wvlet/wvlet) or download a release package; see Installation. The CLI has two commands:
wv: an interactive shell (REPL).wv -c "from 'data.csv' count"runs one query and exits.wvlet: batch commands.wvlet run "from 'data.parquet' limit 10"runs a query;wvlet run -f query.wvruns a file.wvlet compile -f query.wvprints the generated SQL; add--target trino(or another engine) to choose the SQL dialect.wvlet to_wvlet "<sql>"converts SQL to Wvlet.
Engines:
- DuckDB is the default and needs no setup. File paths in
from '...'are read by DuckDB. See DuckDB. - Trino, Snowflake, and others are configured as profiles in
~/.wvlet/profiles.jsonand selected with--profile <name>. See Trino and Connectors. - To check a query without a database, run
wvlet compile: syntax errors are reported with the line and column.
Common mistakes agents make
| Mistake | Fix |
|---|---|
Starting with select ... from t | Start with from t; put select at the end, or omit it. |
== or <> for comparison | Use = and !=. |
HAVING after GROUP BY | Use where after agg. |
UNION / UNION ALL | Use concat { ... }. Wvlet has no duplicate-eliminating union; add dedup if needed. |
CASE ... END | case has no end: case when c then a else b as col. Or use if c then a else b. |
// comment | // is integer division. Use -- comment. |
| Double quotes for column names, as in SQL | "x" is a string literal. Use back quotes: `x`. |
_.count_distinct(col), _.max_by(a, b) | Call the function on the column: col.count_distinct, a.max_by(b) (or SQL count(distinct col), max_by(a, b)). |
Upper-case keywords (FROM, WHERE) | Keywords are lower-case: from, where. |
| Rewriting the whole query to add a step | Append a line: another where, add, or agg after the existing ones. |
How do I keep queries correct?
- Put
testassertions after a query in a.wvfile, for exampletest _.size should be 10ortest _.columns should contain 'user_id'. See Test Syntax. - Compile before running against a remote engine:
wvlet compile -f query.wv --target trino. - Build long queries incrementally: run the first lines, check the output, then append the next operator.