BAML
QuickstartBlogPodcastTeam
DiscordGitHub8,423Learn BAML
Learn BAMLQuickstartBlogPodcastTeam
DiscordGitHub8,423
Release1 day ago9 min read

BAML 0.19.0

Structured agent journals, typed task outcomes, multi-project editing, new float helpers, and migrations for streaming, comparison, and reflection.

Thanks to everyone who contributed!

Special thanks to Ritz for contributing the float helpers and developer setup updates, and to Ben for filing the Rust generator and Linux installer reports that informed this release.

This canary updates the language, SDKs, editor integration, and developer documentation since 0.18.0. Read the breaking changes before upgrading. Update your toolchain and bridge packages together, run baml generate, and rebuild applications and packed executables. Reinstall the matching agent skill with baml agent install and update the editor integration with baml ide install.

Features

Multiple projects in one editor workspace

VS Code can work with multiple independent BAML project roots in one language-server session. The playground tracks the selected project. Projects can share a package name without being merged into one editor project. This does not enable arbitrary user-package dependencies or remove runtime package-name restrictions. (#4804)

Go to Definition now opens the compiler's embedded standard-library source in read-only documents in VS Code and PromptFiddle. Hover and further navigation work there too. Run baml ide install to update the extension; installation no longer extracts a separate copy of the standard library to disk. (#4777)

Toolchain now embeds the agent skill

baml agent install now tells your agent to always use the version of the BAML skill embedded in your toolchain. This allows us to keep the skill version in sync with the BAML version you're using! (#4625, #4723)

baml agent install

It will also proactively notify you if your skill version doesn't match; you can control this behavior using --agent-skill-check auto|require|warn|off or BAML_AGENT_SKILL_CHECK.

Float math helpers

Floats gain exp, ln, log2, log10, cbrt, and signum. New constants are available through float.max_finite(), float.min_finite(), and float.epsilon(). min_finite() is the most negative finite value. signum() returns either −1.0 or +1.0 and returns +1.0 for NaN. (#4751)

function math_examples() -> float[] {
    [
        (2.0).exp(),
        float.e().ln(),
        (8.0).log2(),
        (1000.0).log10(),
        (-8.0).cbrt(),
        (-2.0).signum(),
        float.max_finite(),
        float.min_finite(),
        float.epsilon(),
    ]
}

Structured prompts and dynamic values in SDKs

SDKs can retain prompt roles and media instead of reducing a prompt to plain text. Runtime-created BAML class and enum values keep their identity when passed through the host bridge, including streaming partials. Python supports both synchronous and asynchronous stream iteration. (#4623)

For this BAML function:

class Receipt { total int }

function Extract(text: string) -> Receipt {
    client: "openai/gpt-5.6-luna"
    prompt: `${text} ${ctx.output_format()}`
}

The generated Python spec binding lets you inspect a prompt without making an LLM call:

from baml_sdk import Extract_spec

spec = Extract_spec("Total: 42")
prompt = spec.prompt()
print(prompt.text())
print(prompt.messages())

Use the generated stream binding for streaming:

from baml_sdk import Extract_stream_async

async def stream_receipt(text: str):
    stream = await Extract_stream_async(text)
    async for partial in stream:
        print(partial)
    return await stream.final_async()

If a streamed class was created dynamically in BAML, its partial values cross the bridge as baml_bridge.BamlRuntimeValue. Keep the handle when passing a value back to BAML; use value.to_data() when you need its plain-data representation. The migration below lists the final binding names.

WebSocket upgrades in HTTP servers

baml.http.Server.serve accepts a websocket callback. Return a baml.http.WsAccept callback to accept the connection, or an HTTP response to reject it. (#4496)

function handleSocket(req: baml.http.Request, socket: baml.ws.WebSocket) -> void throws never {
    while (true) {
        let frame = socket.next() catch (_) {
            baml.errors.Io => { break; },
        };
        match (frame) {
            baml.ws.CloseEvent => { break; },
            let data: string | uint8array => {
                socket.send(data) catch (_) {
                    baml.errors.Io => { break; },
                };
            },
        }
    }
}

function serve_echo() -> never {
    let server = baml.http.Server.bind("127.0.0.1:8080");
    let websocket = (req) -> {
        (socket) -> { handleSocket(req, socket) }
    };
    server.serve(
        (req) -> { baml.http.Response.new(200, {}, "ok".to_utf8()) },
        websocket = websocket,
    )
}

This supports HTTP/1.1 WebSocket handshakes. WebSocket subprotocols, extensions, and HTTP/2 upgrades are not negotiated. The default callback rejects upgrades with HTTP 501.

Smaller optimized bytecode

The compiler combines short-circuit branches and removes unnecessary temporary stores while preserving evaluation order. The PR's final analysis reported: (#4759)

MeasurementBaseline 048bf98acOptimized f793a6484Reduction
Displayed O2 bytecode instructions99,86093,9845,876 (5.9%)

The measurement covers the pre-existing bytecode snapshot inventory, including test and standard-library scaffolding. It is not weighted by execution frequency. The PR records Rust 1.97.1 for local validation but does not identify a separate Cargo build profile for the counting script. This is a static instruction-count result, not a throughput or end-to-end latency benchmark.

The optimization remains in this release. Later changes alter the corpus and call layout, so the PR's percentage is not a measurement of the entire 0.18.0 → 0.19.0 release.

Breaking changes

Function specs, streaming, and generated bindings

Use Fn@spec(...) to obtain an ai.FunctionSpec<Out> and Fn@stream(...) to stream a BAML LLM function. A spec supports call, parse, prompt, and build_request. It has one type argument. There is no spec.stream() method. (#4623)

Before, a request override used override_client:

function request(spec: ai.FunctionSpec<string>, client: ai.Client) -> baml.http.Request {
    spec.build_request(override_client = client)
}

After, use client:

function request(spec: ai.FunctionSpec<string>, client: ai.Client) -> baml.http.Request {
    spec.build_request(client = client)
}

The final BAML projection API is:

function Answer(question: string) -> string {
    client: "openai/gpt-5.6-luna"
    prompt: `${question}`
}

function preview() -> string {
    let spec = Answer@spec("What is BAML?");
    spec.prompt().text()
}

function answer_stream() -> ai.stream.Stream<string | null, string> {
    Answer@stream("What is BAML?")
}

Regenerate every host client with baml generate. Replace generated render-prompt, build-request, and parse companion calls with operations on the generated spec binding. Python uses Fn_spec / Fn_spec_async and Fn_stream / Fn_stream_async. TypeScript uses Fn_spec / Fn_spec_async and retains Fn$stream / Fn$stream_async for streaming. Generated names can receive a suffix when they collide with an authored function; use the regenerated exports. TypeScript partial model names such as Receipt$stream retain their $stream suffix. Do not rewrite those type names to @stream.

For the Extract function above, replace the old generated Python prompt helper:

from baml_sdk import Extract__render_prompt

prompt = Extract__render_prompt("Total: 42")

With the spec binding:

from baml_sdk import Extract_spec

prompt = Extract_spec("Total: 42").prompt()

These names use naming_convention = "preserve-case". Re-generated Python partial models remain under baml_sdk.stream_types.

Shorter standard-library names

Update annotations, constructors, catch patterns, and generated imports to the new names. Regenerate host clients to pick up the same changes. (#4725)

BeforeAfter
ai.ClientSelector, ai.clients.ClientSelectorai.Selector, ai.clients.Selector
ai.mcp.McpConnectionai.mcp.Connection
ai.stream.StreamEventai.stream.Event
anthropic.AnthropicClientanthropic.Client
google.GoogleClientgoogle.GeminiClient
baml.csv.CsvError, CsvPosition, CsvValuebaml.csv.Error, Position, Value
baml.csv.CsvRecord, CsvReader, CsvRows<T>, CsvWriterbaml.csv.Record, Reader, Rows<T>, Writer
baml.errors.ErrorContextbaml.errors.Context
baml.host.HostValuebaml.host.Value
baml.spawn.SpawnParamsbaml.spawn.Params
baml.json.JsonDecodeError, JsonParseError, JsonPathError, JsonSerializationErrorbaml.json.DecodeError, ParseError, PathError, SerializationError
baml.toml.TomlParseErrorbaml.toml.ParseError
baml.yaml.YamlParseErrorbaml.yaml.ParseError

Before:

function parse_json(text: string) -> baml.json.json throws baml.json.JsonParseError {
    baml.json.parse(text)
}

After:

function parse_json(text: string) -> baml.json.json throws baml.json.ParseError {
    baml.json.parse(text)
}

The old baml.errors.NotImplemented and DevOther types are removed; use an application-defined error for a recoverable application failure.

Implementation helpers are private: assert.format_operand; CSV CsvNeedData, CsvSkip, and CsvHeaders; SAP ParseCache and NoYield; and the testing scheduler, selection, aggregation, and bookkeeping helpers. Use the public CSV reader/iterator API, ai.stream.Stream, and baml test or documented test runners instead of importing these helpers. Do not migrate to underscore-prefixed implementation names.

Schema-Aligned Parsing error type

baml.sap.parse and parse_type now throw baml.errors.ParseError instead of baml.errors.LlmClient. Update catch patterns and explicit throws annotations for these parsing calls. (#4623)

Before:

function parse_answer(text: string) -> int throws baml.errors.LlmClient {
    baml.sap.parse<int>(text)
}

After:

function parse_answer(text: string) -> int throws baml.errors.ParseError {
    baml.sap.parse<int>(text)
}

A dedicated assertion panic type

Assertion failures now use baml.panics.AssertionFailed instead of the general baml.panics.UserPanic. Panics are not inferred as typed errors in a throws clause. throws never does not rule out a panic. Where intentional recovery is required, use panic-aware handling rather than relying on an ordinary catch-all for typed errors. (#4725)

Before:

function assertion_result() -> bool {
    assert.is_true(false) catch_all_panics (_) {
        baml.panics.UserPanic => { return false; },
    };
    true
}

After:

function assertion_result() -> bool {
    assert.is_true(false) catch_all_panics (_) {
        baml.panics.AssertionFailed => { return false; },
    };
    true
}

Comparison is total and reflexive

baml.ops.Compare now requires cmp(self, other) -> baml.ops.Ordering throws never, with Less, Equal, and Greater variants. lt, le, gt, ge, min, max, and clamp derive from that ordering. Implement baml.ops.Equals consistently with cmp. (#4739)

baml.Comparable, its compare method, and CompareError are removed. baml.Sortable remains, but its SortError associated type is removed and sort() is infallible in the typed-error sense. For a fallible comparison, use sort_by with a callback returning Ordering and declaring its error type.

Before:

function sorted(values: int[]) -> int[] {
    values.sort_by((a: int, b: int) -> int throws never { a.compare(b) })
}

After:

function sorted(values: int[]) -> int[] {
    values.sort_by((a: int, b: int) -> baml.ops.Ordering throws never { a.cmp(b) })
}

A custom natural ordering now looks like this:

class Score {
    value int
    implements baml.ops.Equals {
        function eq(self, other: Self) -> bool throws never { self.value == other.value }
    }
    implements baml.ops.Compare {
        function cmp(self, other: Self) -> baml.ops.Ordering throws never {
            self.value.cmp(other.value)
        }
    }
}

Float comparison departs from IEEE behavior: NaN equals itself and sorts above all numbers. Negative and positive zero compare equal. Replace NaN checks such as x != x with x.is_nan(). Custom equality must be reflexive; comparing a value with itself may bypass its eq implementation.

Narrow unrelated unions before member access

Classes sharing a field or method name no longer automatically provide that member on their union. Narrow the value first, or implement one shared declaring interface on all union arms. (#4611)

Before:

class Cat { name string }
class Dog { name string }
function pet_name(pet: Cat | Dog) -> string { pet.name }

After:

class Cat { name string }
class Dog { name string }
function pet_name(pet: Cat | Dog) -> string {
    match (pet) {
        let cat: Cat => cat.name,
        let dog: Dog => dog.name,
    }
}

Runtime type bindings are local and rigid

unreflect is valid only in a local type T = unreflect(type_value) statement. Replace inline uses with a local binding. The type is rigid like a generic parameter and must stay within its lexical scope. It no longer suppresses static type checks. (#4834)

Before:

function parse_value(t: reflect.Type, text: string) -> unknown {
    baml.json.from_string<unreflect(t)>(text)
}

After:

function parse_value(t: reflect.Type, text: string) -> unknown {
    type T = unreflect(t);
    baml.json.from_string<T>(text)
}

For an unknown value, check membership before treating it as T:

function matches_type(t: reflect.Type, value: unknown) -> bool {
    type T = unreflect(t);
    if let checked: T = value {
        true
    } else {
        false
    }
}

Do not return or store values whose inferred type leaks the local binding. Give the enclosing value an explicit valid supertype when necessary. Inferred throws types are widened to a supertype when a local type cannot escape.

all_settled replaces all_complete

baml.future.all_complete is removed. all_settled returns one Success<T>, Failure<E>, or Panicked outcome per input, in input order. Its returned future has typed error never. An input's failure or panic does not cancel other inputs. Input cancellation becomes a Panicked outcome; cancelling the collector stops the wait. (#4816)

Before, callers expected an array of values or a thrown error:

function collect(tasks: baml.future.Future<int, string>[]) -> int[] {
    await baml.future.all_complete(tasks)
}

After, inspect every outcome:

function collect(tasks: baml.future.Future<int, string>[]) -> string[] {
    let outcomes = await baml.future.all_settled(tasks);
    outcomes.map((outcome) -> string {
        match (outcome) {
            let success: baml.future.Success<int> => `value: ${success.value}`,
            let failure: baml.future.Failure<string> => `error: ${failure.error}`,
            let panicked: baml.future.Panicked => `panic: ${panicked.context.to_string()}`,
        }
    })
}

If you want values with fail-fast typed-error propagation and cancellation of remaining inputs, use baml.future.all instead. It has different failure behavior from collecting all outcomes.

Agent journals use structured content blocks

UserMessage.content and ToolCompleted.content are ai.content.Block[], containing Text or Media. The old model-turn Block union is now ai.content.ModelBlock, which also includes reasoning and tool use. Use constructors and text() for the readable projection. (#4807)

Before:

function journal_text() -> string {
    let user = ai.events.UserMessage { content: "Continue" };
    let result = ai.events.ToolCompleted { id: "lookup", output: "found" };
    user.content + result.output
}

After:

function journal_text() -> string {
    let user = ai.events.UserMessage.new("Continue");
    let result = ai.events.ToolCompleted.new("lookup", "found");
    user.text() + result.text()
}

function screenshot_turn(screenshot: image) -> ai.events.UserMessage {
    ai.events.UserMessage.of(["What changed?", screenshot])
}

Use ToolCompleted.of(id, items) for media-bearing tool results. UserMessage.of(items, metadata = ...) also carries per-message provider directives. Pass media values directly instead of using ai.internal.media_part.

Provider support differs. Gemini/Vertex accepts all four media kinds in user turns and tool results. Anthropic accepts images and PDFs. OpenAI Responses accepts user images, audio, and PDFs; tool audio is moved to a following user turn. OpenAI Chat moves tool media to a following user turn. Bedrock accepts all four user-media kinds and moves tool audio to a following user turn. Unsupported kinds are rejected. Claude Code renders text placeholders. Image-generation clients have narrower input support. Review your provider's accepted media before changing a tool's output.

WebSocket frames and closure

Rename baml.ws.WsStream to baml.ws.WebSocket. send now accepts string | uint8array and returns void. next returns string | uint8array | CloseEvent. close now requires a code and reason, returns void, and can throw InvalidArgument for an invalid code or oversized reason. (#4496)

Before:

function receive(socket: baml.ws.WsStream) -> string {
    let frame = socket.next();
    socket.close();
    frame ?? "closed"
}

After:

function receive(socket: baml.ws.WebSocket) -> string {
    let frame = socket.next();
    match (frame) {
        let closed: baml.ws.CloseEvent => `closed: ${closed.code} ${closed.reason}`,
        let text: string => {
            socket.close(1000, "done");
            text
        },
        let bytes: uint8array => {
            socket.close(1000, "done");
            "binary frame"
        },
    }
}

Stop receive loops on CloseEvent, not null. close queues the handshake; it does not wait for the peer's closing frame.

Changelog feed consumers

The old /api/changelog-feed/entries, /api/changelog-feed/entries/[version], and changelog Markdown representation are removed. There is no replacement JSON feed. Update bookmarks to the changelog; migrate feed integrations to your own release-data source or the authored release posts. (#4742, #4743)

Bug fixes

  • Matching an optional enum containing null no longer crashes with a VM type error. Matching a union of different enums no longer selects an arm just because their variant numbers coincide; unmatched values correctly reach the wildcard arm. (#4623)
  • Rust generation no longer skips otherwise representable LLM functions because ai.errors.Failure is an open interface in the throws contract. Open-interface errors use Error::Runtime; representable concrete errors keep their typed Rust arms. Run baml generate to obtain direct, spec, and stream bindings. Non-identifier string-literal unions such as "graph.query" remain unsupported and can still cause skipped functions. (#4623)
  • Interface default methods, captured interface receivers, and implementations with extra optional arguments dispatch correctly. This includes bound methods passed to higher-order functions. (#4630, #4820, #4808)
  • Lambdas infer types from callable union arms. Early returns use the closure's return type. Lowering preserves solved annotation holes such as () -> _ { 1 }. (#4646, #4721, #4799)
  • Invalid interface signatures and type bounds produce diagnostics instead of compiler panics. Associated-type pins are honored. Required interface signatures must use complete types; replace inference holes in those signatures. (#4686, #4720)
  • Builtin media carrier classes match their primitive aliases, so baml.media.Image accepts an image. Invalid arguments such as image<string> now report E0171. Remove arguments from non-generic builtins and provide both arguments for map<K, V>. (#4806)
  • Linux bootstrap compatibility requires a separate wrapper release containing the installer fix. The installer source selects GNU or musl wrappers, with GNU builds targeting glibc 2.17 on x86_64 and 2.28 on ARM64. After that wrapper is published, re-run the official installer to replace an incompatible wrapper. A language-toolchain update alone does not deliver this fix. (#4632)
  • Generated web SDKs initialize under Cloudflare workerd without requesting unavailable startup-time entropy. Update @boundaryml/baml-bridge-web and regenerate the typescript/web client. Trace identities in this environment use zero random bytes, so separate isolates can collide. (#4692)
  • Runtime-compiled packages can call methods on mounted classes and implement mounted interfaces. Reflection type values dispatch through interfaces correctly. (#4714)
  • Dynamic reflection rejects overlapping interface witnesses and keeps their implementation rules alive across garbage collection. Remove overlapping registrations; a rejected batch can be retried. (#4808)
  • Native callbacks honor explicit throws never. Handle host errors inside such callbacks or declare the appropriate error contract. Regenerate bytecode, update matching bridges, and repack executables after this release's artifact changes. (#4808)
  • Unused expressions retain operations that can throw or panic. Debug builds at O0 retain overwritten user variables. (#4759)
  • map {} evaluates to an empty map. throw [] uses a declared array error type to infer its element type. (#4800)
  • Runtime type bindings with overlapping lifetimes use distinct slots. Escaping inferred types produce diagnostics. (#4834)
  • Awaited panic contexts preserve the producer's trace. (#4816)
  • baml fmt formats interface declarations and preserves comments. Required methods and associated types receive semicolons; those semicolons remain optional in source. Run baml fmt to normalize existing files. (#4781)
  • Inline code and release-post code blocks display consistently. Production book builds check their linked stylesheets to prevent stale annotation and tab styling. (#4745, #4788, #4836)
  • Developer-portal changelog URLs redirect to the product release notes. (#4783)

Boundary

Basically a made up language.

  • Company
  • About Us
  • Why BAML?
  • Privacy Policy
  • Terms of Service
  • Resources
  • Docs
  • Jobs
  • Blog
  • Pricing
  • Social
  • GitHub
  • Twitter
  • Discord
  • LinkedIn
  • YouTube