BAML
QuickstartPodcastTeamChangelogagent tries baml
DiscordGitHub8,423Learn BAML
Learn BAMLQuickstartPodcastTeamChangelogagent tries baml
DiscordGitHub8,423

On this page

  • Design philosophy
  • Part 1 · A better language
  • 1 · Type system
  • 2 · Match
  • 3 · Error handling
  • 4 · Green threads
  • Part 2 · Tools for agents
  • 1 · Namespaces
  • 2 · baml describe
  • 3 · baml run <fn>
  • 4 · baml run -e
  • 5 · baml pack
  • Part 3 · Tools for humans
  • 1 · Workflow View
  • 2 · Profiler
  • Part 4 · Adopting BAML
  • 1 · Drops into your stack
  • 2 · Self-improvement
  • 3 · No supply chain attacks
  • Part 5 · Building agents
  • 1 · LLM functions
  • 2 · Agents & harnesses
  • 3 · Tests
  • 4 · eval / codemode
  • 5 · Sandboxing
  • Try BAML out!

Our design philosophy

  • 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.

    Many ways
    One way
  • 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.

    Non-localReferenced by import
    1.2k tok
    LocalReferenced by name
    1.2k tok
  • 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

install with
$ brew install boundaryml/tap/baml
in a project
$ baml init
# sets up skills for Claude Code, Codex, and more
$ baml agent install
$ baml run main

Full quickstart: editor setup and more options →

Part 1

A better language

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.

1A type-system like TypeScript, but without type erasure

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

load.ts
1interface User {
2 name: string;
3 email: string;
4}
5​
6// 'as' is an unchecked promise the compiler believes
7const user = JSON.parse(raw) as User;
8user.email.toLowerCase();runtime TypeError: email is undefined

baml — caught at compile time

loading editor…

2Match on types, or valuesBEP-015↗

Any of 'em work. No need for instanceof or has everywhere:

typescript

route.ts
1function route(msg: Refund | Question | string) {
2 // a grab-bag of typeof + "key in obj" checks -- no real
3 // match, so overlapping keys quietly pick the wrong arm
4 if (typeof msg === "string") return "text: " + msg;
5 if ("id" in msg) return "refund " + msg.id;
6 if ("text" in msg) return "answer: " + msg.text;
7 // miss a case -> silently returns undefined
8}
9​
10interface Refund { id: string }
11interface Question { text: string }

baml

loading editor…

3Error handling (it reads like match)BEP-002↗

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

run.ts
1function show(ok: boolean) {
2 try {
3 return fetch_page(ok);
4 } catch (e) {
5 // e: unknown -- TS can't tell you what fetch_page throws
6 if (e instanceof NetError) return "recovered: " + e.detail;
7 }
8}

baml · with a live warning

loading editor…

4Green threads a.k.a 'async without async'BEP-034↗

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.

loading editor…

BAML describe can help agents figure out which functions might run asynchronously, by inspecting the code.

spawn can run cpu-bound code in parallel

Promise.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

bun, one thread8.2 s · 1 core
baml, one thread6.8 s · 1 core
baml, spawn ×160.87 s · 10 cores

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.)

show the benchmark source — bench.baml
bench.baml
1// one "log shard": ~48 MB of text, scanned `rounds`
2// times for a marker -- worst case: it never appears,
3// so every scan reads all 48 MB
4function scan_shard(id: int, rounds: int) -> int {
5 let hay = make_shard();
6 let hits = 0;
7 for (let i = 0; i < rounds; i += 1) {
8 if (hay.includes("ERROR RATE EATER")) {
9 hits += 1;
10 };
11 }
12 hits
13}
14​
15// the parallel version: one spawn per shard
16function par(shards: int, rounds: int) -> int {
17 let tasks = [];
18 for (let s = 0; s < shards; s += 1) {
19 tasks.push(spawn_shard(s, rounds));
20 }
21 let total = 0;
22 for (let t in tasks) {
23 total += await t;
24 }
25 total
26}
27​
28// -- helpers below ---------------------------------
29​
30function spawn_shard(id: int, rounds: int) -> baml.future.Future<int, null> {
31 spawn { scan_shard(id, rounds) }
32}
33​
34function make_shard() -> string {
35 let line = "ERA EAGER ERRAND EATER ERROR RATED EARNEST ERROR RACER ERRATA REARED ROARER RETREAT TERRACE ".repeat(11);
36 line.repeat(49152)
37}

We also have built-in primitives for managing concurrency (task groups etc), which we'll get to later!

Join the community on Discord →

Part 2

Tools for agents

BAML ships with various tools to make agents find, test and distribute code more easily.

1ls — the filesystem is the namespace structureBEP-008↗

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.

the filesystem is the map
$ 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.
ns_orders/order.baml
1// baml_src/ns_orders/order.baml — referencing another namespace, unqualified
2function line_item() -> Product {unresolved type: Product. Did you mean `root.catalog.Product`?
3 Product { name: "Keyboard" }
4}
ns_orders/order.baml
1// there's a single fully qualified name, every time
2function line_item() -> root.catalog.Product {
3 root.catalog.Product { name: "Keyboard" }
4}

This is also why constructing a Class in BAML requires always adding the name of the class — MyClass { }. There are no anonymous records.

2baml describe — a built-in AST-based grep, to find things faster

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

agent without describe
$ grep -rn "greet" baml_src/
main.baml:5:function greet(name: string) -> Greeting {
main.baml:10: greet("world").message
main.baml:14: test "greets_world" {
main.baml:18: assert.equal(greet("bob").message, …
$ cat baml_src/main.baml
class 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

agent with describe
$ baml describe greet
function greet baml_src/main.baml:5-7
function greet(name: string) -> Greeting {
Greeting { message: "hi, " + name }
}
dependencies:
class Greeting baml_src/main.baml:1
references (2):
baml_src/main.baml:10 greet("world").message
baml_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.

3baml run <function>BEP-027↗

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 <function>
$ baml run main
Compiling 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 -- --help
function greet(name: string) -> Greeting
Options:
--name <string>
# any function is a target; its params become --flags

4baml run -e — run small baml programs inline

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
$ baml run -e '"a,b,c".split(",")'
Compiling expression
Running expression
["a", "b", "c"]
$ baml run -e '{ let t = 0; for (let i = 0; i < 5; i += 1) { t += i * i; }; t }'
30
Finished expression in 0s
# no file, no project — paste, run, observe

5baml pack — ship a function as a tiny binary

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
$ baml pack -f greet -f main -o greet-bin
Packaging greet,main
Finished greet-bin [greet,main, aarch64-apple-darwin] in 0s
$ ls -lah greet-bin
7.9M greet-bin
$ ./greet-bin greet --name "hacker news"
{"message":"hi, hacker news"}
$ ./greet-bin --help
Usage: greet-bin <COMMAND>
Commands:
greet function greet(name: string) -> Greeting
main function main() -> string
show the source — main.baml
main.baml
1function greet(name: string) -> Greeting {←name: string → --name <flag>
2 Greeting { message: "hi, " + name }
3}
4​
5function main() -> string {
6 greet("world").message
7}
8​
9class Greeting {
10 message: string,
11}

The packed binary is 81% smaller than Bun's

Here's a comparison of BAML vs Bun in creating a compiled binary. The binary size is just 12.1 MB:

binary size

baml pack12.1 MB · 5.7 MB gzipped
bun build --compile63.1 MB · 23.5 MB gzipped

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

Tools for humans

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.

1BAML Workflow View — navigate and understand your code

Here's a fuller BAML project: an LLM “Heads Up” guessing game with an agent loop, a non-LLM binary search, classes, and a couple of testsets. The graph view is the visual counterpart to baml describe: a map you can click through instead of grepping. Open the graph tab and jump around.

loading playground…

2BAML Profiler

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.

BAML playground Flame tab — a flame graph of a run with a per-function self-time table on the left.

Part 4

Adopting BAML

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.

1Drops into your existing stackBEP-030↗

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.)

baml_src/functions, types, tests
→
baml generateone command
→
baml_sdk/typed client + runtime inside
→
your appfrom baml_sdk import b

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

resume.baml
1class Resume {
2 name: string,
3 email: string?,
4}
5​
6function extract_resume(text: string) -> Resume {
7 client: "openai/gpt-4o-mini"
8 prompt: `Extract the resume. ${ctx.output_format}\n${text}`
9}

generated sdk · python

baml_sdk
1from baml_sdk import extract_resume, Resume
2​
3# typed call — or: await extract_resume_async(text=...)
4resume: Resume = extract_resume(text="Jane Doe, jane@acme.com ...")
5​
6resume.name # str
7resume.email # str | None
8resume.model_dump() # plain pydantic underneath

Every function generates a sync + async pair; Resume is a real pydantic v2 model.

2Recursive self-improvement

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.

3No supply chain attacks

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

Building agents

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.

one stochastic call
main()output can now vary
└─ load_config()still deterministic
└─ run_pipeline()output can now vary
└─ summarize()output can now vary
└─ llm.summarize_chunk()⚡ same input → different output

4 of 5 functions can no longer be tested with assert output == expected — and nothing in the language marks them.

1Native LLM Functions — composable building blocks for agents and harnesses

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.

loading playground…

2Build harnesses, agents, or delegate to Claude Code

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!

3Write tests anywhere, or load them at runtimeBEP-023↗

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:

loading editor…

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:

loading editor…

Custom test runners go further: retries, uploading reports, running things in parallel or synchronously.

4eval(), but type-safe

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.

codemode.baml
1let raw = baml.reflect.new_package("my_package");
2baml.package.set_file("virtual/path/to/file.baml", `
3 function hello() -> string {
4 "hello world"
5 }
6`)
7let pkg = raw.build();
8​
9let cb = pkg.get<() -> string>("hello");
10print(cb());
11​
12// and its typesafe!
13let cb = pkg.get<() -> int>("hello") catch (e) {
14 baml.reflect.CompilerTypeError => {
15 print(`"hello" is not a function that returns int. ${e}`)
16 }
17};

5SandboxingBEP-058↗

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.

sandbox.baml
1// inside a mock scope, baml.http.fetch is whatever you say it is.
2let net = baml.mock.new(baml.http.fetch);
3net.replace((req: baml.http.Request) -> baml.http.Response {
4 // lets ban fetch! so even if the llm uses it, we get an error
5 throw baml.NotImplementedError { message: "fetch is disabled in this scope" };
6});
7​
8let shell = baml.mock.new(baml.sys.shell);
9shell.replace((command: string) -> baml.std.ShellOut {
10 // lets ban shell! so even if the llm uses it, we get an error
11 throw baml.NotImplementedError { message: "shell is disabled in this scope" };
12});
13​
14​
15baml.mock.scope([net, shell], () -> void {
16 run_generated(); // every fetch/shell in here hits the stand-in
17});
18​
19// out here, fetch and shell are the real thing again -- the scope undoes itself.

Try BAML out!

install with
$ brew install boundaryml/tap/baml
editor
$ baml ide install --code
in a project
$ baml init
# sets up skills for Claude Code, Codex, and more
$ baml agent install
$ baml run main

boundaryml.com/quickstart →

Questions or feedback?

Join the BAML Discord. We're around to help you get set up, and we read every bit of feedback.

Join the Discord