Back to Articles

BAML: The Type-Safe Compiler That Treats LLM Prompts as Functions

[ View on GitHub ]

BAML: The Type-Safe Compiler That Treats LLM Prompts as Functions

Hook

What if your LLM prompts had the same compile-time guarantees as the rest of your codebase? BAML is a Rust-powered compiler that generates type-safe clients from a prompt DSL—no runtime surprises, no JSON parsing errors.

Context

The explosion of LLM integrations created a new category of technical debt: brittle prompt engineering. Developers write prompts as strings, parse outputs with regex or manual JSON validation, and discover bugs only at runtime when a model returns unexpected formats. Tool-calling APIs promised structured outputs, but each provider implements them differently, and cutting-edge models like OpenAI’s O1 or DeepSeek R1 don’t support them at launch. Teams end up with fragile wrapper code, manual schema validation, and slow iteration cycles where testing a prompt change requires running the entire application.

BAML (Basically a Made-up Language) reframes this problem by treating prompts as first-class functions with type signatures. Instead of embedding prompts in Python strings or TypeScript templates, you define them in .baml files with explicit input parameters and return types. A Rust compiler then generates native client libraries for Python, TypeScript, Ruby, and Go (with additional languages supported via REST API) with full IDE support. The framework includes a VSCode extension with an integrated playground, letting you test prompt changes in seconds rather than minutes. Most critically, BAML’s Schema-Aligned Parsing (SAP) algorithm extracts structured outputs even when models don’t support native tool-calling—handling markdown-wrapped JSON, chain-of-thought reasoning, and other real-world LLM quirks.

Technical Insight

Build Time

Runtime

Parse & Validate

Generate

Provide IntelliSense

Runtime Call

Structured Request

Raw Response

Typed Objects

.baml Files

DSL Definitions

Rust Compiler

Core Engine

Type-Safe Clients

Python/TS/Ruby/etc

VSCode Extension

& Playground

Schema-Aligned Parser

SAP Algorithm

LLM Provider

OpenAI/Anthropic

System architecture — auto-generated

At its core, BAML introduces a schema-first architecture where prompts are compiled artifacts rather than runtime strings. You define functions in .baml files using a Rust-like syntax that specifies both the contract and implementation. Here’s a chat agent that returns one of two tool types:

function ChatAgent(message: Message[], tone: "happy" | "sad") -> StopTool | ReplyTool {
    client "openai/gpt-4o-mini"

    prompt #"
        Be a {{ tone }} bot.

        {{ ctx.output_format }}

        {% for m in message %}
        {{ _.role(m.role) }}
        {{ m.content }}
        {% endfor %}
    "#
}

class Message {
    role string
    content string
}

class ReplyTool {
  response string
}

class StopTool {
  action "stop" @description(#"
    when it might be a good time to end the conversation
  "#)
}

The Rust compiler generates a baml_client module that you import like any other dependency. In Python, calling this function looks native:

from baml_client import b
from baml_client.types import Message, StopTool

messages = [Message(role="assistant", content="How can I help?")]

while True:
  print(messages[-1].content)
  user_reply = input()
  messages.append(Message(role="user", content=user_reply))
  tool = b.ChatAgent(messages, "happy")
  if isinstance(tool, StopTool):
    print("Goodbye!")
    break
  else:
    messages.append(Message(role="assistant", content=tool.response))

Notice that isinstance(tool, StopTool) works because the generated client includes actual Python classes, not dictionaries. Your IDE autocompletes field names, type checkers catch mismatches, and refactoring tools work across the boundary between your code and LLM outputs.

The magic happens through Schema-Aligned Parsing (SAP), BAML’s algorithm for extracting structured data from freeform text. Unlike systems that require strict JSON conformance, SAP handles real-world LLM outputs: markdown code blocks wrapping JSON, chain-of-thought reasoning before the answer, or partial results during streaming. When DeepSeek R1 or OpenAI O1 launch without tool-calling support, BAML works on day one because it doesn’t rely on provider-specific APIs—it parses the text output directly while maintaining type safety.

Streaming support extends this type safety to partial results. Adding streaming requires just two lines:

stream = b.stream.ChatAgent(messages, "happy")
for tool in stream:
    if isinstance(tool, StopTool):
      # Handle partial stop tool
final = stream.get_final_response()

Each chunk in the stream has the same type structure as the final response, with fields marked as optional in a generated Partial type. This lets you build reactive UIs where progress bars update as nested objects populate, without manual state management.

The VSCode extension transforms the development loop. Instead of modifying a prompt, restarting your app, navigating to the right state, and checking logs, you edit the .baml file and click “Run” in the inline playground. The UI shows the compiled prompt with all template variables resolved, the raw API request including multi-modal assets, and the parsed output with type validation errors highlighted. Testing 240 prompt variations in 20 minutes becomes feasible when iteration time drops from 2 minutes to 5 seconds.

Model switching happens declaratively in the BAML file rather than application code. Changing from GPT-4 to O3-mini is a one-line diff:

function Extract() -> Resume {
+  client openai/o3-mini
  prompt #"
    ....
  "#
}

Retry policies, fallbacks, and round-robin strategies are also defined statically in .baml files, enabling A/B testing or reliability patterns without touching your Python or TypeScript codebase. The Client Registry feature allows runtime model selection when needed, but the default path keeps configuration close to the prompt definition.

Gotcha

The biggest constraint is IDE lock-in: BAML currently only supports VSCode, with JetBrains and Neovim listed as “coming soon.” If your team uses Vim, Emacs, or Sublime, you lose the playground and inline testing that make BAML compelling. The DSL itself adds cognitive overhead—your team must learn BAML’s syntax, templating engine, and compilation model. While the syntax resembles Rust and Jinja2, it’s still an abstraction layer that sits between your application and LLM APIs.

The ecosystem is young. Despite 7,814 GitHub stars, community resources appear limited compared to more established frameworks. If you need pre-built integrations beyond the core LLM providers, you may be building them yourself. The README shows detailed examples for Python and TypeScript, with Ruby and Go also supported through generated clients, while other languages are accessible via REST API—suggesting varying levels of first-class support across the language ecosystem. BAML also introduces build-time dependencies: the Rust compiler must run during CI/CD, and generated clients must stay in sync with .baml files. Teams without established code generation workflows may find this friction unfamiliar.

Verdict

Use BAML if you’re building production systems where LLM output reliability directly impacts user experience—think document extraction pipelines, multi-step agents, or structured data APIs. The type safety and IDE tooling shine when prompt changes are frequent and bugs are expensive. It’s especially valuable if you’re working across multiple languages (microservices in Python and TypeScript, for example) and need consistent structured output handling without duplicating parsing logic. Skip BAML for quick prototypes, proof-of-concepts, or exploratory projects where flexibility matters more than reliability. If you’re already deep into another framework’s ecosystem or uncomfortable introducing a new DSL and build step, the migration cost may outweigh benefits. Also skip if your team doesn’t use VSCode—without the playground, you lose a significant portion of BAML’s value proposition.

// ADD TO YOUR README
[![Featured on Starlog](https://starlog.is/api/badge/ai-dev-tools/boundaryml-baml.svg)](https://starlog.is/api/badge-click/ai-dev-tools/boundaryml-baml)