Loading lessons…
Vow is designed around the loop every coding agent runs: write, run, read the error, fix. Every error says what is wrong, where, and the likely fix. There are no nulls and no exceptions, and every function declares whether it can touch the network, files or environment.
fn parsePrice(text) {
let n = num(text)?
if n < 0 { return err("a price can't be negative") }
return ok(n)
}
for raw in ["4.50", "abc", "-2"] {
match parsePrice(raw) {
ok(p) => print("${raw}: ${fixed(p, 2)}")
err(e) => print("${raw}: ${e}")
}
}
4.50: 4.50 abc: "abc" is not a number -2: a price can't be negative
--sandbox refuses a program that reaches beyond what you allow.Vow is meant to be written by AI coding tools. Give your tool the guide once and it can write, check and fix Vow on its own.
npm install -g github:JakeBurwick/vow
Paste this into Claude Code, Codex, Cursor or any coding agent.
This project uses Vow, a programming language for AI agents. Before writing any Vow, read https://vow-lang.vercel.app/llms.txt and the guide it links to. Run programs with `vow file.vow`. After every edit run `vow --check file.vow` and fix each error using its "Likely fix" line. Before finishing, add test blocks for each example and error case and run `vow test file.vow`. Vow has no null and no exceptions: handle every Result with match or ?.
Drop this AGENTS.md into your project root and your agent picks it up automatically. It holds the same instructions plus the five rules agents miss most.
Download AGENTS.md Open in a new tab
Loading AGENTS.md. If this does not change, open it with the link above.
For Claude Code. This gives the agent vow_guide, vow_check and vow_run tools.
claude mcp add vow -- vow-mcp
Tested in October 2026 with Claude Haiku 4.5, Anthropic's smallest current model at the time. Larger models should do at least as well. Comparable small models from other labs should behave similarly, since everything Vow needs is in its guide, but they have not been measured. A very small local model (Llama 3.2 3B) could not solve these tasks in Vow or in TypeScript.
✗ TypeError: can't add a num and a str
→ line 6, col 10
6 | return "Total: " + sum
^^^^^^^^^^^^^^^
'"Total: "' is a str and 'sum' is a num. Vow never converts types implicitly.
Likely fix: convert the number to text first: str(sum)
$ vow --sandbox --allow fs=./data app.vow ✗ SandboxError: this program needs capabilities the sandbox does not grant env: calls fn leak, which declares 'uses env' (line 2) Nothing was run. Likely fix: remove that access, or run with --allow env
Built and measured by Jake Burwick. On three brand-new hard tasks, Vow had one failed submission in nine runs and TypeScript had zero with every run solved, showing the two perform evenly on a small sample, and code that agents finish passes fresh hidden tests at about the same rate with 99.5% for Vow and 99.3% for TypeScript. Measured October 2026; numbers may have changed since. See the benchmarks.
You need Node.js 20 or newer. Vow has no other dependencies.
npm install -g github:JakeBurwick/vow
echo 'print("Hello, Vow!")' > hello.vow
vow hello.vow
To hack on Vow itself, clone the repository and run the CLI from the clone:
git clone https://github.com/JakeBurwick/vow.git
cd vow
echo 'print("Hello, Vow!")' > hello.vow
node cli.js hello.vow
Loading lessons…
This page shows five bugs. Each example includes the same program written in Vow, TypeScript, and Python, and the exact output from bad input. Click a button above the code to pick a bug. Some bugs produce an explicit error in one language and a quiet wrong answer in another.
Every output was captured by running the program, using a script in the repository at site/errors/gen.mjs, and is shown unedited except that file paths are shortened to /work. TypeScript was type-checked with deno check before running, with Deno's default settings. With the TypeScript setting noUncheckedIndexedAccess turned on, the type checker also flags the missing-key case before it runs. Measured on .
A reference for the current language. The complete rules live in the agent guide in the repository (bench/VOW_GUIDE.md); every example below was run with the vow command. To have an AI coding tool write Vow for you, see Use Vow with your AI.
Types are inferred everywhere, so you never write one. Newlines end statements. Booleans use and, or, not.
let count = 3 // num
let name = "Ada" // str
let on = true // bool
let xs = [1, 2, 3] // list of num
let m = {"a": 1} // map: str keys, one value type
count = count + 1 // fine: same type
count = "four" // ✗ a variable never changes type
let name = "Ada"
print("hi ${name}!") // hi Ada!
print("2 + 2 = ${2 + 2}") // any expression
print("{ plain braces }") // plain braces are text
print("{\"name\": \"${name}\"}") // so JSON needs no escaping
print("\${not inserted}") // \$ writes a literal $
print(`raw: C:\path ${x}`) // backticks: fully literal
print(name[0]) // "A"
fn area(w, h) { return w * h }
// no return: the last line is the value
fn greet(name) {
"Hello, ${name}"
}
// generic automatically
fn first(xs) { return xs[0] }
print(first([1, 2])) // 1
print(first(["a", "b"])) // a
let double = x => x * 2
let add = (a, b) => a + b
let fact = n => { // block body:
let out = 1 // last line is the value
for i in range(1, n + 1) { out = out * i }
out
}
let base = 10
let bump = x => x + base // closures capture
let x = 4
if x > 0 {
print("pos")
} else if x < 0 {
print("neg")
} else {
print("zero")
}
for item in [1, 2, 3] { print(item) }
for i in range(0, 5) { print(i) } // 0 to 4
let lo = 0
while lo < 10 {
lo = lo + 3
if lo == 6 { continue }
if lo > 8 { break }
}
// if/else as a value (else required)
let size = if x > 5 { "big" } else { "small" }
// no null, no exceptions: failure is a value
fn safeDiv(a, b) {
if b == 0 { return err("divide by zero") }
return ok(a / b)
}
match safeDiv(10, 2) {
ok(v) => print(v)
err(e) => print("failed: ${e}")
}
// match is an expression; a block arm's
// last line is its value
let v = match num("42") {
ok(n) => {
let d = n * 2
d
}
err(e) => 0
}
// ? unwraps ok, or returns the err to the
// caller (inside a fn only)
fn areaOf(w, h) {
let a = num(w)?
let b = num(h)?
return ok(a * b)
}
fn label(qty) {
match qty {
1 => "single"
2 => "pair"
_ => "bulk" // _ catches the rest, comes last
}
}
print(label(2)) // pair
let kind = match "number" {
"string" => "text"
"number" => "num"
_ => "other"
}
struct Point { x, y }
let p = Point { x: 3, y: 4 } // all fields required
print(p.x + p.y) // 7
p.x = 10 // same type for life
print(p == Point { x: 10, y: 4 }) // value equality: true
// a param's struct is inferred from field use
fn manhattan(q) { abs(q.x) + abs(q.y) }
let xs = [3, 1, 2]
let m = {"a": 1, "b": 2}
m["c"] = 3 // insert or update
print(xs[0]) // traps if out of range
match get(xs, 9) { // safe access: Result
ok(v) => print(v)
err(e) => print("none")
}
print(has(m, "a")) // true
push(xs, 4)
xs = sort(xs) // sort returns a new list
print(keys(m)) // insertion order
print(values(m)) // [1, 2, 3]
print(map(xs, x => x * 2))
print(filter(xs, x => x > 1))
print(any(xs, x => x > 3)) // true
print(all(xs, x => x > 0)) // true
print(count(xs, x => x > 1)) // 3
print(fold(xs, 0, (acc, x) => acc + x)) // 10
print(uniq([1, 1, 2])) // [1, 2]
// regex patterns go in backtick strings
print(words(" a b\n")) // ["a", "b"]
print(matches("2026", `^\d+$`)) // true
print(findAll("5 and 12", `\d+`)) // ["5", "12"]
match find("user=ada", `(\w+)=(\w+)`) { // ok([whole, g1, g2])
ok(m) => print(m[2]) // ada
err(e) => print(e) // "no match"
}
print(replaceAll("a b", `\s+`, " ")) // a b
print(splitOn("a, b;c", `\s*[,;]\s*`)) // ["a", "b", "c"]
print(replace("a.b", ".", "-")) // literal: a-b
print(lines("x\ny")) // ["x", "y"]
print(padLeft("7", 3, "0")) // 007
print(trim(" hi "), upper("hi"), lower("HI"))
match int("42") { // strict whole-number parse
ok(n) => print(n)
err(e) => print(e)
}
match num("4.5") { // any number
ok(n) => print(n)
err(e) => print(e)
}
print(isInt(3.5)) // false
print(sig(1234.5, 2)) // 1200 (significant digits)
print(round(15.625, 2)) // 15.63
print(fixed(212.3, 2)) // "212.30" as text
print(floor(2.7), abs(0 - 3), min(1, 2), max(1, 2), sum([1, 2]))
print(sqrt(16), pow(2, 10), exp(1), ln(1)) // log = ln
print(sin(pi() / 2), cos(0), tan(0), atan2(1, 1))
print(ceil(4.2), trunc(0 - 4.7)) // 5 -4
print(random()) // in [0, 1)
print(randomInt(1, 6)) // 1 to 6, both included
// --seed 42 makes random repeatable
// times are UTC milliseconds since 1970
let t = 1791468180000
print(isoDate(t)) // 2026-10-08T14:03:00Z
print(dateParts(t).weekday) // 4 (0 = Sunday)
match parseDate("2026-12-25") {
ok(xmas) => print(daysBetween(t, xmas)) // 78
err(e) => print(e)
}
let text = `{"name": "Ada", "tags": ["x"], "pet": null}`
match parseJson(text) { // Result: bad input is an err
ok(j) => {
print(field(j, "name")) // Ada (json null if missing)
print(at(field(j, "tags"), 0)) // x
print(has(j, "pet"), isNull(field(j, "pet"))) // true true
print(truthy(field(j, "tags"))) // true; [] and {} are falsy
for t in jsonList(field(j, "tags")) { print(t) }
print(jsonKeys(j)) // ["name", "tags", "pet"]
print("${j}") // str(j): compact JSON text
match get(j, "age") { // get: missing is an err
ok(a) => print(a)
err(e) => print("no age")
}
let j2 = jsonSet(j, "age", 36) // a new json; j is unchanged
print(jsonMerge(j, j2) == j2) // deep merge, right side wins; == compares value
}
err(e) => print("bad JSON: ${e}")
}
struct User { name, age }
print(toJson(User { name: "Ada", age: 36 }))
print(prettyJson([1, 2])) // indented with 2 spaces
fn withdraw(balance, amount)
expects amount > 0, amount <= balance
ensures result >= 0
{
return balance - amount
}
print(withdraw(100, 30)) // 70
print(withdraw(100, 500)) // ✗ refuses to compile: proven broken
// values only known at runtime are checked then,
// and the error says whether caller or fn is at fault
fn fetch(url) uses net { // may reach the network
return httpGet(url)
}
fn save(p, s) uses fs { // may touch files
return writeFile(p, s)
}
fn home() uses env { // may read env variables
return env("HOME")
}
fn shout(s) { upper(s) } // provably touches nothing
// callers inherit the obligation; declaring a
// capability you don't use is also an error
// vow --sandbox --allow net=api.example.com,fs=./data app.vow
// refuses the program before it runs if it reaches further
// geometry.vow: only fn, struct and import
struct Point { x, y }
fn norm(p) { sqrt(_sq(p.x) + _sq(p.y)) }
fn _sq(v) { v * v } // _ = private
// main.vow: vow main.vow
import "./geometry.vow" as geo
let p = geo.Point { x: 3, y: 4 }
print(geo.norm(p)) // 5
Imports go at the top, with a path starting ./ or ../. Modules run in the CLI; the playground runs one file.
readFile(path) // Result: text or err
writeFile(p, s) // Result
appendFile(p, s) // Result: adds to the end
httpGet(url) // Result: body, or err on non-2xx
httpRequest(method, url, headers, body)
// Result: HttpResponse { status, headers, body }
readLine() // Result: next stdin line, err at end of input
readAll() // Result: the rest of stdin
env(name) // Result
args() // list of the user's arguments
eprint(x) // like print, to stderr
exit(code) // end now with this exit code
let n = num(s)? // top level: err stops the program,
// error: ... on stderr, exit 1
now() // current time, UTC ms
// every failure is a Result, so a program
// that compiles has handled all of them.
// In the browser these return err(...).
fn toF(c) { return c * 9 / 5 + 32 }
for a in args() {
match num(a) {
ok(c) => print("${toF(c)}")
err(e) => print("error: ${e}")
}
}
test "boiling point" { expectEq(toF(100), 212) }
test "whole program" { expectEq(run(["0", "x"], ""), "32\nerror: \"x\" is not a number\n") }
Test blocks live in the file and never run during a normal run. vow test file.vow runs them; run(args, stdin) runs the whole program and returns what it printed.
num("42") // ✗ a Result must be used
match r { ok(v) => v } // ✗ missing err arm
let x = num(s)? // ✗ ? only works inside a fn
print("{x}") // ✗ use "${x}" to insert x
if a && b { } // ✗ write: if a and b { }
let m = {"n": "x", "q": 3} // ✗ one value type per map;
// use a struct for records
Agent tests use Claude Haiku in a fresh context per run, judged against answers from Python reference solutions on tasks the language was never tuned on. Vow agents get the language guide and a short pack of example programs; TypeScript and Python agents get nothing. Every number comes from the benchmark logs in the repository. Each table shows the date it was measured; the newest numbers are in RESULTS.md.
6 everyday tasks where the other languages are strongest: 3 that reshape JSON (TypeScript's home) and 3 that clean up spreadsheet CSV exports (Python's home). Each has 33 to 40 hidden tests the agent never sees, built from bugs developers report in those languages: ids past 2^53, money rounding, quoted commas, accents typed two ways. Claude Haiku. A silent bug is a solution that passed every visible test and still gave a wrong answer on a hidden one. Fewer is better.
| language | solutions with a silent bug | measured |
|---|---|---|
| Vow | 0 of 12 | Oct 17 |
| Python | 3 of 12 | Oct 19 |
| TypeScript | 23 of 30 | Oct 17 |
Python ran 12 times on Oct 19 on the same tasks and prompts. All three of Python's silent bugs came from one agent that did not strip a byte order mark at the start of the CSV file. TypeScript was silently wrong in 10 of 15 solutions on its own JSON tasks, mostly big ids merging into one. Vow programs were also the longest of the three.
Three new tasks were written on Oct 19 after Vow's earlier fixes, so none was tuned for. The tasks are merging two JSON config files into canonical JSON, adding up worked hours from timestamped shifts with time zones, and a word frequency table over Unicode text. Each task has 39 to 43 hidden tests. Claude Haiku ran 4 agents per language for 12 solutions each.
| language | solutions with a silent bug | measured |
|---|---|---|
| TypeScript | 0 of 12 | Oct 19 |
| Python | 2 of 12 | Oct 19 |
| Vow | 2 of 12 | Oct 19 |
TypeScript had no silent bugs on these tasks. Python's 2 bugs were logic mistakes. Vow's 2 bugs came from a flaw in Vow itself where printing a decimal number cut it to 10 decimal places. That flaw was fixed the same day so numbers now print exactly up to 15 significant digits. No agent runs were repeated after the fix.
This benchmark measures whether agent programs report errors or output wrong answers when input contains unmentioned bad data. On Oct 20, 2026, Claude Haiku ran 4 agents per language across 3 rounds of 3 new tasks using CSV, JSON, and text logs. Tasks had 9 valid input tests and 10 to 12 bad data tests. Solutions passed all valid tests in rounds 2 and 3. Round 1 valid test passes were 108 of 108 for Vow and Python, and 104 for TypeScript.
| language | round 1 | round 2 | round 3 |
|---|---|---|---|
| Python | 81% | 74% | 71% |
| Vow | 8% | 52% | 56% |
| TypeScript | 29% | 23% | 55% |
Percent of bad data cases each language's programs flagged, measured Oct 20. Python led every round because unhandled exceptions stop Python programs by default. In round 1, Vow agents handled mandatory errors by substituting default values from guide examples, turning bad input into clean reports. An exit builtin was added before round 1. After round 1, top level ? operators stopped programs on bad values, and guide examples stopped using defaults. After round 2, the guide added line loops for piped input so ? reaches each row. Vow flagged 52 percent in round 2 and 56 percent in round 3. Each round used new tasks. Vow moved from last to roughly level with TypeScript.
Measured Oct 7, 2026.
| runtime | time |
|---|---|
| Vow, compiled | 0.09 s |
| Python 3.14 | 0.14 s |
| Vow, old interpreter | 4.59 s |
Agent-written solutions to 8 unseen tasks, 150 fresh inputs each. Measured Oct 8, 2026.
| language | hidden tests passed |
|---|---|
| Vow | 99.5% |
| TypeScript | 99.3% |
3 tasks written Oct 13 and never tuned on: a Lisp interpreter, a JSON schema validator and a spreadsheet evaluator (80 to 230 lines each). Claude Haiku, 9 runs per language: 6 where agents could run their code, 3 where the judge was their only output. Every run solved its task. Fewer is better.
| language | failed submissions | measured |
|---|---|---|
| TypeScript | 0 | Oct 13, 9 runs |
| Vow, after fixes | 0 | Oct 14, 9 runs |
| Vow, first round | 1 | Oct 13, 9 runs |
The one Vow failure was a misread spec rule. On 4 more new tasks (glob matching, CSV tables, line diff, expense splitting) both languages had 0 failed submissions in 12 runs each.
2 tasks that must be split into a main file plus at least 2 imported modules: a library front desk and a monthly budget report (190 to 390 lines). Claude Haiku, 6 runs per language: 4 where agents could run their code, 2 where the judge was their only output. Fewer is better.
| language | failed submissions | measured |
|---|---|---|
| TypeScript | 0 | Oct 14, 6 runs |
| Vow | 0 | Oct 14, 6 runs |
4 hard tasks, Claude Haiku. Agents could check, run and test their program before each judged submission, as real coding agents do. Every run solved its task. Failed submissions per run, fewer is better.
| language | failed per run | measured |
|---|---|---|
| TypeScript | 1.0 | Oct 12, 3 runs |
| Vow, current | 1.0 | Oct 13, 2 runs |
| Vow, a day earlier | 2.3 | Oct 12, 3 runs |
Between the two Vow rows: built-in tests, clearer docs, and fixes for checker bugs agents hit. With Claude Sonnet both languages failed 0 times (Oct 13, 1 run each).
Same tasks, but agents had to submit without running their code first. Fewer is better. Measured Oct 8, 2026.
| language | failed attempts |
|---|---|
| TypeScript | 7 |
| Vow | 15 (24 before the Oct 8 language changes) |
Each program does a real job plus one attack, under the tightest permissions each runtime supports. Measured Oct 2, 2026.
| runtime | attacks blocked |
|---|---|
| TypeScript in Deno | 7 / 7 |
| Vow sandbox | 6 / 7 (no read-only file grant yet) |
| Python in Docker | 6 / 7 |
? propagation operator, breakpoints in the debugger, machine-readable errors ("Copy for AI"), shareable code links.p.x alone), all-fields-required literals and value equality.x => x * 2) and the CLI (node cli.js program.vow) with file, network, environment and argument I/O, every operation returning a Result.parseJson returns a Result and toJson serializes structs, maps and lists.expects/ensures refuse to compile, and guards and ensures facts prove call sites safe.uses net, fs or env, and callers inherit the obligation.match with _, and functions and lambdas whose last line is their value.--sandbox with --allow net=host,fs=dir,env=NAME rejects a program before it runs if it can reach anything not granted.--interp still runs it). A differential test checks that both give identical output.${} strings: only "${expr}" inserts a value, so plain braces in templates and JSON need no escaping. Nested functions can now call themselves.matches, find, findAll, replaceAll, splitOn), words, sig, int, isInt, and list functions (map, filter, any, all, count, fold, uniq, values).field and at give a json null instead of a Result when something is missing, has, isNull and truthy test values, and str(j) renders readable text.import "./x.vow" as x, namespaced functions and structs, private _names, and errors that name the file.sqrt, pow, trigonometry, random and randomInt with --seed, and UTC date functions.readLine and readAll for standard input, httpRequest with methods, headers and bodies, urlEncode/urlDecode, and JSON building with json, jsonSet, jsonDelete, jsonMerge and prettyJson.test "name" { ... } blocks with expectEq and run(args, stdin), run by vow test; vow --check suggests them when a file has none.exit(code), eprint, and ? at the top level, where an err stops the program with error: ... on stderr and exit code 1. vow --help and vow --version.parseCsv for spreadsheet exports, casefold and normalize for matching names, JSON numbers past 15 digits kept exactly, num rejecting hex, Infinity and too-big whole numbers, cmpNum and sortWith for exact ordering, and remove for map keys.