> your AI agent picks dependencies from memory; give it dated facts — try starlog.dev ↗ vet your agent's deps ↗ vibe-coding is fine. vibe-importing isn’t. — try starlog.dev ↗ vibe-importing isn’t fine ↗ your agent has never seen your private packages — try starlog.dev ↗ facts for private packages ↗ a linter for the dependencies your AI agent picks — try starlog.dev ↗ a linter for agent deps ↗

Back to Articles

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

[ View on GitHub ]

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

Hook

What if your LLM prompts could fail at compile-time instead of production? BAML treats prompts as strongly-typed functions that generate native client libraries, bringing the rigor of traditional software engineering to the chaos of prompt engineering.

Context

The LLM integration problem has become a maintenance nightmare. Teams start with hardcoded prompts scattered across their codebase, then graduate to template libraries like LangChain, only to discover that runtime type errors, inconsistent outputs, and model-specific quirks still plague production systems. When a model returns markdown-wrapped JSON instead of pure JSON, or when you need to switch from GPT-4 to Claude mid-project, you're rewriting validation logic and error handlers throughout your application.

The core issue is that prompts exist as strings in most frameworks—there's no contract between what you ask for and what you get back. You can request a structured user profile but receive free-form text. You can specify required fields but get partial responses. Traditional approaches handle this with runtime parsing and prayer, leading to brittle code that breaks silently when models misbehave. BAML emerged from BoundaryML's work building production LLM applications where reliability and type safety weren't optional luxuries but hard requirements for shipping features that wouldn't explode under real user traffic.

Technical Insight

BAML's architecture centers on a Rust-based compiler that transforms a domain-specific language into type-safe client libraries for Python, TypeScript, Ruby, Java, C#, Go, and Rust. You define prompts as functions with explicit input and output schemas, and the compiler generates native code with full IDE autocomplete, type checking, and error handling.

Here's what a BAML function looks like:

class Resume {
  name string
  skills string[]
  experience WorkExperience[]
}

class WorkExperience {
  company string
  role string
  years int
}

function ExtractResume(resume_text: string) -> Resume {
  client GPT4
  prompt #"
    Extract structured information from this resume:
    
    {{ resume_text }}
    
    Return a JSON object with name, skills array, and experience array.
  "#
}

The compiler generates a client in your target language that looks like native code. In TypeScript, you'd use it like this:

import { b } from './baml_client';

const resume = await b.ExtractResume({
  resume_text: userUploadedText
});

// TypeScript knows resume.skills is string[]
// and resume.experience[0].company is string
console.log(resume.skills.join(', '));

The magic happens in BAML's Schema-Aligned Parsing (SAP) algorithm, which bridges the gap between what you ask for and what models actually return. When GPT-4 wraps your JSON in markdown code fences, or Claude adds chain-of-thought reasoning before the structured output, SAP extracts the data you need without failing. This works even with models that lack native tool-calling APIs—the algorithm parses free-form text intelligently, making it work day-one with new releases like DeepSeek-R1 or O3-mini.

The multi-provider architecture is particularly clever. You can define fallback strategies and retry policies declaratively:

client<llm> GPT4Turbo {
  provider openai
  options {
    model gpt-4-turbo-preview
    temperature 0.2
  }
}

client<llm> ClaudeFallback {
  provider anthropic
  options {
    model claude-3-opus-20240229
  }
}

client<llm> Primary {
  strategy [
    { strategy fallback clients [GPT4Turbo, ClaudeFallback] }
  ]
}

function AnalyzeCode(code: string) -> Analysis {
  client Primary
  prompt #"Analyze this code: {{ code }}"#
}

Now AnalyzeCode automatically retries with Claude if GPT-4 fails, without touching your application code. This declarative approach means prompt engineers can modify routing logic without coordinating with backend developers.

The VS Code extension adds an interactive playground where you can test functions with real API calls, see live output parsing, and iterate on prompts with sub-second feedback loops. Instead of the traditional cycle of modifying code, restarting servers, and triggering test cases, you edit BAML files and immediately see results. This tight feedback loop is why teams report 10-50x faster iteration speeds—you're not context-switching between editor, terminal, and application.

Streaming support is built into the type system. When you define a function with streaming enabled, BAML generates clients that emit partial results as the model generates tokens. In a React application, you can render incremental updates without manual parsing:

const stream = b.stream.ExtractResume({ resume_text });

for await (const partial of stream) {
  // partial.skills might be incomplete, but type-safe
  setDisplayedResume(partial);
}

This streaming-first design makes real-time UIs straightforward—the compiler handles the complexity of partial JSON parsing and type coercion at each chunk.

Gotcha

BAML requires adopting a compilation step into your build process, which adds toolchain complexity compared to inline prompting libraries. You need to run the BAML compiler before your application code can use the generated clients, which means configuring your CI/CD pipeline, IDE watchers, and local development environment. Teams accustomed to modifying prompts as strings in their Python or JavaScript files need to adjust to editing .baml files and waiting for compilation. While compilation is fast (typically milliseconds), it's still an extra step that can trip up newcomers or cause confusion when generated files aren't checked into version control.

The Schema-Aligned Parsing algorithm, despite its flexibility, introduces latency overhead compared to native tool-calling APIs when they work correctly. If you're using a model like GPT-4 with reliable function calling, SAP's post-hoc parsing of free-form text is solving a problem you don't have—and costing you extra milliseconds in response time. The tradeoff makes sense for multi-model strategies or models without native tools, but if you're committed to a single provider with solid structured output support, you're paying for flexibility you won't use. Additionally, while BAML supports seven languages, Python and TypeScript appear to have the most mature ecosystems and community examples. If you're building in Ruby or Java, expect to encounter fewer Stack Overflow answers and community templates.

Verdict

Use if: You're building production LLM applications where reliability trumps rapid prototyping, need to support multiple models or providers with fallback strategies, work in a polyglot codebase where type safety across languages matters, or have a team where prompt engineers and application developers need clear separation of concerns. BAML shines when prompt versioning, testing infrastructure, and compile-time guarantees justify the upfront tooling investment. Skip if: You're experimenting with quick prototypes or single-file scripts where inline prompting is faster, you're heavily invested in existing orchestration frameworks like LangChain with custom chains you can't easily migrate, or you're working solo on a small project where the overhead of learning a DSL and managing compilation outweighs the benefits. BAML is engineering infrastructure for teams shipping LLM features at scale, not a casual library for weekend hacks.