The more we invent, the worse agents are at it. Everything different from what they already know should be a deliberate decision.
Agents love TypeScript, and humans do too. Types, unions, generics, give me more. But TypeScript is bandaging up broken JavaScript, so it has real escape hatches agents love to abuse.
// the model reply is just a string const user = reply as unknown as User // "trust me": compiles, checks nothing, breaks later // BAML parses and validates the output, so there is no cast to write function ExtractUser(text: string) -> User
An agent sampling tokens will eventually write an invalid state. If it can't compile, it can't ship, and no human has to catch it.
class Ticket {
status "open" | "closed" // a typo like "opne" will not compile
}As much as it hurts us: today is the most code you'll ever read. The only way to understand it will be through hindsight and focused traces.
Every option on the table eventually gets used by some agent. The codebase ends up carrying five versions of the same thing, and the next agent adds a sixth.
When a change isn't local, the agent goes on a side quest to chase it across the codebase. It comes back drifted, its context polluted with files that have nothing to do with the task.
Humans still need their IDEs. But most code is now read and written by agents, and an agent can't hover, click, or read a tooltip.
Try BAML
Part 1
BAML aims to be an agent-friendly language. In this overview, we'll start with the syntax and type system decisions we made. Then explore the agent-first cli tooling.
As much as we want agents to write code, human trust is still a vital part of a healthy software system. The third section focuses on tooling for humans, and the fourth shares how we made BAML incrementally adoptable, so you won't need to re-write your whole codebase in BAML.
And lastly, not only has the way we write code changed, but also the kind of code we write as well. More and more code is agentic loops, created by LLMs on the fly, and probabilistic. We added a few syntax constructs to help rein in the non-determinism.
BAML has a type system like TypeScript, but persists it at runtime. TypeScript explicitly chose not to be sound, trading it away for productivity. That was the right move for humans, but it's the wrong default when agents are writing the code. It's not a coincidence there are 5 different schema validation libraries for TS: the type system doesn't mean enough.
BAML has no any, types are what the code says at runtime, and it includes advanced features like unions, generics, recursive types, and interfaces on day one.
typescript — where the types could lie to you
interface User { name: string; email: string;}// 'as' is an unchecked promise the compiler believesconst user = JSON.parse(raw) as User;user.email.toLowerCase();runtime TypeError: email is undefinedbaml — caught at compile time
Any of 'em work. No need for instanceof or has everywhere:
typescript
function route(msg: Refund | Question | string) { // a grab-bag of typeof + "key in obj" checks -- no real // match, so overlapping keys quietly pick the wrong arm if (typeof msg === "string") return "text: " + msg; if ("id" in msg) return "refund " + msg.id; if ("text" in msg) return "answer: " + msg.text; // miss a case -> silently returns undefined}interface Refund { id: string }interface Question { text: string }baml
TypeScript exceptions have no types, so catching the right one means ugly code. BAML reads every throws statement and tells you every single error a function can throw. Hover fetch_page below to see its full inferred error set. That live warning is the compiler proving the ParseError arm can never fire.
typescript
function show(ok: boolean) { try { return fetch_page(ok); } catch (e) { // e: unknown -- TS can't tell you what fetch_page throws if (e instanceof NetError) return "recovered: " + e.detail; }}baml · with a live warning
Doing work in parallel is important. But we always hated having an async and non-async version of our code. We chose Go's approach to concurrency, but with a typescript feel.
BAML supports lightweight green threads via spawn and await. Run any function asynchronously without having to write async function in 10 other files everywhere. Easy to parallelize slow LLM http requests and tool calls.
BAML describe can help agents figure out which functions might run asynchronously, by inspecting the code.
spawn can run cpu-bound code in parallelPromise.all only parallelizes I/O; compute still runs on one thread. In BAML you can split 38 GB of logs into chunks and scan them in parallel, 9x faster.
scan 38.4 GB of text · wall clock
BAML's stdlib string search is native Rust, so even one thread edges out Bun here. (The one place Bun still wins per core is tight arithmetic loops, where its JIT beats our interpreter.)
// one "log shard": ~48 MB of text, scanned `rounds`// times for a marker -- worst case: it never appears,// so every scan reads all 48 MBfunction scan_shard(id: int, rounds: int) -> int { let hay = make_shard(); let hits = 0; for (let i = 0; i < rounds; i += 1) { if (hay.includes("ERROR RATE EATER")) { hits += 1; }; } hits}// the parallel version: one spawn per shardfunction par(shards: int, rounds: int) -> int { let tasks = []; for (let s = 0; s < shards; s += 1) { tasks.push(spawn_shard(s, rounds)); } let total = 0; for (let t in tasks) { total += await t; } total}// -- helpers below ---------------------------------function spawn_shard(id: int, rounds: int) -> baml.future.Future<int, null> { spawn { scan_shard(id, rounds) }}function make_shard() -> string { let line = "ERA EAGER ERRAND EATER ERROR RATED EARNEST ERROR RACER ERRATA REARED ROARER RETREAT TERRACE ".repeat(11); line.repeat(49152)}We also have built-in primitives for managing concurrency (task groups etc), which we'll get to later!
Part 2
BAML ships with various tools to make agents find, test and distribute code more easily.
AI agents spend too much time searching for things in large projects. In BAML the project structure is self-describing: an agent can ls a BAML project and know how it's laid out, because namespaces are just directories with a ns_ prefix. There are no imports, since everything is referred to by its fully qualified name, like Go. Inside a namespace directory, all types, functions and objects are available in every file by default.
$ ls baml_src/ns_catalog ns_orders$ ls baml_src/ns_catalog/product.baml# the layout of baml_src/ is the layout# of the program. ls is a map of it.
// baml_src/ns_orders/order.baml — referencing another namespace, unqualifiedfunction line_item() -> Product {unresolved type: Product. Did you mean `root.catalog.Product`? Product { name: "Keyboard" }}// there's a single fully qualified name, every timefunction line_item() -> root.catalog.Product { root.catalog.Product { name: "Keyboard" }}This is also why constructing a Class in BAML requires always adding the name of the class — MyClass { }. There are no anonymous records.
describe is easier for agents to use than an LSP, and more informative than grep. Agents writing BAML code don't need to search through 10 files to figure out how things work. Here's a transcript of an agent searching with grep, versus with baml describe:
agent with grep
$ grep -rn "greet" baml_src/main.baml:5:function greet(name: string) -> Greeting {main.baml:10: greet("world").messagemain.baml:14: test "greets_world" {main.baml:18: assert.equal(greet("bob").message, …$ cat baml_src/main.bamlclass Greeting {message: string,… 19 more lines read into context …$ grep -rn "Greeting" baml_src/main.baml:1:class Greeting {main.baml:5:function greet(name: string) -> Greeting {main.baml:6: Greeting { message: "hi, " + name }# 3 tool calls, a whole file in context — and the# caller list is still just text matches
agent with describe
$ baml describe greetfunction greet baml_src/main.baml:5-7function greet(name: string) -> Greeting {Greeting { message: "hi, " + name }}dependencies:class Greeting baml_src/main.baml:1references (2):baml_src/main.baml:10 greet("world").messagebaml_src/main.baml:18 assert.equal(greet("bob")…✓ baml describe gives you signature, deps, every reference
The reference list is the part grep can't give you: every call site, resolved — handy for spotting near-duplicates before writing a second copy of a function. We'll keep making improvements to this tool.
BAML makes it easy for agents to run any function in your project as if it were a CLI command. Function parameters get parsed automatically and can be set with CLI flags.
$ baml run mainCompiling 1 file(s)Compiled 1 file(s) in 1s"hi, world"$ baml run greet -- --name "hacker news"Greeting { message: "hi, hacker news" }$ baml run greet -- --helpfunction greet(name: string) -> GreetingOptions:--name <string># any function is a target; its params become --flags
Run small baml programs inline, without having to write to a file. Small simple feature, but great for agents writing/testing small baml scripts.
$ baml run -e '"a,b,c".split(",")'Compiling expressionRunning expression["a", "b", "c"]$ baml run -e '{ let t = 0; for (let i = 0; i < 5; i += 1) { t += i * i; }; t }'30Finished expression in 0s# no file, no project — paste, run, observe
BAML pack is a CLI that takes your baml program and auto-creates a CLI for you from the function signature. It can compile and run on any target architecture. Useful for agents creating shareable mini programs.
$ baml pack -f greet -f main -o greet-binPackaging greet,mainFinished greet-bin [greet,main, aarch64-apple-darwin] in 0s$ ls -lah greet-bin7.9M greet-bin$ ./greet-bin greet --name "hacker news"{"message":"hi, hacker news"}$ ./greet-bin --helpUsage: greet-bin <COMMAND>Commands:greet function greet(name: string) -> Greetingmain function main() -> string
function greet(name: string) -> Greeting {name: string → --name <flag> Greeting { message: "hi, " + name }}function main() -> string { greet("world").message}class Greeting { message: string,}Here's a comparison of BAML vs Bun in creating a compiled binary. The binary size is just 12.1 MB:
binary size
Bun 1.3.14, BAML release toolchain, aarch64-apple-darwin. Bun embeds a whole JavaScript engine; the BAML runtime is 12.1 MB.
Part 3
We also built tools to keep humans in the loop. Even if most code isn't being read, these tools can help humans dive deep and iterate quickly when they need to.
We also shipped a profiler to help you visualize flame graphs and see what's causing potential slowness. Agents can use this tool too, but humans can also visualize and dive into the nitty-gritty details.

Part 4
Although we are still pre-1.0, BAML is ready to use today. Here's how we make it easier to use and trust in production.
When we first made BAML 2 years ago we decided it had to be callable from other languages, with an amazing developer experience.
BAML can generate SDKs for your favorite language, and call your functions using these type-safe interfaces, even if they include generics or class methods. Think of an OpenAPI client generator, except the contract carries real business logic, not just data shapes. (For more details, check out our talk at rust conf.)
The types come out native — a pydantic model in Python, a typed class in TypeScript — with your functions, methods, and generics intact. Pick a feature and a language to see the same BAML file generate each SDK:
the baml file
class Resume { name: string, email: string?,}function extract_resume(text: string) -> Resume { client: "openai/gpt-4o-mini" prompt: `Extract the resume. ${ctx.output_format}\n${text}`}generated sdk · python
from baml_sdk import extract_resume, Resume# typed call — or: await extract_resume_async(text=...)resume: Resume = extract_resume(text="Jane Doe, jane@acme.com ...")resume.name # strresume.email # str | Noneresume.model_dump() # plain pydantic underneathEvery function generates a sync + async pair; Resume is a real pydantic v2 model.
To help keep BAML stable and improving over time, we're simulating thousands of agents writing BAML code to get feedback from agents themselves. We built agent-tries-baml to recursively self-improve BAML and make it easier for agents to write. For example, we test our BAML skill against agents to figure out which set of instructions helps agents write BAML faster.
BAML is still < 1.0, but we're close to reaching full stability. Feel free to join our language experiments if you're curious about this process.
Okay, to be fair, BAML doesn't yet have a package manager. We're working on it! In the meantime, just make AI agents write all the code you need.
Part 5
Writing code is one thing, but in the future every software program will interact with AI agents or non-deterministic AI code. Whilst BAML supports writing anything from a web-server to a data-processing library, our main focus is to provide primitives to help teams deal with nondeterminism. To do this we make sure BAML programs are observable, testable, and measurable.
4 of 5 functions can no longer be tested with assert output == expected — and nothing in the language marks them.
An LLM call in BAML is just a function: the prompt is the body, the return type is the schema. Because it's a real function, it can be evaluated, optimized, and tracked at runtime by observability platforms.
If you've used BAML in the last 2 years, you'll be happy to hear we still have our error-correcting JSON parser, which reliably coaxes structured output out of small language models.
BAML ships with tooling to observe LLM function inputs and outputs, like our workflow visualizer in VSCode. It's especially helpful when working with multimodal outputs, like images.
We're currently building our first-class standard library to build AI agents and harnesses, or call other kinds of agents (Claude Code). It will support anything from realtime voice agents to batched APIs. Let us know if you're interested in an early preview!
Write tests anywhere, in any file.
Create arbitrary groups and add tests dynamically — generate tests for each item in an array, create tests from a CSV file, or from S3:
View tests in the Playground: in case a human needs to see things, we have nice utilities. Or just have agents run baml test.
Create evals — LLM-as-judge, statistical analysis, etc. In other frameworks that's a YAML schema and a hosted UI. In BAML, it's all just code. Pass a test when at least N% of runs do, using custom test runners:
Custom test runners go further: retries, uploading reports, running things in parallel or synchronously.
Agents don't just call tools, they also write and run code. Twitter X calls it codemode.
In python, you would write eval('print("hello world")') to do codemode. But eval is unsafe and loses all type-safety and predictability.
BAML's reflection APIs give you eval, but with typed compiler errors. If the string has the wrong signature, you can get a runtime-compiler error that you can feed back to the agent so it can fix its code.
Coming soon: the reflection API below isn't available yet.
let raw = baml.reflect.new_package("my_package");baml.package.set_file("virtual/path/to/file.baml", ` function hello() -> string { "hello world" }`)let pkg = raw.build();let cb = pkg.get<() -> string>("hello");print(cb());// and its typesafe!let cb = pkg.get<() -> int>("hello") catch (e) { baml.reflect.CompilerTypeError => { print(`"hello" is not a function that returns int. ${e}`) }};Running code an agent just wrote is scary. We've started using machine sandboxing to isolate the code from the rest of the system, but what if we wanted to guarantee that the code doesn't make any network calls? We could just prompt it, but...
We can do a bit better. BAML supports mocking any function, whether it's in the standard library or in your own package. You can swap it out with another implementation, and it only works in a certain scope.
This doesn't replace the need for machine sandboxing. mock can't sandbox machine state (though vfs's are much simpler now). However, it does give you an option to not require machine sandboxing for every problem.
Coming soon: the mocking primitive below isn't available yet.
// inside a mock scope, baml.http.fetch is whatever you say it is.let net = baml.mock.new(baml.http.fetch);net.replace((req: baml.http.Request) -> baml.http.Response { // lets ban fetch! so even if the llm uses it, we get an error throw baml.NotImplementedError { message: "fetch is disabled in this scope" };});let shell = baml.mock.new(baml.sys.shell);shell.replace((command: string) -> baml.std.ShellOut { // lets ban shell! so even if the llm uses it, we get an error throw baml.NotImplementedError { message: "shell is disabled in this scope" };});baml.mock.scope([net, shell], () -> void { run_generated(); // every fetch/shell in here hits the stand-in});// out here, fetch and shell are the real thing again -- the scope undoes itself.Join the BAML Discord. We're around to help you get set up, and we read every bit of feedback.
Join the Discord