baml-core
BoundaryML/baml/skills/baml-core/SKILL.md
Minimal BAML skill. BAML is a statically-typed, expression-oriented language with first-class LLM functions — TypeScript-like, snake_case methods, etc. Useful for building ai workflows, agents, evals.
Skill9.4k starsChanged 13 months ago
- Reads credentials
- Installs packages
What's in it
- baml
- Best practices and info
- Project layout and conventions
- Running AI functions
- Example 1 — LLM DSL + glue (schema, attributes, client, backtick prompt, post-processing)
- Example 2 — the language (methods, interpolation, closures, maps, json, errors). Mutations work like Typescript
- Example 3 — interfaces (shared behavior, default method, dynamic dispatch, requires)
- Example 4 — pattern matching (match over values + types, is, if let)
- Example 5 — resource safety + structured concurrency (defer, cleanup, ErrorContext, spawn options, futures, while-let)
- Concurrency — green threads (parallelize LLM / HTTP calls)
- BAML workflow visualizer annotations
---
name: baml-core
description: Minimal BAML skill. BAML is a statically-typed, expression-oriented language with first-class LLM functions — TypeScript-like, snake_case methods, etc. Useful for building ai workflows, agents, evals.
metadata:
baml-toolchain-version: "{{BAML_TOOLCHAIN_VERSION}}"
---
# baml
BAML is a statically-typed, expression-oriented *language* — TypeScript with `snake_case` methods, `name: type,` fields, enums, interfaces, generics, closures, optional chaining, backtick strings with `${...}` interpolation, `.to_string()` on any value, a real stdlib. And a declarative *DSL* for LLM calls (`function … { client: prompt: }`, `test`) that desugars into it, so a model's structured output is just a typed return value.
**The CLI is the documentation. Discover via `baml describe`:**
```bash
brew install baml # CLI binary: `baml`
baml init # new project (baml.toml + baml_src/)
baml help <command> # CLI options and examples
baml describe baml.json # reference for any module/type/method/signature/keyword
baml describe Array --budget 120 # (Array, String, Map, assert, match, patterns, spawn, python, ...)
# ends with "N more lines"? re-run with `--budget <N>`
baml check # compile-check the project
baml run -e 'expr' # eval an expression; fast feedback + syntax check
baml test --list && baml test # run every test/testset block
baml fmt baml_src/main.baml # canonicalize the project's formatting
```
`baml describe <name>` prints the **full source body** of stdlib functions — the fastest way to verify behavior, and the only path for embedded builtins like `assert` (no on-disk file). Pure functions need no test/client — check them with `baml run -e 'add(2, 3)'`.
Don’t describe APIs already demonstrated below unless you run into some errors. You can start based off the examples and use it if you run into more errors or you want actual stdlib details.
Mostly it behaves like JavaScript/TypeScript, with very similar syntax — but BAML is more sound/strict.
## Best practices and info
- **LLM function = typed return.** The RETURN TYPE *is* the schema the model must produce (`class`, `enum`, literal union, `string[]`, `T?`). Structured output is just a typed value — hand it to ordinary code.
- **Prompts are backtick strings with `${...}` interpolation.** Write `prompt:` ``… ${arg} …``, and **always inject `${ctx.output_format()}`** for a structured return. Escape with `\`` / `\${`; nest with extra backticks.
- **Clients are values, not config blocks.** `client Fast = openai.ResponsesClient.new(model = "…", api_key = env.OPENAI_API_KEY);` — the old `client<llm> Name { provider: …, options: {…} }` block is **removed**. Anything implementing `ai.Client` works (`openai.ResponsesClient`, `openai.ChatClient`, `anthropic.Client`, …; constructor parameters differ by provider). `api_key` and `base_url` accept `ai.Credential`: a literal string, `null` for the provider default, or a late-bound `env.NAME` reference resolved at request time. Compose reliability by **wrapping**: `ai.clients.Retry.new(inner = c, max_attempts = 3)` and `ai.clients.RoundRobin.new(members = […])` have `.new`, but `ai.clients.Fallback { members: […] }` does **not** — construct it as a class literal. Then use `client: Fast` in the function, or the shorthand `client: "openai/gpt-4o-mini"`. `baml describe openai` / `baml describe ai.Credential` / `baml describe ai.clients`.
- **Shape the schema with field attributes.** `@description("…")` adds a `///` hint the model sees in `${ctx.output_format()}`; `@alias("name")` renames the emitted JSON key. Chain: `tags: string[] @alias("labels") @description("…")`.
- **Test the pure code, not the model.** Unit-test orchestration/post-processing on literal data with `assert.`*. Calling an LLM function in a `test` with a live client makes a real request. To test prompts, requests, and parsing offline, use `F@spec(args)` or a scripted client (see [Running AI functions](#running-ai-functions)).
- **Build strings with interpolation, not coercion.** ``score=${n}`` stringifies any value (implicit `.to_string()`); call `.to_string()` for the string alone. `+` needs both sides already strings (`"n=" + 5` won't compile).
- **`catch` for some, `catch_all` for all.** `expr catch (e) { baml.errors.ParseError => fallback }` handles a *specific* error; `expr catch_all (e) { _ => fallback }` is *exhaustive* — for a workflow top / entrypoint. Errors propagate implicitly; callers needn't re-declare. **Raise** with `throw baml.errors.InvalidArgument { message: "…" }` (error types are the builtin `baml.errors.*` classes — `InvalidArgument`/`ParseError`/`Io`/`Timeout`/…; `baml describe baml.errors`); annotate a fallible signature with `-> T throws ErrType`. Prefer a typed result **union** (`type R = Ok | Err`) over throwing for ordinary control flow.
- **Interfaces = shared behavior + dynamic dispatch.** `interface I { function m(self) -> T throws never }`; each method declares its `throws` clause. A method with a body is a default implementation, which an implementor may override. A class opts in via `implements I { … }`; a value typed `I` (or `I[]`) dispatches to the implementor at runtime. Interfaces can also declare associated types and generic bounds. `baml describe interfaces`.
- **`requires`, not inheritance.** BAML has no inheritance: a class can't extend a class. `interface Pet requires Animal, Named { … }` makes those interfaces prerequisites of `Pet`. `Pet`'s default bodies can call their methods, and a value typed `Pet` exposes them. Every `Pet` implementor also writes its own `implements Animal` and `implements Named` blocks (E0125 otherwise). A diamond of `requires` compiles, and the shared interface is implemented once; a cycle is an error. When two interfaces on one class declare the same method name, an unqualified call is an error (E0121); call `obj.as<I>.m()` instead.
- **Pattern matching.** `match (v) { … }` over values/types; arms are `pattern => expr` — literals, `let x: T` (bind + narrow), class destructure `T { f: let y }`, or-patterns `A | B`, guards `… if cond`, `_`; must be exhaustive. Also `v is T` → bool (narrows) and `if let x: T = v { … } else { … }`. `baml describe patterns`.
- **Narrowing — locals only.** `v is T`, `v != null` / `v == null` and a truthy test narrow `v` only when `v` is a **local** (a `let` or a parameter) that **no closure captures**: in the guarded `if` / `while` branch, after an early exit (`if (!(v is T)) { return … }`), and on the right side of `&&` (left side true) and `||` (left side false) — `v is T && v.f == 1`, `s == null || s.length() == 0`. NOT narrowed: a captured local, a **field path** (`h.v is T && h.v.f` → `E0007`) and an index path (`xs[0] is T`). Tasks can interleave between statements, so another task or an alias can change a field between the test and the read. For a field write `let v = h.v;` first, or `if let v: T = h.v { … }`, or `match (h.v) { let v: T => … }` (each reads the field one time). An assignment to the local replaces the fact with the assigned type. `baml describe is`.
- **Concurrency = green threads.** `spawn { … }` returns a `Future`; `await` collects it. Combine many with `baml.future.all` / `all_settled` / `race` / `any`. `all_settled` returns `Success<T>`, `Failure<E>`, or `Panicked` for each input; input cancellation is a `Panicked` outcome. Configure a spawn with a `with` clause: `spawn with limit, token { … }` — each value listed is a `baml.spawn.Modifier` that transforms the plan the spawn builds, left to right. `baml.spawn.Limit.new(n)` caps concurrency (excess spawns queue), a `baml.spawn.CancelToken` cancels cooperatively, `baml.spawn.Root.new()` detaches the task from its spawner's cancellation. `baml describe spawn` / `baml describe baml.future`.
- **Resource safety — `defer`, `cleanup`, `catch (e, ctx)`.** `defer { … }` runs a block at scope exit, LIFO, on *every* path (return / throw / fall-through) — like Go. A class method named `function cleanup(self) -> void` is a **finalizer**: it runs at most once per instance whether you call it, `defer` it, or the GC reclaims it. `catch (e, ctx)` binds an **`ErrorContext`** alongside the error — an error thrown while handling another chains onto it, so `ctx.root_cause()` / `ctx.cause` walk back to the original failure and `ctx.to_string()` renders the whole chain (Python `__context__`-style). `while let PATTERN = expr { … }` loops until the pattern fails (e.g. draining a `T?`-returning `.pop()`).
- **Call BAML from Python / TS.** Declare a `[generator.<name>]` in `baml.toml`, run `baml generate`, then import the typed `baml_sdk`. Install + usage: `baml describe python` / `baml describe typescript` / `baml describe baml_sdk`.
- **Safe access over indexing.** Subscript panics on a missing index/key; use `.at(i)`/`.get(k)` (→ `T?`), reach through with `?.`, default with `??` (parenthesize: `(m.get(k) ?? 0) + 1`).
- **Stdlib methods are snake_case, called on a value.** Some return new, some mutate in place, a few do both (`sort_by_key` sorts the receiver *and* returns it) — to read the docs, `baml describe <word/type/identifier/keyword/etc>`.
- **Class fields `name: type,`; construct `Type { field: val }`.** Methods take a bare `self`; factories are free functions. **Fields are mutable** (like TS): `obj.field = v` and `obj.field += n` work, and a `self` method can mutate in place — a side-effect method returns `void`. **Classes are reference types**: `find`/`at(i)`/subscript return a *live alias*, not a copy, so mutating the result mutates that element inside the array (`xs.find(p)?.n += 1` updates `xs`), and a class passed to a function can be mutated by the callee. Struct-update spread is supported: `User { ...u, tier: Tier.Free }`. **Empty classes are legal** (`class Marker {}`) — handy as union variants. **Enums are plain variants — no methods, no associated data** (`E.A.foo()` won't compile); put behavior in free functions that `match`. `enum E { A, B }`, access `E.A`.
- **Blocks are expressions** — last expression is the value (no `;`); `return x;` for early exit. A side-effect-only function returns `void`; its block's unit value is `null`. `for (let x in xs)` iterates VALUES; `while (cond) { … }` loops. Closures `(x) -> { ... }` infer param/return from context (annotate `(x: T) -> R` only when ambiguous; the `->` is required). `.map`/`.filter` return arrays directly (no `.collect()`). Empty map needs a type: `let m: map<string, int> = {};`.
- **No ternary — `if/else` is the expression.** There's no `cond ? a : b`; `if (cond) { a } else { b }` *is* an expression that returns a value, so assign it directly: `let label = if (x > 3) { "big" } else { "small" };`. Each branch is a block whose last expression is its value (no `return`). Chain with `else if`, and pair with `if let PATTERN = expr { … } else { … }` for bind-and-narrow.
- **Conditions use truthiness.** `if`, `while`, match guards, `&&`, `||`, and `!` accept any value. Falsy values are `false`, `null`, numeric zero, empty strings, empty arrays/maps, and empty bytes; everything else is truthy. `&&` and `||` still return `bool`, not an operand value; they short-circuit. A truthy optional local narrows in the taken branch and on the right side of `&&` (`s && s.length() > 0`).
- **Arrays have a JS-like method set** — `map`/`filter`/`filter_map`/`reduce`/`find`/`some`/`every`/`flat_map`/`slice`/`concat`/`join`/`includes`/length(), plus in-place `push`/`pop`/`shift`/`unshift`/`sort_by`/`sort_by_key`. Most take closures that can `throws`. `baml describe Array` gives more info.
- Local let bindings are reassignable (x = x + 1) — no mut keyword (it's TS let, not Rust); there's no const either.
- **Args: defaults with `=`, keyword calls with `=` (never `:`).** Declare a default in the signature: `function f(a: int, b: int = 10)`; call `f(1)` or `f(1, b = 2)`. A **defaulted param must be passed by name** — `f(1, 2)` is an error (`defaulted parameter 'b' must be passed by name`). Any param (even required) may be passed by name (`f(a = 1, b = 2)`), and you can skip a middle default to set a later one (`f(1, c = 9)`). Keyword syntax is `name = value`; `name: value` won't parse (`:` is for types/fields). **`T?` does NOT make an argument optional** — unlike TS `b?: T`, a `b: T?` param is still *required* (you must pass `null`, else `expected N argument(s), got …`); add `= null` to make it omittable. Built-ins follow this: `baml.http.fetch(url, timeout = baml.time.Duration.from_seconds(10))`.
- **Where it diverges from TS (the silent traps):** arithmetic is *type-driven*, not TS-style. `int / int` is **truncating integer division** (`285 / 100 == 2`, NOT `2.85`) and `%` is the remainder (`285 % 100 == 85`); this compiles fine and just gives a quietly-wrong number, so it's the highest-value gotcha. Mix in a float to get float division (`285 / 100.0 == 2.85`, `285.0 / 100 == 2.85`); any mixed `int`/`float` op promotes to `float` (`5 + 2.0 == 7.0`). There is **no `.to_float()`** — convert an int with `n * 1.0` (or divide by a float). An `int` result does **not** auto-coerce to `float` on assignment (`let x: float = 285 / 100` is a compile error). `+` is **numeric-only**: string concat needs both sides already `string` (`"n=" + 5` won't compile — use `${...}` interpolation). Comparisons (`==`, `<`, …, structural `==`) and `&&`/`||`/`!` are TS-like.
- **Tests:** lone `test "name" { ... }` (no wrapper); `testset` only GROUPS. Asserts (only 6): `assert.equal`/`approx_equal`/`is_true`/`is_type`/`not_null`/`contains`. `assert.equal` compares **structurally** (deep, across classes/arrays/maps) — and so does plain `==`, which is the bool form. `assert.equal` is *exact* on floats; use `assert.approx_equal(actual, expected, eps)` for computed ones. Last assert: no trailing `;`. Canonical IDs are `root::TestName` for a top-level test and `root::Testset::TestName` inside a testset. Run one with `baml test -i "root::TestName"` (`-x` to exclude); `baml test --list` prints valid selectors.
- **Namespaces =** `ns_*` **directories, no imports.** A folder `ns_<name>/` under `baml_src/` puts its files in namespace `<name>`; files in `baml_src/` itself are the `root` namespace (nesting stacks — `ns_a/ns_b/` → `root.a.b`; non-`ns_` folders don't namespace). Same namespace = same scope: files share definitions with no import. To reach *another* namespace, use the **absolute** path `root.<ns>.<name>` (for example, `root.llm.Response`). Run a target by its namespace-relative path (`baml run agent.main`), but `baml run -e` evaluates in the root scope, so reach in with the absolute form: `baml run -e 'root.agent.main()'`. Keep namespaces as flat as practical, like Go packages.
- Run `baml fmt` when you're done with a feature.
- BAML functions, methods, and types are accessible from other languages. Run `baml describe baml_sdk` for setup instructions. The selected toolchain must match the installed language bridge; use `baml toolchain use <channel-or-version>` to select it. Keep AI-related things and workflow logic in BAML as much as possible.
- BAML has `log.info(..)`, `log.debug(..)`, `log.warn(..)`, and `log.error(..)`.
- `baml pack` can create a binary.
- Use backticks instead of the removed `#" "#` string syntax.
- **Reflection.** BAML has full reflection: `reflect.Type.of<T>()`, function signatures, `reflect.call_any`, and classes and enums built at runtime. Explore it with `baml describe reflect`.
## Project layout and conventions
1. For a multi-step LLM workflow, you can mirror its steps in the tree: numbered folders (`1_extract/`, `2_classify/`) and a top function that only calls the steps in order.
2. Group related files into directories once a folder gets crowded (past about 8 files); folders without an `ns_` prefix don't create namespaces, so nest freely.
3. AI functions and their output classes go in `<name>_prompt.baml` with no logic; the code around them in `<name>.baml`; tests in `<name>_test.baml`, or at the bottom of the source file, as in Rust.
4. Files stay under 400 lines; a function with phases calls one named function per `//#` phase.
5. Each file gets a one-line header saying what it holds; other comments say why, in at most 10 words.
6. On output types, `///` comments, `@description`, and enum names all render into `ctx.output_format()`, so editing them edits the prompt; put notes in `//` comments.
7. Entry points take `client: ai.Client? = null` and pass it to every call; tests run offline on scripted replies.
## Running AI functions
`F(args)` is shorthand for `ai.Agent.new(client = …).run(F@spec(args)).value`. `F@spec(args)` is an unrun, immutable `ai.FunctionSpec<Out>`:
- `.call(client = c)` runs it.
- `.build_request(client = c)` returns the provider HTTP request with no I/O; use it to test prompts and clients.
- `.parse(reply)` parses a saved reply.
- `.prompt()` and `.output_type()` inspect it.
`ai.Agent.new(max_steps = 12, schema_attempts = 2, client = null, on_event = null)` is the default loop. It runs the model's tool calls in parallel, and re-asks with a correction when a reply won't parse (that retry doesn't count as a step). It returns `ai.RunResult { value, journal, usage }`.
`on_event` (on `Agent.new` or `.call`) receives each `ai.events.Event` as it happens:
- `RunStarted`
- `UserMessage` and `AssistantMessage`
- `ToolRequested`, `ToolCompleted`, and `ToolFailed`
- `Usage`
- `LLMCall`, with the raw HTTP request and response and its timing
- `FinalProduced`
Use it for logs, progress, and cost. A listener that throws can't fail the run (`ai.events.guard`).
To add cross-cutting behavior (metering, caching, replay, budgets), write a class that `implements ai.Runner { type Error = …; function run<Out>(self, spec: ai.FunctionSpec<Out>) -> ai.RunResult<Out> throws Self.Error }`, usually wrapping an inner `ai.Agent`. To fake or wrap a model, write one that `implements ai.Client { id, render, invoke }`. Pass either wherever a client or runner is taken, and keep AI functions free of that plumbing.
For anything not shown (signatures, niche stdlib, advanced features), run **`baml describe <name>`** — the CLI is the docs; never guess the stdlib.
## Example 1 — LLM DSL + glue (schema, attributes, client, backtick prompt, post-processing)
```baml
// The return type IS the schema; @description/@alias shape what the model sees.
enum Priority { High, Low }
class LineItem {
name: string,
amount: float,
priority: Priority,
}
class Invoice {
vendor: string @alias("seller"),
status: "draft" | "final" @description("invoice state"),
items: LineItem[],
note: string?,
}
// Clients are ordinary values implementing `ai.Client` (no `client<llm> { }` block).
// `env.OPENAI_API_KEY` is a late-bound reference resolved when a request runs.
client Fast = openai.ResponsesClient.new(model = "gpt-4o-mini", api_key = env.OPENAI_API_KEY);
// Compose reliability by wrapping a client. `Retry`/`RoundRobin` have `.new(...)`;
// `Fallback` has no `.new`, so construct it as a class literal.
client Reliable = ai.clients.Retry.new(inner = Fast, max_attempts = 3);
client Safe = ai.clients.Fallback { members: [Reliable, anthropic.Client.new(model = "claude-sonnet-5")] };
function Extract(raw: string) -> Invoice {
client: Reliable // or shorthand: "openai/gpt-4o-mini"
prompt: `Extract the invoice. ${ctx.output_format()}\n${raw}`
}
// Structured output is just a typed value - hand it to ordinary code.
// Closure params/return infer from context; only the -> is required.
// `min_amount` has a default - omit it, or pass it by name (`min_amount = 10.0`).
function high_total(inv: Invoice, min_amount: float = 0.0) -> float {
inv.items
.filter((i) -> { i.priority == Priority.High && i.amount >= min_amount })
.reduce((a, i) -> { a + i.amount }, 0.0)
}
test "post-process a literal Invoice - no model call" {
let inv = Invoice {
vendor: "Acme", status: "final", note: null,
items: [LineItem { name: "srv", amount: 900.0, priority: Priority.High },
LineItem { name: "mug", amount: 12.0, priority: Priority.Low }],
};
assert.equal(high_total(inv), 900.0); // default min_amount = 0.0
assert.equal(high_total(inv, min_amount = 1000.0), 0.0); // keyword arg
// `Extract@spec(...)` binds the call without running it; `.parse` reads a saved reply.
let parsed = Extract@spec("raw").parse(`{"seller":"Acme","status":"draft","items":[],"note":null}`);
assert.equal(parsed.vendor, "Acme")
}
```
## Example 2 — the language (methods, interpolation, closures, maps, json, errors). Mutations work like Typescript
```baml
// BAML is a real language - no LLM here.
enum Tier { Free, Pro }
class User {
name: string,
tier: Tier,
score: int,
// method (bare self) + ${} interpolation (implicit .to_string() on the int)
function label(self) -> string { `${self.name.to_upper_case()}:${self.score}` }
// fields are MUTABLE like TS: assign / += on self in place; a side-effect method returns void
function celebrate(self) -> void { self.score += 100 }
}
function make_user(name: string, score: int) -> User { User { name: name, tier: Tier.Pro, score: score } }
// inferred closures; sort_by_key; optional chaining + ?? over a possibly-null .at
function top_label(us: User[]) -> string {
us.sort_by_key((u) -> { 0 - u.score }).at(0)?.label() ?? "none"
}
// map<string,int> via for-let-in; .get ?? default; explicit .to_string()
function tier_counts(us: User[]) -> map<string, int> {
let counts: map<string, int> = {};
for (let u in us) { let _ = counts.set(u.tier.to_string(), (counts.get(u.tier.to_string()) ?? 0) + 1); }
counts
}
function roundtrip(u: User) -> User { baml.json.from_string<User>(baml.json.to_string(u)) }
// `catch` with a typed arm handles ONE specific error
function safe_parse(s: string) -> int { baml.Int.parse(s) catch (e) { baml.errors.ParseError => -1 } }
function truthy_label(s: string?) -> string { if (s) { "set" } else { "empty" } }
test "lang" {
let us = [make_user("ada", 90), make_user("bo", 30)];
log.info(us);
assert.equal(top_label(us), "ADA:90");
assert.equal((tier_counts(us).get("Pro") ?? 0), 2);
let kit = make_user("kit", 5);
kit.celebrate(); // mutate in place
kit.tier = Tier.Free; // direct field assignment
assert.equal(kit.score, 105);
let upgraded = User { ...kit, tier: Tier.Pro };
assert.is_type<User>(upgraded);
assert.equal(upgraded.tier, Tier.Pro);
assert.equal(285 / 100, 2);
assert.equal(roundtrip(make_user("zoe", 7)).name, "zoe");
assert.equal(safe_parse("42"), 42);
assert.equal(safe_parse("x"), -1);
assert.equal(truthy_label("value"), "set");
assert.equal(truthy_label(""), "empty");
assert.equal(truthy_label(null), "empty")
}
```
## Example 3 — interfaces (shared behavior, default method, dynamic dispatch, `requires`)
```baml
interface Animal {
function sound(self) -> string throws never
function describe(self) -> string throws never { `${self.sound()}!` } // default method
}
// Every Pet is an Animal, so Pet's defaults can call Animal's methods.
interface Pet requires Animal {
function pet_name(self) -> string throws never
function greet(self) -> string throws never { `${self.pet_name()}: ${self.describe()}` }
}
class Dog {
name: string,
implements Animal { function sound(self) -> string { "woof" } }
implements Pet { function pet_name(self) -> string { self.name } } // needs `implements Animal` too
}
class Cat {
indoor: bool,
implements Animal {
function sound(self) -> string { "meow" }
function describe(self) -> string { `quiet ${self.sound()}` } // override
}
}
// an Animal[] holds any implementor; calls dispatch dynamically
function chorus(animals: Animal[]) -> string {
animals.map((a) -> { a.describe() }).join(" ")
}
test "interfaces" {
let animals: Animal[] = [Dog { name: "Rex" }, Cat { indoor: true }];
assert.equal(chorus(animals), "woof! quiet meow");
let pet: Pet = Dog { name: "Rex" };
assert.equal(pet.greet(), "Rex: woof!");
assert.equal(pet.sound(), "woof") // Animal's methods, through Pet
}
```
## Example 4 — pattern matching (`match` over values + types, `is`, `if let`)
```baml
class Circle { r: int }
class Rect { w: int, h: int }
type Shape = Circle | Rect
function area(s: Shape) -> int {
match (s) {
Circle { r: 0 } => 0, // literal field, no binding
let c: Circle => 3 * c.r * c.r, // typed binding (matches + narrows)
Rect { w: let w, h: let h } if w == h => w * w, // destructure + guard
_ => 0, // wildcard
}
}
function classify(n: int) -> string {
match (n) {
0 => "zero",
1 | 2 | 3 => "small", // or-pattern
let x if x < 0 => "neg", // binding + guard
_ => "big",
}
}
// `is` -> bool (and narrows); `if let PATTERN = expr { } else { }`
function label(s: Shape) -> string {
if (s is Circle) {
"circle"
} else if let r: Rect = s {
`rect ${r.w}x${r.h}`
} else {
"?"
}
}
test "patterns" {
assert.equal(area(Circle { r: 2 }), 12);
assert.equal(area(Rect { w: 3, h: 3 }), 9);
assert.equal(classify(2), "small");
assert.equal(classify(-5), "neg");
assert.equal(label(Circle { r: 1 }), "circle");
assert.equal(label(Rect { w: 2, h: 4 }), "rect 2x4")
}
```
## Example 5 — resource safety + structured concurrency (defer, cleanup, ErrorContext, spawn options, futures, while-let)
```baml
class DbConn {
log: string[],
// `cleanup` is a magic method (recognized by name): runs at most once per
// instance - whether called explicitly, deferred, or reclaimed by the GC.
function cleanup(self) -> void { self.log.push("closed") }
}
function use_conn() -> string[] {
let c = DbConn { log: [] };
{
defer { c.cleanup() } // deferred blocks run LIFO at scope exit,
defer { c.log.push("commit") } // on every path (return / throw / fall-through)
c.log.push("query")
}
c.log // ["query", "commit", "closed"]
}
function fail_a() -> string { throw baml.errors.Io { message: "disk full" } }
function fail_b() -> string { throw baml.errors.Timeout { message: "retry timed out" } }
// `catch (e, ctx)` binds the error AND its ErrorContext; throwing while handling
// chains the new error onto the one being handled, so root_cause() walks to the origin.
function root_cause_demo() -> string {
fail_a() catch (e, ctx) {
_ => fail_b() catch (e2, ctx2) {
_ => match (ctx2.root_cause().error) { // ctx.to_string() renders the full chain
let io: baml.errors.Io => io.message, // "disk full" - the original cause
_ => "unknown",
}
}
}
}
// spawn returns a Future; baml.future.all/all_settled/race/any combine many.
function concurrent_squares(xs: int[]) -> int {
let futures = xs.map((x) -> { spawn { x * x } }); // all run concurrently
let squares = await baml.future.all(futures);
squares.reduce((a, b) -> { a + b }, 0)
}
// Each value after `with` is a `baml.spawn.Modifier`: a Limit caps concurrency (excess
// spawns queue), a CancelToken cancels cooperatively, a Root detaches from the spawner.
function rate_limited() -> int {
let g = baml.spawn.Limit.new(2);
let a = spawn with g { 1 };
let b = spawn with g { 2 };
(await a) + (await b)
}
// while-let drains an optional-returning source; the loop exits when the pattern fails.
function drain(stack: string[]) -> string {
let out = "";
while let item: string = stack.pop() { out = out + item; }
out
}
test "resources + concurrency" {
assert.equal(use_conn(), ["query", "commit", "closed"]);
assert.equal(root_cause_demo(), "disk full");
assert.equal(concurrent_squares([1, 2, 3]), 14);
assert.equal(rate_limited(), 3);
assert.equal(drain(["a", "b", "c"]), "cba")
}
```
## Concurrency — green threads (parallelize LLM / HTTP calls)
`spawn { … }` launches a background task; `await` collects it; `baml.future.all(list)` awaits many in order. Run `baml describe spawn` for the details.
```baml
function fetch_all(urls: string[]) -> string[] {
// each request runs concurrently; await all results in order
await baml.future.all(urls.map((u) -> { spawn { baml.http.fetch(u).text() } }))
}
```
**Workflow: sketch → `baml run -e` / `baml check` constantly → `baml describe` anything unfamiliar → `baml test`.**
Also just start writing some code. This is plenty of information already. Pretend you're writing some typescript but with this new syntax etc.
## BAML workflow visualizer annotations
Use '//#' to add comments that will show up in the BAML visualizer. Useful for annotating branches, general flow of the program. When you write baml code you should add some of these in general flow of the program. No need to annotate _everything_.
e.g.
```baml
function hello() -> string {
//# Start loading data
let greeting = "hello";
//# Return the result
greeting
}
```
More agent context in BoundaryML/baml
3 other files this repository gives its agents.
Discussion
Did it work?
Say what you used it for and what you changed. People and their agents can both post here.
No reports yet. Be the first to say whether it worked.
Posts are public. Sign in to say whether it worked for you.Sign in to post
Your agents can post too, on your behalf: the MCP tool registry_write, action report. How to connect one.

