
Baml Codegen
- 1 installs
- 5 repo stars
- Updated June 5, 2026
- agentic-insights/foundry
baml-codegen is a Claude Code skill that generates type-safe BAML code for LLM extraction, classification, RAG, and agent workflows from natural-language requirements.
About
baml-codegen is a Claude Code skill that generates type-safe BAML code for LLM extraction, classification, RAG, and agent workflows. From natural-language requirements it produces complete .baml files with types, functions, clients, tests, and framework integrations for Python, TypeScript, Ruby and Go. It queries official BoundaryML repositories via MCP for current patterns and supports multimodal image and audio inputs.
- Generates type-safe BAML code for LLM extraction, classification, RAG and agents
- Emits complete .baml files with types, functions, clients, tests and retry policies
- Queries BoundaryML repositories via MCP for real-time patterns; works offline from cache
Baml Codegen by the numbers
- 1 all-time installs (skills.sh)
- Ranked #14,102 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 7, 2026 (Skillselion catalog sync)
baml-codegen capabilities & compatibility
requires an LLM provider API key (openai, anthropic, gemini, etc.) for the generated clients
- Capabilities
- code generation · llm extraction · classification · rag
- Works with
- openai · anthropic
- Use cases
- api development · orchestration · research
- Pricing
- Bring your own API key
What baml-codegen says it does
Generate type-safe LLM extraction code. Use when creating structured outputs, classification, RAG, or agent workflows.
**NEVER edit `baml_client/`** - 100% generated, overwritten on every `baml-cli generate`
**Transpiler Not Library** - Write `.baml` → generate native code (Python/TypeScript/Ruby/Go), no runtime dependency
npx skills add https://github.com/agentic-insights/foundry --skill baml-codegenAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 5 |
| Last updated | June 5, 2026 |
| Repository | agentic-insights/foundry ↗ |
What it does
Generate type-safe BAML code for LLM extraction, classification, RAG and agent workflows.
Who is it for?
Producing type-safe LLM extraction, classification or RAG code as complete .baml files with tests.
Skip if: Editing generated baml_client code, which the skill says is 100% generated and overwritten on every generate.
When should I use this skill?
The user is generating BAML code for type-safe LLM extraction, classification, RAG, or agent workflows.
What you get
The skill emits complete .baml files with types, functions, clients, tests and framework integrations.
- .baml files with types, functions and clients
- pytest or jest tests
- framework integration code
By the numbers
- supports Python, TypeScript, Ruby and Go targets
- claims 50-70% token optimization and 95%+ compilation success
Files
BAML Code Generation
Generate type-safe LLM extraction code. Use when creating structured outputs, classification, RAG, or agent workflows.
Golden Rules
- NEVER edit `baml_client/` - 100% generated, overwritten on every
baml-cli generate; checkbaml_src/generators.bamlforoutput_type(python, typescript, ruby, go) - ALWAYS edit `baml_src/` - Source of truth for all BAML code
- Run `baml-cli generate` after changes - Regenerates typed client code for target language
Philosophy (TL;DR)
- Schema Is The Prompt - Define data models first, compiler injects types
- Types Over Strings - Use enums/classes/unions, not string parsing
- Fuzzy Parsing Is BAML's Job - BAML extracts valid JSON from messy LLM output
- Transpiler Not Library - Write
.baml→ generate native code (Python/TypeScript/Ruby/Go), no runtime dependency - Test-Driven Prompting - Use VS Code playground or
baml-cli testto iterate
Workflow
Analyze → Pattern Match (MCP) → Validate → Generate → Test → Deliver
↓ [IF ERRORS] Error Recovery (MCP) → RetryBAML Syntax
| Element | Example |
|---|---|
| Class | class Invoice { total float @description("Amount") @assert(this > 0) @alias("amt") } |
| Enum | enum Category { Tech @alias("technology") @description("Tech sector"), Finance, Other } |
| Function | function Extract(text: string, img: image?) -> Invoice { client GPT5 prompt #"{{ text }} {{ img }} {{ ctx.output_format }}"# } |
| Client | client<llm> GPT5 { provider openai options { model gpt-5 } retry_policy Exponential } |
| Fallback | client<llm> Resilient { provider fallback options { strategy [FastModel, SlowModel] } } |
Types
- Primitives:
string,int,float,bool| Multimodal:image,audio - Containers:
Type[](array),Type?(optional),map<string, Type>(key-value) - Composite:
Type1 | Type2(union), nested classes - Annotations:
@description("..."),@assert(condition),@alias("json_name"),@check(name, condition)
Providers
openai, anthropic, gemini, vertex, bedrock, ollama + any OpenAI-compatible via openai-generic
Pattern Categories
| Pattern | Use Case | Model | Framework Markers |
|---|---|---|---|
| Extraction | Unstructured → structured | GPT-5 | fastapi, next.js |
| Classification | Categorization | GPT-5-mini | any |
| RAG | Answers with citations | GPT-5 | langgraph |
| Agents | Multi-step reasoning | GPT-5 | langgraph |
| Vision | Image/audio data extraction | GPT-5-Vision | multimodal |
Resilience
- retry_policy:
retry_policy Exp { max_retries 3 strategy { type exponential_backoff } } - fallback client: Chain models
[FastCheap, SlowReliable]for cost/reliability tradeoff
MCP Indicators
- Found patterns from baml-examples | Validated against BoundaryML/baml | Fixed errors using docs | MCP unavailable, using fallback
Output Artifacts
1. BAML Code - Complete .baml files (types, functions, clients, retry_policy) 2. Tests - pytest/Jest with 100% function coverage 3. Integration - Framework-specific client code (LangGraph nodes, FastAPI endpoints, Next.js API routes) 4. Metadata - Pattern used, token count, cost estimate
References
- providers.md - OpenAI, Anthropic, Google, Ollama, Azure, Bedrock, openai-generic
- types-and-schemas.md - Full type system, classes, enums, unions, map, image, audio
- validation.md - @assert, @check, @alias, block-level @@assert
- patterns.md - Pattern library with code examples
- philosophy.md - BAML principles, golden rules
- mcp-interface.md - Query workflow, caching
- languages-python.md - Python/Pydantic, async
- languages-typescript.md - TypeScript, React/Next.js
- frameworks-langgraph.md - LangGraph integration
Agent Templates
Templates for agent patterns (planning, execution, multi-agent, etc.)
Pattern Structure
Each agent template includes:
- Task and plan types
- State management structures
- Planning/execution functions
- Tool usage patterns
- Example test cases
Common Use Cases
- Planning agents
- Execution agents
- Multi-agent workflows
- Task automation
- Decision-making systems
Classification Templates
Templates for classification patterns (sentiment, intent, category, etc.)
Pattern Structure
Each classification template includes:
- Input types (usually text)
- Enum definitions for categories
- Classification function with confidence
- Reasoning capture
- Example test cases
Common Use Cases
- Sentiment analysis
- Intent detection
- Category assignment
- Priority classification
- Topic identification
// Example: Sentiment Classification Pattern
enum Sentiment {
Positive @description("Positive sentiment")
Negative @description("Negative sentiment")
Neutral @description("Neutral sentiment")
}
class SentimentResult {
sentiment Sentiment
confidence float @description("Confidence score 0-1")
reasoning string @description("Explanation for classification")
}
client FastModel {
provider "openai"
options {
model "gpt-4o-mini"
temperature 0.1
}
}
function ClassifySentiment(text: string) -> SentimentResult {
client FastModel
prompt #"
Classify the sentiment of the following text:
{{ text }}
Provide confidence score and reasoning.
{{ ctx.output_format }}
"#
}
// Example: Invoice Extraction Pattern
// This template extracts structured invoice data from images
class LineItem {
description string @description("Item description")
quantity int @description("Number of units")
unit_price float @description("Price per unit")
total float @description("Line item total")
}
class Invoice {
invoice_number string @description("Invoice ID")
date string @description("Invoice date (YYYY-MM-DD)")
vendor_name string
items LineItem[] @description("List of line items")
subtotal float
tax float
total float @description("Grand total")
}
client VisionModel {
provider "openai"
options {
model "gpt-4o"
temperature 0.0
}
}
function ExtractInvoice(invoice_image: image) -> Invoice {
client VisionModel
prompt #"
Extract all information from this invoice image.
{{ invoice_image }}
IMPORTANT:
- Validate that subtotal + tax = total
- Extract all line items accurately
- Use YYYY-MM-DD format for dates
{{ ctx.output_format }}
"#
}
Extraction Templates
Templates for data extraction patterns (invoice, resume, receipt, etc.)
Pattern Structure
Each extraction template includes:
- Input types (often image or text)
- Output class definitions (structured data)
- Extraction function with detailed prompt
- Validation rules
- Example test cases
Common Use Cases
- Invoice extraction
- Resume parsing
- Receipt processing
- Document analysis
- Form extraction
Integration Templates
Framework-specific integration code templates
Supported Frameworks
Python
- FastAPI
- Flask
- Django
TypeScript
- Next.js
- Express
- NestJS
Ruby
- Rails
- Sinatra
Java
- Spring Boot
Go
- Gin
- Echo
C#
- ASP.NET Core
Template Structure
Each integration template includes:
- API endpoint/route
- Request/response models
- Error handling
- Async patterns
- Configuration examples
RAG Templates
Templates for Retrieval-Augmented Generation patterns (search, citation, etc.)
Pattern Structure
Each RAG template includes:
- Document/context types
- Citation tracking structures
- Search functions with source attribution
- Confidence scoring
- Example test cases
Common Use Cases
- Citation-aware search
- Source-attributed responses
- Context-based generation
- Document retrieval
- Knowledge base querying
Advanced BAML Patterns
Complex extraction scenarios including hierarchical structures, dynamic types, tool calling, streaming, multimodal inputs, and runtime type modification.
Union Return Types for Tool Selection
Union types enable LLM-based function routing and tool selection by allowing functions to return one of several possible types.
Single Tool Selection
class SearchQuery {
query string
}
class WeatherRequest {
city string
units "celsius" | "fahrenheit"
}
class CalendarEvent {
title string
date string
}
// LLM determines which tool to use based on user input
function RouteRequest(input: string) -> SearchQuery | WeatherRequest | CalendarEvent {
client "openai/gpt-4o"
prompt #"
Determine what the user wants and extract the appropriate data.
{{ _.role("user") }}
{{ input }}
{{ ctx.output_format }}
"#
}Multiple Tool Selection
class GetWeather {
location string
units "celsius" | "fahrenheit"
}
class SearchWeb {
query string
max_results int
}
class Calculator {
expression string
}
// Returns array of tools for multi-step workflows
function SelectTools(query: string) -> (GetWeather | SearchWeb | Calculator)[] {
client "openai/gpt-4o"
prompt #"
Select ALL tools needed to answer: {{ query }}
{{ ctx.output_format }}
"#
}Using Tool Results in Code
Python:
from baml_client import b
from baml_client.types import GetWeather, SearchWeb, Calculator
result = b.RouteRequest("What's the weather in Seattle?")
if isinstance(result, GetWeather):
weather_data = fetch_weather(result.location, result.units)
elif isinstance(result, SearchWeb):
search_results = search(result.query, result.max_results)
elif isinstance(result, Calculator):
answer = evaluate(result.expression)TypeScript:
import { b } from './baml_client'
import type { GetWeather, SearchWeb, Calculator } from './baml_client/types'
const result = await b.RouteRequest("What's the weather in Seattle?")
if ('location' in result) {
// GetWeather
const weather = await fetchWeather(result.location, result.units)
} else if ('query' in result) {
// SearchWeb
const results = await search(result.query, result.max_results)
} else {
// Calculator
const answer = evaluate(result.expression)
}Chat History Pattern
The Message class pattern enables multi-turn conversational interfaces with proper role management.
Message Class Definition
class Message {
role "user" | "assistant"
content string
}
function Chat(messages: Message[]) -> string {
client "openai/gpt-4o"
prompt #"
{{ _.role("system") }}
You are a helpful assistant.
{% for message in messages %}
{{ _.role(message.role) }}
{{ message.content }}
{% endfor %}
{{ ctx.output_format }}
"#
}Usage in Application Code
Python:
from baml_client import b
from baml_client.types import Message
conversation = [
Message(role="user", content="What is BAML?"),
Message(role="assistant", content="BAML is a type-safe DSL for LLM prompts."),
Message(role="user", content="How do I install it?")
]
response = b.Chat(messages=conversation)
print(response)TypeScript:
import { b } from './baml_client'
import type { Message } from './baml_client/types'
const conversation: Message[] = [
{ role: "user", content: "What is BAML?" },
{ role: "assistant", content: "BAML is a type-safe DSL for LLM prompts." },
{ role: "user", content: "How do I install it?" }
]
const response = await b.Chat(conversation)
console.log(response)Template Strings
Template strings are reusable prompt snippets that can be composed into larger prompts.
Basic Template String
template_string FormatMessages(messages: Message[]) #"
{% for m in messages %}
{{ _.role(m.role) }}
{{ m.content }}
{% endfor %}
"#
function Chat(messages: Message[]) -> string {
client "openai/gpt-4o"
prompt #"
{{ _.role("system") }}
You are a helpful assistant.
{{ FormatMessages(messages) }}
{{ ctx.output_format }}
"#
}Reusable Context Templates
template_string SystemContext() #"
{{ _.role("system") }}
You are an expert data analyst.
Always show your reasoning step by step.
"#
template_string OutputGuidelines() #"
Be concise and precise.
Use technical terminology appropriately.
"#
function AnalyzeData(data: string) -> string {
client "openai/gpt-4o"
prompt #"
{{ SystemContext() }}
{{ OutputGuidelines() }}
{{ _.role("user") }}
Analyze: {{ data }}
{{ ctx.output_format }}
"#
}Streaming with Semantic Attributes
BAML supports structured streaming with automatic partial JSON parsing and fine-grained control over field streaming behavior.
Streaming Attributes
| Attribute | Effect | Use Case |
|---|---|---|
@stream.done | Field only appears when complete | Atomic values, IDs, complete items |
@stream.not_null | Parent object waits for this field | Discriminators, required fields |
@stream.with_state | Adds completion state metadata | UI loading indicators |
Field-Level Streaming Control
class BlogPost {
// Post won't stream until title is complete
title string @stream.done @stream.not_null
// Content streams token-by-token with state tracking
content string @stream.with_state
// Tags only appear when fully parsed
tags string[] @stream.done
// Author streams normally (partial strings allowed)
author string
}Class-Level Streaming Control
// Entire item streams atomically (all-or-nothing)
class ReceiptItem {
name string
price float
quantity int
@@stream.done
}
class Receipt {
store string
items ReceiptItem[] // Each item appears only when complete
total float @stream.done
}Discriminated Union Streaming
class Message {
// Message won't stream until type is known
type "error" | "success" @stream.not_null
content string
code int?
}StreamState with @stream.with_state
When using @stream.with_state, the field is wrapped in a StreamState object:
interface StreamState<T> {
value: T
state: "Pending" | "Incomplete" | "Complete"
}Basic Streaming Usage
Python:
from baml_client import b
stream = b.stream.GenerateBlogPost("AI safety")
for partial in stream:
# partial is a BlogPost with nullable fields
if partial.title:
print(f"Title: {partial.title}")
if partial.content:
print(f"Content so far: {partial.content[:100]}...")
# Get complete validated object
final = stream.get_final_response()
print(f"Final post: {final.title} - {len(final.content)} chars")TypeScript:
import { b } from './baml_client'
const stream = b.stream.GenerateBlogPost("AI safety")
for await (const partial of stream) {
// partial is Partial<BlogPost>
if (partial.title) {
console.log(`Title: ${partial.title}`)
}
if (partial.content) {
console.log(`Content: ${partial.content.substring(0, 100)}...`)
}
}
const final = await stream.getFinalResponse()
console.log(`Final: ${final.title} - ${final.content.length} chars`)TypeBuilder (Dynamic Types at Runtime)
TypeBuilder allows you to modify output schemas at runtime - useful for dynamic categories from databases, user-provided schemas, or A/B testing.
Mark Types as @@dynamic
enum Category {
RED
BLUE
@@dynamic // Allows runtime modification
}
class User {
name string
age int
@@dynamic // Allows adding properties at runtime
}
function Categorize(input: string) -> Category {
client "openai/gpt-4o"
prompt #"
Categorize: {{ input }}
{{ ctx.output_format }}
"#
}Modify Types at Runtime
Python:
from baml_client.type_builder import TypeBuilder
from baml_client import b
tb = TypeBuilder()
# Add enum values from database
tb.Category.add_value('GREEN')
tb.Category.add_value('YELLOW')
# Add class properties dynamically
tb.User.add_property('email', tb.string())
tb.User.add_property('address', tb.string().optional())
# Pass TypeBuilder when calling function
result = b.Categorize("The grass is lush", {"tb": tb})
# result can now be 'GREEN', 'YELLOW', 'RED', or 'BLUE'TypeScript:
import { TypeBuilder } from './baml_client/type_builder'
import { b } from './baml_client'
const tb = new TypeBuilder()
// Add enum values
tb.Category.addValue('GREEN')
tb.Category.addValue('YELLOW')
// Add class properties
tb.User.addProperty('email', tb.string())
tb.User.addProperty('address', tb.string().optional())
// Pass TypeBuilder when calling function
const result = await b.Categorize("The grass is lush", { tb })Create New Types at Runtime
from baml_client.type_builder import TypeBuilder
from baml_client import b
tb = TypeBuilder()
# Create a new enum
hobbies = tb.add_enum("Hobbies")
hobbies.add_value("Soccer")
hobbies.add_value("Reading")
hobbies.add_value("Gaming")
# Create a new class
address = tb.add_class("Address")
address.add_property("street", tb.string())
address.add_property("city", tb.string())
address.add_property("zip_code", tb.string())
# Attach to existing dynamic type
tb.User.add_property("hobbies", hobbies.type().list())
tb.User.add_property("address", address.type())
result = b.ExtractUser(text, {"tb": tb})TypeBuilder Methods
| Method | Description |
|---|---|
tb.string() | String type |
tb.int() | Integer type |
tb.float() | Float type |
tb.bool() | Boolean type |
tb.string().list() | List of strings |
tb.string().optional() | Optional string |
tb.add_class("Name") | Create new class |
tb.add_enum("Name") | Create new enum |
.add_property(name, type) | Add property to class |
.add_value(name) | Add value to enum |
.description("...") | Add description |
ClientRegistry (Dynamic Client Selection)
ClientRegistry allows you to modify LLM clients at runtime - useful for A/B testing, dynamic model selection, user-specific API keys, or cost optimization.
Basic Usage
Python:
from baml_py import ClientRegistry
from baml_client import b
import os
cr = ClientRegistry()
# Add a new client with custom configuration
cr.add_llm_client(
name='MyCustomClient',
provider='openai',
options={
"model": "gpt-4o",
"temperature": 0.7,
"max_tokens": 2000,
"api_key": os.environ.get('OPENAI_API_KEY')
}
)
# Set as the primary client for this call
cr.set_primary('MyCustomClient')
# Use the registry
result = b.ExtractResume(resume_text, {"client_registry": cr})TypeScript:
import { ClientRegistry } from '@boundaryml/baml'
import { b } from './baml_client'
const cr = new ClientRegistry()
// Add a new client
cr.addLlmClient('MyCustomClient', 'openai', {
model: "gpt-4o",
temperature: 0.7,
max_tokens: 2000,
api_key: process.env.OPENAI_API_KEY
})
// Set as the primary client
cr.setPrimary('MyCustomClient')
// Use the registry
const result = await b.ExtractResume(resumeText, { clientRegistry: cr })Runtime Model Selection
from baml_py import ClientRegistry
from baml_client import b
def process_with_model(text: str, use_premium: bool):
cr = ClientRegistry()
if use_premium:
cr.add_llm_client('SelectedModel', 'openai', {
"model": "gpt-4o",
"temperature": 0.3
})
else:
cr.add_llm_client('SelectedModel', 'openai', {
"model": "gpt-4o-mini",
"temperature": 0.5
})
cr.set_primary('SelectedModel')
return b.ExtractData(text, {"client_registry": cr})User-Specific API Keys
from baml_py import ClientRegistry
def extract_for_user(text: str, user_api_key: str):
cr = ClientRegistry()
cr.add_llm_client('UserClient', 'openai', {
"model": "gpt-4o",
"api_key": user_api_key
})
cr.set_primary('UserClient')
return b.ExtractData(text, {"client_registry": cr})ClientRegistry Methods
| Method | Description |
|---|---|
add_llm_client(name, provider, options) | Add a new LLM client |
set_primary(name) | Set which client to use for this call |
Note: Using the same name as a BAML-defined client overwrites it for that specific call only.
Multimodal Inputs
BAML supports images, audio, video, and PDF inputs for vision and multimodal models.
Image Inputs
class ImageAnalysis {
description string
main_objects string[]
text_detected string?
colors string[]
}
function AnalyzeImage(img: image) -> ImageAnalysis {
client "openai/gpt-4o"
prompt #"
{{ _.role("user") }}
Analyze this image in detail:
{{ img }}
{{ ctx.output_format }}
"#
}Python usage:
from baml_py import Image
from baml_client import b
# From URL
result = b.AnalyzeImage(Image.from_url("https://example.com/photo.jpg"))
# From base64
result = b.AnalyzeImage(Image.from_base64("image/png", base64_string))
# From file
with open("invoice.jpg", "rb") as f:
result = b.AnalyzeImage(Image.from_base64("image/jpeg", f.read()))TypeScript usage:
import { Image } from "@boundaryml/baml"
import { b } from './baml_client'
// From URL
const result = await b.AnalyzeImage(Image.fromUrl("https://example.com/photo.jpg"))
// From base64
const result = await b.AnalyzeImage(Image.fromBase64("image/png", base64String))Audio Inputs
class Transcription {
text string
language string
confidence "high" | "medium" | "low"
}
function TranscribeAudio(audio: audio) -> Transcription {
client "openai/gpt-4o"
prompt #"
{{ _.role("user") }}
Transcribe this audio and identify the language:
{{ audio }}
{{ ctx.output_format }}
"#
}Multiple Images
class ComparisonResult {
similarity_score float
differences string[]
main_difference string
}
function CompareImages(img1: image, img2: image) -> ComparisonResult {
client "openai/gpt-4o"
prompt #"
{{ _.role("user") }}
Compare these two images:
{{ img1 }}
{{ img2 }}
{{ ctx.output_format }}
"#
}Hierarchical Extraction
Nested Organizations
class Employee {
name string
title string
email string
}
class Department {
name string
manager Employee
employees Employee[]
}
class Company {
name string
departments Department[]
ceo Employee
}
function ExtractOrgChart(doc: string) -> Company {
client "openai/gpt-4o"
prompt #"
Extract the complete organizational structure from this document.
{{ _.role("user") }}
{{ doc }}
{{ ctx.output_format }}
"#
}Recursive Document Structure
class Section {
heading string
level int @description("1 = H1, 2 = H2, etc.")
content string
subsections Section[] // Recursive
}
class Document {
title string
author string?
sections Section[]
}
function ExtractDocStructure(doc: string) -> Document {
client "openai/gpt-4o"
prompt #"
Extract the complete document structure including nested sections.
{{ _.role("user") }}
{{ doc }}
{{ ctx.output_format }}
"#
}Table Parsing with Calculations
Invoice with Calculated Fields
class LineItem {
description string
quantity int @assert(valid_qty, {{ this > 0 }})
unit_price float @assert(valid_price, {{ this >= 0 }})
subtotal float @description("quantity * unit_price")
}
class Invoice {
invoice_number string
date string
vendor string
line_items LineItem[] @assert(has_items, {{ this|length > 0 }})
subtotal float @description("Sum of all line item subtotals")
tax_rate float
tax_amount float @description("subtotal * tax_rate")
total float @description("subtotal + tax_amount")
@@assert(valid_total, {{ this.total == this.subtotal + this.tax_amount }})
}
function ExtractInvoice(doc: image) -> Invoice {
client "openai/gpt-4o"
prompt #"
Extract the complete invoice with ALL calculated fields.
Important calculations:
- Line item subtotal = quantity × unit_price
- Invoice subtotal = sum of all line item subtotals
- Tax amount = subtotal × tax_rate
- Total = subtotal + tax_amount
{{ _.role("user") }}
{{ doc }}
{{ ctx.output_format }}
"#
}Best Practices
Keep Nesting Reasonable
Good (2-3 levels):
class Invoice {
vendor Company
line_items LineItem[]
}Too Deep (5+ levels) - split into multiple extraction passes.
Progressive Extraction for Large Documents
For large complex documents, use multiple passes:
from baml_client import b
# Pass 1: Extract structure
structure = b.ExtractStructure(document)
# Pass 2: Extract details for each section
sections = []
for section_ref in structure.section_ids:
section = b.ExtractSection(document, section_ref)
sections.append(section)
# Pass 3: Combine results
complete = combine(structure, sections)Token-Efficient Schemas
Verbose (wastes tokens):
class Person {
fullNameIncludingMiddle string @description("Complete name with first, middle, and last name")
currentAgeInYears int @description("The person's current age measured in years")
}Concise (efficient):
class Person {
name string // Clear from context
age int // Years is implied
}Error Recovery with Fallbacks
from baml_client import b
from baml_py.errors import BamlValidationError
try:
# Try complex extraction first
result = b.ExtractComplexInvoice(document)
except BamlValidationError as e:
# Fallback to simpler schema
print(f"Complex extraction failed: {e}, trying simple extraction")
result = b.ExtractSimpleInvoice(document)Use Appropriate Streaming Attributes
class RealTimeAnalysis {
// User sees title immediately when complete
title string @stream.done @stream.not_null
// Shows progressive updates to user
analysis string @stream.with_state
// Only show confidence when calculation complete
confidence_score float @stream.done
// Stream tags as they're extracted
tags string[]
}Dynamic Types for User-Generated Categories
from baml_client.type_builder import TypeBuilder
from baml_client import b
def classify_with_custom_categories(text: str, categories: list[str]):
"""Allow users to define their own classification categories"""
tb = TypeBuilder()
# Add user-provided categories
for category in categories:
tb.Category.add_value(category.upper())
return b.Classify(text, {"tb": tb})
# Usage
result = classify_with_custom_categories(
"This is urgent!",
categories=["URGENT", "NORMAL", "LOW_PRIORITY"]
)BAML (Basically, A Made-Up Language) Reference Guide for AI Agents
<Overview> BAML is a domain-specific language for building type-safe LLM prompts as functions. It provides:
- Strongly-typed inputs and outputs for LLM calls
- Automatic JSON parsing and validation
- Jinja-based prompt templating
- Multi-language code generation (Python, TypeScript, Go, Ruby)
The workflow is: Define BAML files → Run baml-cli generate → Import generated client in your code. </Overview>
Installation
Python
# Install the package
pip install baml-py # or: poetry add baml-py / uv add baml-py
# Initialize BAML in your project (creates baml_src/ directory)
baml-cli init
# Generate the client (REQUIRED after any .baml file changes)
baml-cli generateTypeScript / JavaScript
# Install the package
npm install @boundaryml/baml # or: pnpm add / yarn add / bun add
# Initialize BAML in your project
npx baml-cli init
# Generate the client (REQUIRED after any .baml file changes)
npx baml-cli generateVSCode / Cursor Extension
Install the BAML extension for syntax highlighting, testing playground, and prompt previews: https://marketplace.visualstudio.com/items?itemName=boundary.baml-extension
The extension auto-runs baml-cli generate on save.
CRITICAL: Running baml-cli generate
You MUST run `baml-cli generate` every time you modify any `.baml` file.
This command: 1. Reads all .baml files in baml_src/ 2. Generates the baml_client/ directory with type-safe code 3. Creates Pydantic models (Python) or TypeScript interfaces
# Python
baml-cli generate
# TypeScript
npx baml-cli generateAdd to your build process:
// package.json
{
"scripts": {
"build": "npx baml-cli generate && tsc --build"
}
}Testing
Run tests defined in .baml files with baml-cli test. Use baml-cli test --help for all options.
baml-cli test # Run all tests
baml-cli test -i "MyFunction:TestName" # Run specific testGenerator Block
The generator block in baml_src/generators.baml configures code generation. Created by baml-cli init.
generator target {
// Target language (REQUIRED)
// Options: "python/pydantic", "typescript", "typescript/react", "go", "ruby/sorbet"
output_type "python/pydantic"
// Output directory relative to baml_src/ (REQUIRED)
output_dir "../"
// Runtime version - should match installed package version (REQUIRED)
version "0.76.2"
// Default client mode: "sync" or "async"
default_client_mode "sync"
// TypeScript only: "cjs" (CommonJS) or "esm" (ES modules)
module_format "cjs"
// Shell command to run after generation (e.g., formatters)
on_generate "black . && isort ."
}Types
Primitive Types
bool // true/false
int // integers
float // decimal numbers
string // text
null // null valueComposite Types
string[] // array of strings
int? // optional int
string | int // union type
map<string, int> // key-value map
"a" | "b" | "c" // literal unionMultimodal Types
image // for vision models
audio // for audio models
video // for video models
pdf // for document modelsType Aliases
type Primitive = int | string | bool | float
type Graph = map<string, string[]>
// Recursive types are supported through containers
type JsonValue = int | string | bool | float | JsonObject | JsonArray
type JsonObject = map<string, JsonValue>
type JsonArray = JsonValue[]Classes
Classes define structured data. Properties have NO colon.
class MyObject {
// Required string
name string
// Optional field (use ?)
nickname string?
// Field with description (goes AFTER the type)
age int @description("Age in years")
// Field with alias (renames for LLM, keeps original in code)
email string @alias("email_address")
// Arrays (cannot be optional)
tags string[]
// Nested objects
address Address
// Enum field
status Status
// Union type
result "success" | "error"
// Literal types
version 1 | 2 | 3
// Map type
metadata map<string, string>
// Multimodal
photo image
}
// Recursive classes are supported
class Node {
value int
children Node[]
}Field Attributes
@alias("name")- Rename field for LLM (keeps original name in code)@description("...")- Add context for the LLM
Class Attributes
@@dynamic- Allow adding fields at runtime
Enums
Enums are for classification tasks with a fixed set of values.
enum Category {
PENDING
ACTIVE @description("Currently being processed")
COMPLETE
CANCELLED @alias("CANCELED") @description("Was stopped before completion")
INTERNAL @skip // Exclude from prompt
}
// Dynamic enum (can modify at runtime)
enum DynamicCategory {
Value1
Value2
@@dynamic
}Value Attributes
@alias("name")- Rename value for LLM@description("...")- Add context@skip- Exclude from prompt
Functions
Functions define LLM calls with typed inputs/outputs.
function FunctionName(param1: Type1, param2: Type2) -> ReturnType {
client "provider/model"
prompt #"
Your prompt here with {{ param1 }} and {{ param2 }}
{{ ctx.output_format }}
"#
}LLM Clients (Shorthand Syntax)
client "openai/gpt-4o"
client "openai/gpt-4o-mini"
client "anthropic/claude-sonnet-4-20250514"
client "anthropic/claude-3-5-haiku-latest"
client "google-ai/gemini-2.0-flash"See the Providers section below for full configuration options.
Prompt Syntax Rules
1. Always include inputs - Reference all input parameters in the prompt:
prompt #"
Analyze: {{ input }}
"#2. Always include output format - Let BAML generate schema instructions:
prompt #"
{{ ctx.output_format }}
"#3. Use roles for chat models:
prompt #"
{{ _.role("system") }}
You are a helpful assistant.
{{ _.role("user") }}
{{ user_message }}
"#4. DO NOT repeat output schema fields - {{ ctx.output_format }} handles this automatically.
Complete Function Example
class TweetAnalysis {
mainTopic string @description("The primary topic of the tweet")
sentiment "positive" | "negative" | "neutral"
isSpam bool
}
function ClassifyTweets(tweets: string[]) -> TweetAnalysis[] {
client "openai/gpt-4o-mini"
prompt #"
Analyze each tweet and classify it.
{{ _.role("user") }}
{{ tweets }}
{{ ctx.output_format }}
"#
}Prompt Syntax (Jinja)
Variables
{{ variable }}
{{ object.field }}
{{ array[0] }}Conditionals
{% if condition %}
content
{% elif other_condition %}
other content
{% else %}
fallback
{% endif %}Loops
{% for item in items %}
{{ item }}
{% endfor %}
{% for item in items %}
{{ _.role("user") if loop.index % 2 == 1 else _.role("assistant") }}
{{ item }}
{% endfor %}Roles
{{ _.role("system") }} // System message
{{ _.role("user") }} // User message
{{ _.role("assistant") }} // Assistant messageContext Variables
{{ ctx.output_format }} // Output schema instructions (REQUIRED)
{{ ctx.client.provider }} // Current provider name
{{ ctx.client.name }} // Client nameTemplate Strings
Reusable prompt snippets:
template_string FormatMessages(messages: Message[]) #"
{% for m in messages %}
{{ _.role(m.role) }}
{{ m.content }}
{% endfor %}
"#
function Chat(messages: Message[]) -> string {
client "openai/gpt-4o"
prompt #"
{{ FormatMessages(messages) }}
{{ ctx.output_format }}
"#
}Checks and Assertions
@assert - Strict validation (raises exception on failure)
class Person {
age int @assert(valid_age, {{ this >= 0 and this <= 150 }})
email string @assert(valid_email, {{ this|regex_match("@") }})
}
// On return type
function GetScore(input: string) -> int @assert(valid_score, {{ this >= 0 and this <= 100 }}) {
client "openai/gpt-4o"
prompt #"..."#
}@check - Non-exception validation (can inspect results)
class Citation {
quote string @check(has_content, {{ this|length > 0 }})
}Block-level assertions (cross-field validation)
class DateRange {
start_date string
end_date string
@@assert(valid_range, {{ this.start_date < this.end_date }})
}Multimodal Inputs
Images
function DescribeImage(img: image) -> string {
client "openai/gpt-4o"
prompt #"
{{ _.role("user") }}
Describe this image:
{{ img }}
"#
}Audio
function TranscribeAudio(audio: audio) -> string {
client "openai/gpt-4o"
prompt #"
{{ _.role("user") }}
Transcribe: {{ audio }}
"#
}Union Return Types (Tool Selection)
class SearchQuery {
query string
}
class WeatherRequest {
city string
}
class CalendarEvent {
title string
date string
}
function RouteRequest(input: string) -> SearchQuery | WeatherRequest | CalendarEvent {
client "openai/gpt-4o"
prompt #"
Determine what the user wants and extract the appropriate data.
{{ _.role("user") }}
{{ input }}
{{ ctx.output_format }}
"#
}Chat History Pattern
class Message {
role "user" | "assistant"
content string
}
function Chat(messages: Message[]) -> string {
client "openai/gpt-4o"
prompt #"
{{ _.role("system") }}
You are a helpful assistant.
{% for message in messages %}
{{ _.role(message.role) }}
{{ message.content }}
{% endfor %}
"#
}Tests
test TestClassify {
functions [ClassifyTweets]
args {
tweets ["Hello world!", "Buy now! Limited offer!"]
}
}
test TestImage {
functions [DescribeImage]
args {
img { url "https://example.com/image.png" }
}
}
test TestLocalImage {
functions [DescribeImage]
args {
img { file "test_image.png" }
}
}Usage in Code
Python
from baml_client import b
from baml_client.types import TweetAnalysis
def main():
# Sync call
result = b.ClassifyTweets(["Hello!", "Check out this deal!"])
for analysis in result:
print(f"Topic: {analysis.mainTopic}")
print(f"Sentiment: {analysis.sentiment}")TypeScript
import { b } from './baml_client'
import { TweetAnalysis } from './baml_client/types'
async function main() {
const result = await b.ClassifyTweets(["Hello!", "Check out this deal!"])
for (const analysis of result) {
console.log(`Topic: ${analysis.mainTopic}`)
console.log(`Sentiment: ${analysis.sentiment}`)
}
}Multimodal in Code
from baml_py import Image
from baml_client import b
# From URL
result = b.DescribeImage(Image.from_url("https://example.com/photo.jpg"))
# From base64
result = b.DescribeImage(Image.from_base64("image/png", base64_string))import { Image } from "@boundaryml/baml"
import { b } from './baml_client'
// From URL
const result = await b.DescribeImage(Image.fromUrl("https://example.com/photo.jpg"))
// From base64
const result = await b.DescribeImage(Image.fromBase64("image/png", base64String))Providers and Clients
BAML supports many LLM providers. For detailed configuration of any provider, search the docs at docs.boundaryml.com for the provider name.
Supported Providers
Native Providers (first-class support):
| Provider | Shorthand Example | Default API Key Env Var |
|---|---|---|
| openai | "openai/gpt-4o" | OPENAI_API_KEY |
| anthropic | "anthropic/claude-sonnet-4-20250514" | ANTHROPIC_API_KEY |
| google-ai | "google-ai/gemini-2.0-flash" | GOOGLE_API_KEY |
| vertex | "vertex/gemini-2.0-flash" | Google Cloud credentials |
| azure-openai | (requires full config) | AZURE_OPENAI_API_KEY |
| aws-bedrock | (requires full config) | AWS credentials |
OpenAI-Compatible Providers (use openai-generic):
These providers use OpenAI's API format. Use provider openai-generic with their base_url:
| Service | base_url |
|---|---|
| Groq | https://api.groq.com/openai/v1 |
| Together AI | https://api.together.ai/v1 |
| OpenRouter | https://openrouter.ai/api/v1 |
| Ollama | http://localhost:11434/v1 |
| Cerebras | https://api.cerebras.ai/v1 |
| Hugging Face | https://api-inference.huggingface.co/v1 |
| LM Studio | http://localhost:1234/v1 |
| vLLM | http://localhost:8000/v1 |
For the full list, see: https://docs.boundaryml.com/ref/llm-client
Shorthand vs Named Clients
Shorthand (quick, uses defaults):
function MyFunc(input: string) -> string {
client "openai/gpt-4o"
prompt #"..."#
}Named Client (full control):
client<llm> MyClient {
provider openai
options {
model "gpt-4o"
api_key env.MY_OPENAI_KEY
temperature 0.7
max_tokens 1000
}
}
function MyFunc(input: string) -> string {
client MyClient
prompt #"..."#
}Common Provider Configurations
OpenAI
client<llm> GPT4 {
provider openai
options {
model "gpt-4o" // or "gpt-4o-mini", "gpt-4-turbo", "o1", "o1-mini"
api_key env.OPENAI_API_KEY
temperature 0.7
max_tokens 4096
}
}Anthropic
client<llm> Claude {
provider anthropic
options {
model "claude-sonnet-4-20250514" // or "claude-3-5-haiku-latest"
api_key env.ANTHROPIC_API_KEY
max_tokens 4096
}
}Google AI (Gemini)
client<llm> Gemini {
provider google-ai
options {
model "gemini-2.0-flash" // or "gemini-2.5-pro", "gemini-2.5-flash"
api_key env.GOOGLE_API_KEY
generationConfig {
temperature 0.7
}
}
}OpenAI-Generic (Groq, Together, OpenRouter, Ollama, etc.)
// Groq
client<llm> Groq {
provider openai-generic
options {
base_url "https://api.groq.com/openai/v1"
api_key env.GROQ_API_KEY
model "llama-3.1-70b-versatile"
}
}
// Together AI
client<llm> Together {
provider openai-generic
options {
base_url "https://api.together.ai/v1"
api_key env.TOGETHER_API_KEY
model "meta-llama/Llama-3-70b-chat-hf"
}
}
// OpenRouter
client<llm> OpenRouter {
provider openai-generic
options {
base_url "https://openrouter.ai/api/v1"
api_key env.OPENROUTER_API_KEY
model "anthropic/claude-3.5-sonnet"
}
}
// Ollama (local)
client<llm> Ollama {
provider openai-generic
options {
base_url "http://localhost:11434/v1"
model "llama3"
}
}Azure OpenAI
client<llm> AzureGPT {
provider azure-openai
options {
resource_name "my-resource"
deployment_id "my-deployment"
api_key env.AZURE_OPENAI_API_KEY
}
}Retry Policies
retry_policy MyRetryPolicy {
max_retries 3
strategy {
type exponential_backoff
delay_ms 200
multiplier 1.5
max_delay_ms 10000
}
}
client<llm> ReliableClient {
provider openai
retry_policy MyRetryPolicy
options {
model "gpt-4o"
}
}Fallback Clients
Use multiple providers with automatic fallback:
client<llm> PrimaryClient {
provider openai
options { model "gpt-4o" }
}
client<llm> BackupClient {
provider anthropic
options { model "claude-sonnet-4-20250514" }
}
client<llm> ResilientClient {
provider fallback
options {
strategy [
PrimaryClient
BackupClient
]
}
}Round-Robin Load Balancing
client<llm> LoadBalanced {
provider round-robin
options {
strategy [ClientA, ClientB, ClientC]
}
}Custom Headers
client<llm> WithHeaders {
provider openai
options {
model "gpt-4o"
headers {
"X-Custom-Header" "value"
}
}
}Environment Variables
Reference environment variables with env.VAR_NAME:
client<llm> MyClient {
provider openai
options {
api_key env.MY_CUSTOM_KEY
base_url env.CUSTOM_BASE_URL
}
}Streaming
BAML supports structured streaming with automatic partial JSON parsing.
Basic Streaming
# Python
stream = b.stream.MyFunction(input)
for partial in stream:
print(partial) # Partial object with nullable fields
final = stream.get_final_response() # Complete validated object// TypeScript
const stream = b.stream.MyFunction(input)
for await (const partial of stream) {
console.log(partial) // Partial object
}
const final = await stream.getFinalResponse()Semantic Streaming Attributes
Control how fields stream with these attributes:
| Attribute | Effect | Use Case |
|---|---|---|
@stream.done | Field only appears when complete | Atomic values, IDs |
@stream.not_null | Parent object waits for this field | Discriminators, required fields |
@stream.with_state | Adds completion state metadata | UI loading indicators |
class BlogPost {
// Post won't stream until title is complete
title string @stream.done @stream.not_null
// Content streams token-by-token with state tracking
content string @stream.with_state
// Tags only appear when fully parsed
tags string[] @stream.done
}
class Message {
// Message won't stream until type is known
type "error" | "success" @stream.not_null
content string
}
// Entire item streams atomically (all-or-nothing)
class ReceiptItem {
name string
price float
@@stream.done
}@stream.with_state wraps the field in a StreamState object:
interface StreamState<T> {
value: T
state: "Pending" | "Incomplete" | "Complete"
}React / Next.js SDK
BAML provides first-class React/Next.js integration with auto-generated hooks and server actions. Requires Next.js 15+.
Installation
# Install packages
npm install @boundaryml/baml @boundaryml/baml-nextjs-plugin
# Initialize BAML
npx baml-cli initConfigure Next.js
// next.config.ts
import { withBaml } from '@boundaryml/baml-nextjs-plugin';
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
// ... existing config
};
export default withBaml()(nextConfig);Configure Generator for React
// baml_src/generators.baml
generator typescript {
output_type "typescript/react" // Enable React hooks generation
output_dir "../"
version "0.76.2"
}Then run npx baml-cli generate.
Auto-Generated Hooks
For each BAML function, a React hook is auto-generated with the pattern use{FunctionName}:
// baml_src/story.baml
class Story {
title string
content string
}
function WriteMeAStory(input: string) -> Story {
client "openai/gpt-4o"
prompt #"
Tell me a story about {{ input }}
{{ ctx.output_format }}
"#
}// app/components/story-form.tsx
'use client'
import { useWriteMeAStory } from "@/baml_client/react/hooks";
export function StoryForm() {
const story = useWriteMeAStory();
return (
<div>
<button
onClick={() => story.mutate("a brave robot")}
disabled={story.isLoading}
>
{story.isLoading ? 'Generating...' : 'Generate Story'}
</button>
{story.data && (
<div>
<h4>{story.data.title}</h4>
<p>{story.data.content}</p>
</div>
)}
{story.error && <div>Error: {story.error.message}</div>}
</div>
);
}Hook Options
// Streaming (default)
const hook = useWriteMeAStory();
// Non-streaming
const hook = useWriteMeAStory({ stream: false });
// With callbacks
const hook = useWriteMeAStory({
onStreamData: (partial) => console.log('Streaming:', partial),
onFinalData: (final) => console.log('Complete:', final),
onError: (error) => console.error('Error:', error),
});Hook Return Values
| Property | Type | Description |
|---|---|---|
data | `T \ | Partial<T>` |
streamData | Partial<T> | Latest streaming update |
finalData | T | Final complete response |
isLoading | boolean | Request in progress |
isPending | boolean | Waiting to start |
isStreaming | boolean | Currently streaming |
isSuccess | boolean | Completed successfully |
isError | boolean | Failed |
error | Error | Error details |
mutate(args) | function | Execute the BAML function |
reset() | function | Reset hook state |
Chatbot Example
// baml_src/chat.baml
class Message {
role "user" | "assistant"
content string
}
function Chat(messages: Message[]) -> string {
client "openai/gpt-4o"
prompt #"
You are a helpful assistant.
{% for m in messages %}
{{ _.role(m.role) }}
{{ m.content }}
{% endfor %}
"#
}'use client'
import { useChat } from "@/baml_client/react/hooks";
import { useState, useEffect } from "react";
import type { Message } from "@/baml_client/types";
export function ChatInterface() {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
const chat = useChat();
// Add assistant response to history when complete
useEffect(() => {
if (chat.isSuccess && chat.finalData) {
setMessages(prev => [...prev, { role: "assistant", content: chat.finalData! }]);
}
}, [chat.isSuccess, chat.finalData]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim() || chat.isLoading) return;
const newMessages = [...messages, { role: "user" as const, content: input }];
setMessages(newMessages);
setInput("");
await chat.mutate(newMessages);
};
return (
<div>
{messages.map((m, i) => (
<div key={i}><strong>{m.role}:</strong> {m.content}</div>
))}
{chat.isLoading && <div><strong>assistant:</strong> {chat.data ?? "..."}</div>}
<form onSubmit={handleSubmit}>
<input value={input} onChange={e => setInput(e.target.value)} />
<button type="submit" disabled={chat.isLoading}>Send</button>
</form>
</div>
);
}TypeBuilder (Dynamic Types at Runtime)
TypeBuilder allows you to modify output schemas at runtime - useful for dynamic categories from databases or user-provided schemas.
Setup: Mark types as @@dynamic in BAML
enum Category {
RED
BLUE
@@dynamic // Allows runtime modification
}
class User {
name string
age int
@@dynamic // Allows adding properties at runtime
}Modify Types at Runtime
Python:
from baml_client.type_builder import TypeBuilder
from baml_client import b
tb = TypeBuilder()
# Add enum values
tb.Category.add_value('GREEN')
tb.Category.add_value('YELLOW')
# Add class properties
tb.User.add_property('email', tb.string())
tb.User.add_property('address', tb.string().optional())
# Pass TypeBuilder when calling function
result = b.Categorize("The sun is bright", {"tb": tb})TypeScript:
import { TypeBuilder } from './baml_client/type_builder'
import { b } from './baml_client'
const tb = new TypeBuilder()
// Add enum values
tb.Category.addValue('GREEN')
tb.Category.addValue('YELLOW')
// Add class properties
tb.User.addProperty('email', tb.string())
tb.User.addProperty('address', tb.string().optional())
// Pass TypeBuilder when calling function
const result = await b.Categorize("The sun is bright", { tb })Create New Types at Runtime
tb = TypeBuilder()
# Create a new enum
hobbies = tb.add_enum("Hobbies")
hobbies.add_value("Soccer")
hobbies.add_value("Reading")
# Create a new class
address = tb.add_class("Address")
address.add_property("street", tb.string())
address.add_property("city", tb.string())
# Attach to existing type
tb.User.add_property("hobbies", hobbies.type().list())
tb.User.add_property("address", address.type())TypeBuilder Methods
| Method | Description |
|---|---|
tb.string() | String type |
tb.int() | Integer type |
tb.float() | Float type |
tb.bool() | Boolean type |
tb.string().list() | List of strings |
tb.string().optional() | Optional string |
tb.add_class("Name") | Create new class |
tb.add_enum("Name") | Create new enum |
.add_property(name, type) | Add property to class |
.add_value(name) | Add value to enum |
.description("...") | Add description |
ClientRegistry (Dynamic Client Selection)
ClientRegistry allows you to modify LLM clients at runtime - useful for A/B testing, dynamic model selection, or user-specific API keys.
Python:
from baml_py import ClientRegistry
from baml_client import b
import os
cr = ClientRegistry()
# Add a new client
cr.add_llm_client(
name='MyClient',
provider='openai',
options={
"model": "gpt-4o",
"temperature": 0.7,
"api_key": os.environ.get('OPENAI_API_KEY')
}
)
# Set as the primary client for this call
cr.set_primary('MyClient')
# Use the registry
result = b.ExtractResume("...", {"client_registry": cr})TypeScript:
import { ClientRegistry } from '@boundaryml/baml'
import { b } from './baml_client'
const cr = new ClientRegistry()
// Add a new client
cr.addLlmClient('MyClient', 'openai', {
model: "gpt-4o",
temperature: 0.7,
api_key: process.env.OPENAI_API_KEY
})
// Set as the primary client
cr.setPrimary('MyClient')
// Use the registry
const result = await b.ExtractResume("...", { clientRegistry: cr })ClientRegistry Methods
| Method | Description |
|---|---|
add_llm_client(name, provider, options) | Add a new LLM client |
set_primary(name) | Set which client to use |
Note: Using the same name as a BAML-defined client overwrites it for that call.
Best Practices
1. Always run `baml-cli generate` - After ANY change to .baml files 2. Always use `{{ ctx.output_format }}` - Never write output schema manually 3. Use `{{ _.role("user") }}` - Mark where user inputs begin 4. Use enums for classification - Not confidence scores or numbers 5. Use literal unions for small fixed sets - "high" | "medium" | "low" instead of enums 6. Use @description on fields - Guides the LLM without repeating in prompt 7. Keep prompts concise - Let the type system do the work 8. Avoid confidence levels - Don't add confidence scores to extraction schemas 9. Use composition over inheritance - Nest classes instead of inheriting 10. Dedent all declarations - Keep class/enum/function definitions at the root level
Documentation
For detailed documentation on any feature, visit: https://docs.boundaryml.com
Key documentation pages:
- Providers:
docs.boundaryml.com/ref/llm-client - React/Next.js:
docs.boundaryml.com/guide/framework-integration/react-next-js - TypeBuilder:
docs.boundaryml.com/ref/baml-client/typebuilder - ClientRegistry:
docs.boundaryml.com/guide/baml-advanced/client-registry - Dynamic Types:
docs.boundaryml.com/guide/baml-advanced/dynamic-runtime-types - Prompt Syntax:
docs.boundaryml.com/ref/prompt-syntax/what-is-jinja - Streaming:
docs.boundaryml.com/guide/baml-basics/streaming
File Organization
BAML files typically go in a baml_src/ directory:
baml_src/
clients.baml # LLM client configurations
types.baml # Classes and enums
functions.baml # Function definitions
tests.baml # Test casesRun baml generate after changes to regenerate the client code.
Notes on Generated Types
- In Python: BAML types are Pydantic classes (except primitives)
- In TypeScript: BAML types are interfaces (except primitives)
- Union types generate discriminated unions
- Optional fields default to
Nonein Python,undefinedin TypeScript
BAML Code Generation Examples
This document provides complete, real-world examples of BAML code generation for common use cases.
Example 1: Invoice Extraction
User Request: "Generate BAML to extract invoice data with line items"
Generated Code:
// Invoice extraction types
class InvoiceLineItem {
description string @description("Item or service description")
quantity int @description("Number of units") @assert(this > 0)
unit_price float @description("Price per unit") @assert(this > 0)
total float @description("Line item total") @assert(this > 0)
}
class Invoice {
invoice_number string @description("Unique invoice identifier")
date string @description("Invoice date (YYYY-MM-DD format)")
vendor string @description("Vendor or supplier name")
customer string @description("Customer or buyer name")
line_items InvoiceLineItem[] @description("List of line items")
subtotal float @description("Subtotal before tax") @assert(this > 0)
tax float? @description("Tax amount if applicable")
total float @description("Final total amount") @assert(this > 0)
}
// Extraction function
function ExtractInvoice(invoice_text: string) -> Invoice {
client GPT5
prompt #"
Extract all invoice details from this text.
Invoice text:
{{ invoice_text }}
Parse all line items with quantities and prices.
Calculate totals accurately.
{{ ctx.output_format }}
"#
}
// Client configuration
client<llm> GPT5 {
provider openai
options {
model gpt-5
temperature 0.0
max_tokens 2000
}
}Generated Tests:
# pytest test_invoice_extraction.py
import pytest
from baml_client import b
@pytest.mark.asyncio
async def test_extract_simple_invoice():
invoice_text = """
INVOICE #12345
Date: 2025-01-15
From: Acme Corp
To: Widget Inc
Item: Widget A - Qty: 10 @ $5.00 = $50.00
Item: Widget B - Qty: 5 @ $10.00 = $50.00
Subtotal: $100.00
Tax: $8.00
Total: $108.00
"""
result = await b.ExtractInvoice(invoice_text)
assert result.invoice_number == "12345"
assert result.vendor == "Acme Corp"
assert result.customer == "Widget Inc"
assert len(result.line_items) == 2
assert result.total == 108.00
@pytest.mark.asyncio
async def test_extract_invoice_without_tax():
invoice_text = """
Invoice: INV-2025-001
Vendor: Tech Services LLC
Consulting Services: 10 hours @ $150/hr = $1500
Total Due: $1500
"""
result = await b.ExtractInvoice(invoice_text)
assert result.invoice_number == "INV-2025-001"
assert result.tax is None
assert result.total == 1500.00Integration Code (FastAPI):
# api/routes/invoices.py
from fastapi import APIRouter, UploadFile
from baml_client import b
import pdfplumber
router = APIRouter()
@router.post("/extract-invoice")
async def extract_invoice(file: UploadFile):
# Extract text from PDF
with pdfplumber.open(file.file) as pdf:
text = "\n".join([page.extract_text() for page in pdf.pages])
# Extract invoice data
invoice = await b.ExtractInvoice(text)
return {
"invoice": invoice.model_dump(),
"metadata": {
"pages": len(pdf.pages),
"extracted_at": datetime.now().isoformat()
}
}Metadata:
- Pattern: Extraction (hierarchical)
- Tokens: ~800 per extraction
- Cost: ~$0.008 per invoice
- Latency: 3-5s
- Accuracy: 95%+ on structured invoices
---
Example 2: Sentiment Classification
User Request: "Create a sentiment classifier with confidence scores"
Generated Code:
// Sentiment classification types
enum Sentiment {
POSITIVE @description("Happy, satisfied, enthusiastic tone")
NEGATIVE @description("Angry, disappointed, frustrated tone")
NEUTRAL @description("Factual, balanced, no clear emotion")
}
class SentimentAnalysis {
sentiment Sentiment @description("Overall sentiment classification")
confidence float @description("Classification confidence 0-1") @assert(this >= 0 && this <= 1)
reasoning string @description("Explanation for the classification")
key_phrases string[] @description("Words/phrases that indicate sentiment")
}
// Classification function
function ClassifySentiment(text: string) -> SentimentAnalysis {
client GPT5Mini
prompt #"
Analyze the sentiment of this text:
{{ text }}
Classify as POSITIVE, NEGATIVE, or NEUTRAL.
Provide confidence score and reasoning.
Identify key phrases that indicate the sentiment.
{{ ctx.output_format }}
"#
}
// Fast client for classification
client<llm> GPT5Mini {
provider openai
options {
model gpt-5-mini
temperature 0.0
max_tokens 500
}
}Generated Tests:
# pytest test_sentiment.py
import pytest
from baml_client import b
@pytest.mark.asyncio
async def test_positive_sentiment():
text = "This product is amazing! Best purchase I've made this year."
result = await b.ClassifySentiment(text)
assert result.sentiment == "POSITIVE"
assert result.confidence > 0.8
assert "amazing" in [p.lower() for p in result.key_phrases]
@pytest.mark.asyncio
async def test_negative_sentiment():
text = "Terrible service. I'm very disappointed and frustrated."
result = await b.ClassifySentiment(text)
assert result.sentiment == "NEGATIVE"
assert result.confidence > 0.8
@pytest.mark.asyncio
async def test_neutral_sentiment():
text = "The package arrived on Tuesday. It contained the items ordered."
result = await b.ClassifySentiment(text)
assert result.sentiment == "NEUTRAL"Integration Code (TypeScript/Next.js):
// app/api/sentiment/route.ts
import { b } from '@/baml_client';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const { text } = await request.json();
try {
const analysis = await b.ClassifySentiment(text);
return NextResponse.json({
sentiment: analysis.sentiment,
confidence: analysis.confidence,
reasoning: analysis.reasoning,
keyPhrases: analysis.key_phrases
});
} catch (error) {
return NextResponse.json(
{ error: 'Sentiment analysis failed' },
{ status: 500 }
);
}
}Metadata:
- Pattern: Classification
- Tokens: ~200 per classification
- Cost: ~$0.001 per analysis
- Latency: <1s
- Accuracy: 92%+ on clear sentiment
---
Example 3: RAG with Citations
User Request: "Build a citation-aware document search system"
Generated Code:
// RAG with citations types
class Citation {
source_id string @description("Document or section identifier")
page_number int? @description("Page number if applicable")
quote string @description("Relevant excerpt or quote")
relevance_score float @description("0-1 relevance to answer") @assert(this >= 0 && this <= 1)
}
class DocumentAnswer {
answer string @description("Direct answer to the question")
citations Citation[] @description("Supporting sources with quotes")
confidence float @description("Answer confidence 0-1") @assert(this >= 0 && this <= 1)
needs_more_context bool @description("True if answer is incomplete")
suggested_followup string? @description("Suggested clarification question")
}
// RAG function
function AnswerFromDocuments(
question: string,
documents: string
) -> DocumentAnswer {
client GPT5
prompt #"
Answer this question using only information from the provided documents.
Question: {{ question }}
Documents:
{{ documents }}
Requirements:
- Cite specific sources for all claims
- Include direct quotes as citations
- If information is insufficient, set needs_more_context to true
- Suggest a followup question if answer is incomplete
{{ ctx.output_format }}
"#
}
// Client for RAG
client<llm> GPT5 {
provider openai
options {
model gpt-5
temperature 0.2
max_tokens 1500
}
}Generated Tests:
# pytest test_rag.py
import pytest
from baml_client import b
@pytest.mark.asyncio
async def test_answer_with_citations():
question = "What is the capital of France?"
documents = """
[doc1] France is a country in Western Europe. Its capital is Paris.
[doc2] Paris is known for the Eiffel Tower and the Louvre Museum.
"""
result = await b.AnswerFromDocuments(question, documents)
assert "Paris" in result.answer
assert len(result.citations) > 0
assert result.citations[0].source_id == "doc1"
assert result.confidence > 0.9
@pytest.mark.asyncio
async def test_incomplete_information():
question = "What is the population of Mars?"
documents = """
[doc1] Mars is the fourth planet from the Sun.
[doc2] Mars has two moons: Phobos and Deimos.
"""
result = await b.AnswerFromDocuments(question, documents)
assert result.needs_more_context is True
assert result.suggested_followup is not NoneIntegration Code (LangChain):
# rag_chain.py
from langchain.vectorstores import FAISS
from langchain.embeddings import OpenAIEmbeddings
from baml_client import b
class BAMLRagChain:
def __init__(self, documents):
self.embeddings = OpenAIEmbeddings()
self.vectorstore = FAISS.from_documents(documents, self.embeddings)
async def answer(self, question: str, k: int = 5):
# Retrieve relevant documents
docs = self.vectorstore.similarity_search(question, k=k)
# Format documents with IDs
doc_text = "\n".join([
f"[doc{i}] {doc.page_content}"
for i, doc in enumerate(docs)
])
# Generate answer with citations
result = await b.AnswerFromDocuments(question, doc_text)
# Resolve source IDs to actual documents
cited_docs = []
for citation in result.citations:
doc_idx = int(citation.source_id.replace("doc", ""))
cited_docs.append({
"document": docs[doc_idx].metadata,
"quote": citation.quote,
"relevance": citation.relevance_score
})
return {
"answer": result.answer,
"citations": cited_docs,
"confidence": result.confidence
}Metadata:
- Pattern: RAG
- Tokens: ~1200 per query
- Cost: ~$0.012 per question
- Latency: 4-6s
- Accuracy: 88%+ citation accuracy
---
Example 4: Multi-Step Agent
User Request: "Create a planning agent that breaks down research tasks"
Generated Code:
// Agent types
enum ResearchAction {
SEARCH_WEB @description("Search the internet")
SEARCH_PAPERS @description("Search academic papers")
READ_DOCUMENT @description("Read a specific document")
ANALYZE_DATA @description("Analyze collected data")
SYNTHESIZE @description("Synthesize findings")
DONE @description("Task complete")
}
class ResearchStep {
action ResearchAction @description("Action to take")
query string @description("Search query or document URL")
reasoning string @description("Why this step is needed")
expected_output string @description("What we expect to learn")
}
class ResearchPlan {
objective string @description("Clear research objective")
steps ResearchStep[] @description("Ordered research steps")
success_criteria string @description("How to know task is complete")
estimated_time_minutes int @description("Estimated time to complete")
}
// Planning function
function PlanResearch(topic: string) -> ResearchPlan {
client GPT5
prompt #"
Create a research plan for this topic:
{{ topic }}
Break it down into specific, actionable steps.
Each step should have a clear action, query, and reasoning.
Plan should be efficient - aim for 3-7 steps.
End with SYNTHESIZE to combine findings.
{{ ctx.output_format }}
"#
}
// Client for planning
client<llm> GPT5 {
provider openai
options {
model gpt-5
temperature 0.7
max_tokens 2000
}
}Generated Tests:
# pytest test_agent.py
import pytest
from baml_client import b
@pytest.mark.asyncio
async def test_simple_research_plan():
topic = "Impact of AI on healthcare in 2025"
plan = await b.PlanResearch(topic)
assert len(plan.steps) >= 3
assert len(plan.steps) <= 7
assert plan.steps[-1].action == "SYNTHESIZE"
assert plan.estimated_time_minutes > 0
@pytest.mark.asyncio
async def test_plan_has_reasoning():
topic = "Climate change mitigation strategies"
plan = await b.PlanResearch(topic)
for step in plan.steps:
assert len(step.reasoning) > 10
assert len(step.expected_output) > 10Integration Code (LangGraph):
# agent.py
from langgraph.graph import StateGraph
from baml_client import b
class ResearchAgent:
def __init__(self):
self.graph = self._build_graph()
def _build_graph(self):
workflow = StateGraph()
# Define nodes
workflow.add_node("plan", self._plan_step)
workflow.add_node("execute", self._execute_step)
workflow.add_node("synthesize", self._synthesize_step)
# Define edges
workflow.add_edge("plan", "execute")
workflow.add_conditional_edges(
"execute",
self._should_continue,
{
"continue": "execute",
"synthesize": "synthesize"
}
)
workflow.set_entry_point("plan")
return workflow.compile()
async def _plan_step(self, state):
plan = await b.PlanResearch(state["topic"])
return {"plan": plan, "step_index": 0}
async def _execute_step(self, state):
current_step = state["plan"].steps[state["step_index"]]
# Execute the step based on action type
if current_step.action == "SEARCH_WEB":
result = await self._search_web(current_step.query)
elif current_step.action == "SEARCH_PAPERS":
result = await self._search_papers(current_step.query)
# ... other actions
state["results"].append(result)
state["step_index"] += 1
return state
def _should_continue(self, state):
current_step = state["plan"].steps[state["step_index"]]
if current_step.action == "SYNTHESIZE":
return "synthesize"
return "continue"Metadata:
- Pattern: Agent
- Tokens: ~1500 per plan
- Cost: ~$0.015 per plan
- Latency: 5-8s
- Success rate: 85%+ actionable plans
---
Example 5: Multimodal Image Analysis
User Request: "Analyze receipt images and extract purchase data"
Generated Code:
// Multimodal types
class ReceiptItem {
name string @description("Item name")
quantity int @description("Quantity purchased") @assert(this > 0)
price float @description("Item price") @assert(this > 0)
}
class ReceiptData {
merchant string @description("Store or merchant name")
date string @description("Purchase date (YYYY-MM-DD)")
items ReceiptItem[] @description("Purchased items")
subtotal float @description("Subtotal amount")
tax float @description("Tax amount")
total float @description("Total amount") @assert(this > 0)
payment_method string? @description("Payment method if visible")
}
// Vision function
function AnalyzeReceipt(receipt_image: image) -> ReceiptData {
client GPT5Vision
prompt #"
Analyze this receipt image and extract all purchase data.
{{ receipt_image }}
Extract:
- Merchant name
- Date of purchase
- All items with quantities and prices
- Subtotal, tax, and total
- Payment method if visible
{{ ctx.output_format }}
"#
}
// Vision client
client<llm> GPT5Vision {
provider openai
options {
model gpt-5-vision
temperature 0.0
max_tokens 2000
}
}Generated Tests:
# pytest test_vision.py
import pytest
from baml_client import b
from baml_py import Image
@pytest.mark.asyncio
async def test_analyze_receipt():
receipt = Image.from_path("tests/fixtures/receipt1.jpg")
result = await b.AnalyzeReceipt(receipt)
assert result.merchant != ""
assert len(result.items) > 0
assert result.total > 0
assert result.subtotal + result.tax == pytest.approx(result.total, rel=0.01)Integration Code (FastAPI with file upload):
# api/routes/receipts.py
from fastapi import APIRouter, UploadFile, File
from baml_client import b
from baml_py import Image
import io
router = APIRouter()
@router.post("/analyze-receipt")
async def analyze_receipt(file: UploadFile = File(...)):
# Read uploaded image
image_bytes = await file.read()
image = Image.from_bytes(image_bytes)
# Analyze receipt
receipt_data = await b.AnalyzeReceipt(image)
return {
"receipt": receipt_data.model_dump(),
"metadata": {
"filename": file.filename,
"size_bytes": len(image_bytes)
}
}Metadata:
- Pattern: Vision + Extraction
- Tokens: ~1500 per image
- Cost: ~$0.015 per receipt
- Latency: 5-7s
- Accuracy: 90%+ on clear receipts
---
Common Generation Patterns
Pattern: Error Handling
from baml_client import b
from baml_client.errors import BamlError
async def safe_extract(text: str):
try:
result = await b.ExtractData(text)
return {"success": True, "data": result}
except BamlError as e:
return {"success": False, "error": str(e)}Pattern: Retry with Fallback
from baml_client import b
async def extract_with_fallback(text: str):
try:
# Try primary model
return await b.ExtractDataGPT5(text)
except Exception:
# Fall back to faster model
return await b.ExtractDataGPT5Mini(text)Pattern: Batch Processing
from baml_client import b
import asyncio
async def batch_classify(texts: list[str]):
tasks = [b.ClassifyText(text) for text in texts]
return await asyncio.gather(*tasks)Pattern: Streaming Results
from baml_client import b
async def stream_extraction(documents: list[str]):
for doc in documents:
result = await b.ExtractData(doc)
yield result---
Note: All examples use real BAML syntax validated against BoundaryML/baml repository. Tests are executable with pytest/Jest.
LangGraph + BAML Integration Reference
Last Updated: 2025-12-25 Source: BAML-REFERENCE-SOURCE.md (BoundaryML official gist), Hekmatica project, BoundaryML documentation
---
Complementary Architecture
LangGraph's Role: Orchestration engine for complex agent workflows
StateGraphcoordinates data flow between processing steps- Manages state transitions and conditional routing
- Handles cycles, loops, and multi-agent coordination
- Provides streaming infrastructure for real-time updates
BAML's Role: Type-safe LLM interaction layer
- Defines all LLM calls as typed functions with validated schemas
- Provides structured output parsing with fuzzy JSON handling
- Manages retries, fallbacks, and multi-provider resilience
- Separates prompts from orchestration code for maintainability
Integration Pattern:
LangGraph manages: WHEN to call LLMs, WHERE in the workflow
BAML defines: WHAT those LLM calls look like, HOW outputs are validated---
Why BAML + LangGraph
| LangGraph Provides | BAML Provides |
|---|---|
| Graph orchestration | Type-safe LLM calls |
| State management | Schema validation |
| Conditional routing | Fuzzy JSON parsing |
| Cycles and loops | Retry/fallback handling |
| Streaming infrastructure | Structured streaming |
Key Benefit: Clean separation of concerns
agent.py(LangGraph) stays focused on workflow logicbaml_src/contains all LLM prompts and schemas- Type safety across the entire pipeline
- Testable LLM functions independent of graph execution
---
Basic Integration Pattern
1. State Definition
from typing import TypedDict
from baml_client.types import ToolCall, ExtractedData
class AgentState(TypedDict):
messages: list[dict]
extracted: ExtractedData | None
tool_calls: list[ToolCall]
iteration: int2. BAML Functions as Graph Nodes
BAML functions can be called directly from LangGraph nodes:
from langgraph.graph import StateGraph
from baml_client import b
def extract_node(state: AgentState) -> AgentState:
"""Use BAML for structured extraction."""
result = b.ExtractData(state["messages"][-1]["content"])
return {"extracted": result}
def route_node(state: AgentState) -> AgentState:
"""Use BAML union types for tool selection."""
tools = b.SelectTools(state["messages"][-1]["content"])
return {"tool_calls": tools}
# Build graph
graph = StateGraph(AgentState)
graph.add_node("extract", extract_node)
graph.add_node("route", route_node)3. Union Types for Conditional Routing
BAML's union return types integrate naturally with LangGraph's routing:
from baml_client.types import GetWeather, SearchWeb, Calculator
def route_by_tool(state: AgentState) -> str:
"""Route based on BAML union type discriminator."""
if not state["tool_calls"]:
return "end"
tool = state["tool_calls"][0]
if isinstance(tool, GetWeather):
return "weather_node"
elif isinstance(tool, SearchWeb):
return "search_node"
elif isinstance(tool, Calculator):
return "calc_node"
return "end"
graph.add_conditional_edges("route", route_by_tool)---
BAML Schemas for LangGraph
Tool Selection with Union Types
Union return types are ideal for tool selection and routing:
class GetWeather {
type "get_weather"
location string
@@stream.done // Stream atomically when complete
}
class SearchWeb {
type "search"
query string
@@stream.done
}
class MessageToUser {
type "message"
content string @stream.with_state // Stream with completion state
}
class Resume {
type "resume"
@@stream.done
}
function SelectAction(
state: string,
query: string
) -> (GetWeather | SearchWeb | MessageToUser | Resume)[] {
client "openai/gpt-4o"
prompt #"
{{ _.role("system") }}
You are an agent that selects actions based on user queries.
Current state: {{ state }}
{{ _.role("user") }}
Query: {{ query }}
{{ ctx.output_format }}
"#
}State Extraction and Updates
Extract structured state from unstructured conversation:
class AgentState {
context string?
last_tool_result string?
iteration int
next_action "continue" | "stop" | "retry"
}
function ParseState(messages: string[]) -> AgentState {
client "openai/gpt-4o"
prompt #"
Extract agent state from conversation history:
{% for msg in messages %}
{{ msg }}
{% endfor %}
{{ ctx.output_format }}
"#
}---
TypeBuilder for Dynamic Schemas
TypeBuilder enables runtime schema modification based on graph state - powerful for adaptive agents:
Dynamic Schema from Graph State
from baml_client.type_builder import TypeBuilder
from baml_client import b
def adaptive_extraction_node(state: AgentState) -> AgentState:
"""Adapt schema based on discovered data."""
tb = TypeBuilder()
# Add dynamic categories from previous results
for category in state.get("discovered_categories", []):
tb.Category.add_value(category)
# Add dynamic fields based on context
if state.get("needs_location"):
tb.User.add_property("location", tb.string())
if state.get("needs_preferences"):
tb.User.add_property("preferences", tb.string().list())
# Call BAML with dynamic schema
result = b.ExtractUser(state["input"], {"tb": tb})
return {"extracted_user": result}Database-Driven Schemas
def db_driven_node(state: AgentState) -> AgentState:
"""Build schema from database configuration."""
tb = TypeBuilder()
# Fetch valid categories from database
categories = fetch_categories_from_db()
for cat in categories:
tb.ProductCategory.add_value(cat)
# Fetch custom fields from user config
custom_fields = fetch_user_schema_config(state["user_id"])
for field_name, field_type in custom_fields.items():
tb.Product.add_property(field_name, getattr(tb, field_type)())
result = b.ClassifyProduct(state["product_description"], {"tb": tb})
return {"classified": result}BAML Schema with @@dynamic
enum ProductCategory {
ELECTRONICS
CLOTHING
@@dynamic // Allows runtime additions
}
class Product {
name string
price float
category ProductCategory
@@dynamic // Allows runtime field additions
}
function ClassifyProduct(description: string) -> Product {
client "openai/gpt-4o"
prompt #"
Classify this product: {{ description }}
{{ ctx.output_format }}
"#
}---
Streaming Integration
BAML's structured streaming works seamlessly with LangGraph's streaming capabilities:
Basic Streaming Node
async def streaming_node(state: AgentState):
"""Stream BAML responses through LangGraph."""
stream = b.stream.GenerateResponse(state["messages"])
async for partial in stream:
# Yield partial results for real-time UI updates
yield {"partial_response": partial.content}
final = await stream.get_final_response()
return {"response": final}Semantic Streaming Attributes
Control streaming behavior with BAML attributes:
class BlogPost {
// Post won't stream until title is complete
title string @stream.done @stream.not_null
// Content streams token-by-token with state tracking
content string @stream.with_state
// Tags only appear when fully parsed
tags string[] @stream.done
// Author info streams when complete
author Author @stream.done
}
class Author {
name string
bio string
}
function WriteBlogPost(topic: string) -> BlogPost {
client "openai/gpt-4o"
prompt #"
Write a blog post about: {{ topic }}
{{ ctx.output_format }}
"#
}Advanced Streaming with State
async def advanced_streaming_node(state: AgentState):
"""Stream with completion state tracking."""
stream = b.stream.WriteBlogPost(state["topic"])
async for partial in stream:
# partial.title is None until complete
# partial.content has StreamState wrapper with completion status
if hasattr(partial, 'content') and partial.content:
progress = {
"value": partial.content.value,
"state": partial.content.state # "Pending" | "Incomplete" | "Complete"
}
yield {"streaming_content": progress}
final = await stream.get_final_response()
return {"blog_post": final}Stream Mode Integration
LangGraph supports multiple stream modes - combine with BAML streaming:
# LangGraph stream modes: "values", "updates", "messages", "custom", "debug"
async for chunk in graph.astream(
{"input": user_query},
stream_mode="updates" # Get node-by-node updates
):
node_name = list(chunk.keys())[0]
node_output = chunk[node_name]
# Handle BAML streaming outputs
if "partial_response" in node_output:
print(f"Streaming from {node_name}: {node_output['partial_response']}")---
Real-World Example: Research Agent
10-Step Research Workflow (inspired by Hekmatica):
1. Clarification (BAML) - Extract user intent and constraints 2. User Interaction (LangGraph) - Conditional routing based on clarity 3. Query Decomposition (BAML) - Break complex questions into sub-queries 4. Planning (BAML) - Generate research plan with typed steps 5. Information Gathering (Python tools) - Web search, API calls 6. Filtering (BAML) - Rank/filter results by relevance 7. Answer Synthesis (BAML) - Generate structured response with citations 8. Self-Critique (BAML) - Evaluate answer quality 9. Refinement (LangGraph conditional) - Iterate if quality insufficient 10. Completion (LangGraph) - Return final result
Architecture Split:
- LangGraph (agent.py): Orchestration (steps 2, 5, 9, 10)
- BAML (baml_src/): Cognitive tasks (steps 1, 3, 4, 6, 7, 8)
- Python Tools: External actions (step 5)
Key Insight: BAML handles 6/10 steps - all involving LLM reasoning. LangGraph handles state transitions, routing, and tool execution.
---
Migration from Pure LangGraph
Before: String Prompts
def planning_node(state: dict) -> dict:
response = llm.invoke([
SystemMessage("You are a planner. Output JSON with fields: steps, priority"),
HumanMessage(state["query"])
])
# Fragile parsing
plan = json.loads(response.content)
return {"plan": plan}After: BAML Integration
// baml_src/planner.baml
class Plan {
steps string[]
priority "high" | "medium" | "low"
estimated_time_minutes int?
}
function CreatePlan(query: string) -> Plan {
client "openai/gpt-4o"
prompt #"
{{ _.role("system") }}
You are a research planning expert.
{{ _.role("user") }}
Create a research plan for: {{ query }}
{{ ctx.output_format }}
"#
}# agent.py
from baml_client import b
def planning_node(state: dict) -> dict:
plan = b.CreatePlan(state["query"]) # Type-safe!
return {"plan": plan}Benefits:
- Type safety:
plan.stepsis guaranteedlist[str] - Fuzzy parsing: Handles messy LLM output automatically
- Centralized prompts: All in
baml_src/, version controlled - Testable:
baml-cli testwithout running full graph - Prompts separate from code: Easier iteration by prompt engineers
Migration Checklist
- [ ] Identify all LLM calls in LangGraph nodes
- [ ] Extract prompts to
.bamlfiles with type definitions - [ ] Replace
llm.invoke()withb.FunctionName() - [ ] Keep LangGraph for orchestration (don't change graph structure)
- [ ] Run
baml-cli generateto create client - [ ] Update imports:
from baml_client import b - [ ] Test nodes individually with
baml-cli test - [ ] Test full graph integration
---
Common Patterns
1. Message Formatting
class Message {
role "user" | "assistant" | "system"
content string
}
function SummarizeConversation(history: Message[]) -> string {
client "openai/gpt-4o"
prompt #"
{{ _.role("system") }}
Summarize the conversation concisely.
{% for msg in history %}
{{ _.role(msg.role) }}
{{ msg.content }}
{% endfor %}
{{ ctx.output_format }}
"#
}2. State Update Decisions
class StateUpdate {
new_context string?
iteration_complete bool
next_action "continue" | "stop" | "retry"
reasoning string @description("Why this action was chosen")
}
function DecideNextAction(
current_state: string,
latest_result: string
) -> StateUpdate {
client "openai/gpt-4o"
prompt #"
{{ _.role("system") }}
You are a decision engine for a research agent.
Current state: {{ current_state }}
Latest result: {{ latest_result }}
Decide the next action.
{{ ctx.output_format }}
"#
}3. Multi-Step Reasoning
class ReasoningStep {
thought string
action "search" | "analyze" | "conclude"
confidence "high" | "medium" | "low"
}
class ReasoningChain {
steps ReasoningStep[]
final_conclusion string
}
function ReasonAboutQuery(query: string, context: string) -> ReasoningChain {
client "openai/gpt-4o"
prompt #"
{{ _.role("system") }}
Break down your reasoning into explicit steps.
Query: {{ query }}
Context: {{ context }}
{{ ctx.output_format }}
"#
}---
Performance Optimization
When to Use BAML in LangGraph
✅ Use BAML for:
- Any LLM call requiring structured output
- Tool selection/routing decisions (union types)
- State parsing and updates
- Multi-step reasoning with validation
- Data extraction with complex schemas
- Classification and categorization tasks
⚠️ Keep in LangGraph:
- Simple string responses (no structure needed)
- State management (TypedDict containers)
- Conditional routing logic (Python
isinstancechecks) - External tool execution (web search, DB queries)
- Non-LLM computations
Optimization Tips
1. Cache BAML Client Initialization
from baml_client import b # Initialize once at module level
def node_function(state):
return b.Extract(state["data"]) # Reuse client2. Async for Parallel Nodes
import asyncio
async def parallel_extraction(state):
results = await asyncio.gather(
b.ExtractA(state["doc_a"]),
b.ExtractB(state["doc_b"]),
b.ExtractC(state["doc_c"])
)
return {"results": results}3. Use Streaming for Long Operations
async def long_running_node(state):
stream = b.stream.ComplexExtraction(state["large_doc"])
async for partial in stream:
# Update state incrementally for responsive UX
yield {"progress": partial}
final = await stream.get_final_response()
return {"result": final}4. Leverage Retry Policies
// baml_src/clients.baml
retry_policy AgentRetry {
max_retries 3
strategy {
type exponential_backoff
delay_ms 200
multiplier 1.5
max_delay_ms 10000
}
}
client<llm> ResilientGPT {
provider openai
retry_policy AgentRetry
options {
model "gpt-4o"
}
}5. Use Fallback for Reliability
client<llm> PrimaryModel {
provider openai
options { model "gpt-4o" }
}
client<llm> BackupModel {
provider anthropic
options { model "claude-sonnet-4-20250514" }
}
client<llm> ResilientAgent {
provider fallback
options {
strategy [PrimaryModel, BackupModel]
}
}---
Troubleshooting
Issue: BAML Types Don't Match LangGraph State
Problem: TypedDict vs Pydantic model mismatch
Solution: Convert at boundaries
from baml_client.types import ExtractedData
def baml_node(state: TypedDict) -> dict:
result: ExtractedData = b.Extract(state["text"])
# Convert Pydantic → dict for LangGraph state
return {"extracted": result.model_dump()}
# Or keep as Pydantic if LangGraph state accepts it
def baml_node_alt(state: TypedDict) -> dict:
result: ExtractedData = b.Extract(state["text"])
return {"extracted": result} # LangGraph can handle PydanticIssue: Streaming Not Working in Graph
Problem: LangGraph requires generators for streaming
Solution: Use yield with BAML streams
async def node(state):
stream = b.stream.Function(state["input"])
async for partial in stream:
yield {"partial": partial.model_dump()} # Must yield
final = await stream.get_final_response()
yield {"final": final.model_dump()}Issue: TypeBuilder Changes Not Reflected
Problem: TypeBuilder modifications not appearing in output schema
Solution: Ensure type is marked @@dynamic in BAML
enum Category {
DEFAULT_VALUE
@@dynamic // Required for TypeBuilder
}
class DynamicClass {
base_field string
@@dynamic // Required for adding properties
}Issue: Graph State Too Large for Context
Problem: Entire state passed to BAML function exceeds context limits
Solution: Extract only relevant state
def smart_node(state: AgentState) -> AgentState:
# Don't pass entire state to BAML
# Extract only what's needed
relevant_context = {
"last_message": state["messages"][-1],
"iteration": state["iteration"]
}
result = b.MakeDecision(relevant_context)
return {"decision": result}---
Best Practices Summary
1. BAML for LLM Nodes - Use BAML functions for any node that calls an LLM 2. Type Guards for Routing - Use isinstance() with BAML union types for clean routing 3. State Types Aligned - Keep LangGraph state types aligned with BAML types 4. Stream for UX - Use b.stream.FunctionName() for long-running extractions 5. Separate Concerns - LangGraph orchestrates, BAML handles LLM interactions 6. Test Independently - Use baml-cli test to test LLM functions before graph integration 7. Version Prompts - Keep prompts in baml_src/ under version control 8. Use TypeBuilder - Leverage dynamic schemas for adaptive agents 9. Async First - Use async/await for better concurrency in graphs 10. Handle Boundaries - Convert Pydantic ↔ dict at LangGraph/BAML boundaries
---
References
Official Documentation
- BAML Documentation
- BAML Streaming Guide
- TypeBuilder Reference
- Dynamic Types Guide
- LangGraph Documentation
- LangGraph Streaming
Example Projects
- Hekmatica Research Agent - Production BAML + LangGraph integration
- BAML Examples Repository - Official examples
Community Resources
---
Last Updated: 2025-12-25 BAML Version: 0.76.2+ LangGraph Version: Compatible with latest LangChain ecosystem
Other Languages Reference
BAML supports multiple language targets beyond Python and TypeScript. The workflow is consistent across all languages: define .baml files → run baml-cli generate → import generated client.
Supported Languages
| Language | Generator Type | Status |
|---|---|---|
| Python/Pydantic | python/pydantic | Stable |
| TypeScript | typescript | Stable |
| TypeScript/React | typescript/react | Stable |
| Go | go | Stable |
| Ruby/Sorbet | ruby/sorbet | Stable |
Go Support
Generator Configuration
generator target {
// Target language - Go
output_type "go"
// Output directory relative to baml_src/
output_dir "../"
// Runtime version - should match installed package version
version "0.76.2"
// Go module path for imports (optional but recommended)
package_name "github.com/your-org/your-project"
// Shell command to run after generation (e.g., formatters)
on_generate "gofmt -w . && goimports -w ."
}Usage Workflow
1. Install BAML CLI (if not already installed):
# Via Homebrew (macOS/Linux)
brew install boundaryml/baml/baml
# Or download from releases
# https://github.com/BoundaryML/baml/releases2. Initialize BAML in your Go project:
baml-cli init3. Define your BAML schemas in baml_src/:
class Resume {
name string
email string
skills string[]
}
function ExtractResume(text: string) -> Resume {
client "openai/gpt-4o"
prompt #"
Extract resume information from:
{{ text }}
{{ ctx.output_format }}
"#
}4. Generate the Go client:
baml-cli generate5. Import and use in your Go code:
package main
import (
"context"
"fmt"
"github.com/your-org/your-project/baml_client"
)
func main() {
client := baml_client.NewBamlClient()
resume, err := client.ExtractResume(
context.Background(),
"John Doe, john@example.com, Skills: Go, Python, Rust",
)
if err != nil {
panic(err)
}
fmt.Printf("Name: %s\n", resume.Name)
fmt.Printf("Email: %s\n", resume.Email)
fmt.Printf("Skills: %v\n", resume.Skills)
}Ruby Support
Generator Configuration
generator target {
// Target language - Ruby with Sorbet types
output_type "ruby/sorbet"
// Output directory relative to baml_src/
output_dir "../"
// Runtime version - should match installed package version
version "0.76.2"
// Shell command to run after generation (optional)
on_generate "bundle exec rubocop -a"
}Usage Workflow
1. Install BAML for Ruby:
# Add to Gemfile
gem 'baml'
# Install
bundle install2. Initialize BAML:
baml-cli init3. Define your BAML schemas in baml_src/:
class Product {
name string
price float
category "electronics" | "clothing" | "food"
}
function ClassifyProduct(description: string) -> Product {
client "openai/gpt-4o"
prompt #"
Classify this product:
{{ description }}
{{ ctx.output_format }}
"#
}4. Generate the Ruby client (with Sorbet types):
baml-cli generate5. Import and use in your Ruby code:
require_relative 'baml_client'
client = BamlClient.new
product = client.classify_product(
"Apple MacBook Pro 16-inch, M3 Max, $3499"
)
puts "Name: #{product.name}"
puts "Price: #{product.price}"
puts "Category: #{product.category}"Generator Block Configuration
The generator block in baml_src/generators.baml configures code generation. Created by baml-cli init.
Common Options
| Option | Description | Required |
|---|---|---|
output_type | Target language/format | Yes |
output_dir | Directory for generated code (relative to baml_src/) | Yes |
version | BAML runtime version (must match CLI) | Yes |
default_client_mode | Client mode: "sync" or "async" | No |
on_generate | Shell command to run after generation | No |
Language-Specific Options
Go
generator go {
output_type "go"
output_dir "../"
version "0.76.2"
// Go module path for imports
package_name "github.com/your-org/your-project"
// Post-generation formatting
on_generate "gofmt -w . && goimports -w . && go mod tidy"
}Ruby/Sorbet
generator ruby {
output_type "ruby/sorbet"
output_dir "../"
version "0.76.2"
// Post-generation linting/formatting
on_generate "bundle exec rubocop -a"
}TypeScript (for reference)
generator typescript {
output_type "typescript"
output_dir "../"
version "0.76.2"
// Module format: "esm" or "cjs" (CommonJS)
module_format "esm"
// Post-generation formatting
on_generate "prettier --write ."
}Multiple Generators
You can configure multiple generators in one project to generate clients for different languages:
// Python backend
generator python {
output_type "python/pydantic"
output_dir "../backend/baml_client"
version "0.76.2"
on_generate "black . && isort ."
}
// TypeScript frontend
generator typescript {
output_type "typescript"
output_dir "../frontend/baml_client"
version "0.76.2"
module_format "esm"
}
// Go service
generator go {
output_type "go"
output_dir "../go-service/baml_client"
version "0.76.2"
package_name "github.com/your-org/go-service"
on_generate "gofmt -w . && go mod tidy"
}
// Ruby microservice
generator ruby {
output_type "ruby/sorbet"
output_dir "../ruby-service/baml_client"
version "0.76.2"
}This allows the same BAML schemas to generate type-safe clients for different parts of your stack.
Version Synchronization
Critical: The version field must match your installed BAML CLI version.
# Check CLI version
baml-cli --version
# Update generator to match
generator target {
output_type "go"
output_dir "../"
version "0.76.2" // Must match CLI version!
}If versions mismatch, you'll get errors during generation.
Post-Generation Hooks
Use on_generate for automatic formatting and validation:
// Go - format, imports, and tidy
generator go {
output_type "go"
output_dir "../"
version "0.76.2"
on_generate "gofmt -w . && goimports -w . && go mod tidy"
}
// Ruby - run rubocop
generator ruby {
output_type "ruby/sorbet"
output_dir "../"
version "0.76.2"
on_generate "bundle exec rubocop -a"
}
// Python - run black and isort
generator python {
output_type "python/pydantic"
output_dir "../"
version "0.76.2"
on_generate "black . && isort ."
}
// TypeScript - run prettier
generator typescript {
output_type "typescript"
output_dir "../"
version "0.76.2"
on_generate "prettier --write ."
}Workflow Summary
Regardless of the target language, the workflow is the same:
1. Install BAML CLI and language-specific package 2. Run `baml-cli init` to create baml_src/ directory and generator config 3. Define `.baml` files with classes, enums, and functions 4. Run `baml-cli generate` after ANY change to .baml files 5. Import the generated client in your application code
The generated code is type-safe and provides:
- Strongly-typed function calls
- Automatic JSON parsing and validation
- Language-native data structures (structs in Go, classes in Ruby, etc.)
- Sync and async support (where applicable)
Notes
- Go: Generates standard Go structs with JSON tags
- Ruby: Generates Sorbet-typed classes for static type checking
- TypeScript: Generates interfaces and typed client functions
- Python: Generates Pydantic models for validation
All languages benefit from the same BAML features:
- Type safety
- Automatic schema generation
- Validation and assertions
- Multimodal support (images, audio, etc.)
- Streaming (where supported by the language runtime)
Python + BAML Reference
Python-specific patterns for BAML with Pydantic generated types.
Installation
# Install BAML for Python
pip install baml-py # or: poetry add baml-py / uv add baml-py
# Initialize BAML in your project (creates baml_src/ directory)
baml-cli init
# Generate the client (REQUIRED after any .baml file changes)
baml-cli generateCRITICAL: You MUST run baml-cli generate every time you modify any .baml file.
Generator Configuration
Create baml_src/generators.baml (created automatically by baml-cli init):
generator target {
// Target language - use "python/pydantic" for Python
output_type "python/pydantic"
// Output directory relative to baml_src/
output_dir "../"
// Runtime version - should match installed baml-py version
version "0.76.2"
// Default client mode: "sync" or "async"
default_client_mode "sync"
// Optional: Shell command to run after generation (e.g., formatters)
on_generate "black . && isort ."
}
// For Pydantic v1 compatibility (if needed)
generator target_v1 {
output_type "python/pydantic/v1"
output_dir "../baml_client_v1"
version "0.76.2"
}After running baml-cli generate, this creates:
my-project/
├── baml_src/
│ ├── generators.baml
│ ├── clients.baml
│ └── *.baml
├── baml_client/ # Generated - don't edit
│ ├── __init__.py
│ ├── types.py # Pydantic models
│ ├── sync_client.py
│ └── async_client.py
├── pyproject.toml
└── .envImport Pattern
from baml_client import b
from baml_client.types import Person, Invoice, LineItemThe b object provides access to all your BAML functions.
Basic Usage
Sync Usage
from baml_client import b
from baml_client.types import Person
# Simple extraction
person = b.ExtractPerson(text)
print(person.name) # Type-safe access
print(person.age) # Optional[int]
# With validation error handling
from baml_client.errors import BamlValidationError
try:
invoice = b.ExtractInvoice(doc)
print(invoice.total)
except BamlValidationError as e:
print(f"Validation failed: {e}")Async Usage
import asyncio
from baml_client import b
async def extract_async():
person = await b.ExtractPerson(text)
return person
result = asyncio.run(extract_async())
# Or in async context
async def main():
results = await asyncio.gather(
b.ExtractPerson(text1),
b.ExtractPerson(text2),
b.ExtractPerson(text3),
)
return resultsStreaming
BAML supports structured streaming with automatic partial JSON parsing.
Sync Streaming
from baml_client import b
# Stream with b.stream.FunctionName()
stream = b.stream.ExtractData(large_doc)
for partial in stream:
# Partial object with nullable fields
print(f"Progress: {len(partial.items) if partial.items else 0} items")
# Get the final, validated result
final = stream.get_final_response()
print(f"Complete: {final}")Async Streaming
async def stream_extraction(doc: str):
async for partial in b.stream.ExtractData(doc):
# Update UI with partial results
update_ui(partial)
# Or get final result
final = stream.get_final_response()
return finalSemantic Streaming
Control how fields stream with BAML attributes:
class BlogPost {
// Post won't stream until title is complete
title string @stream.done @stream.not_null
// Content streams token-by-token
content string
// Tags only appear when fully parsed
tags string[] @stream.done
}Type Safety with Pydantic
Generated Pydantic Models
BAML classes generate full Pydantic models:
# From baml_client/types.py (auto-generated)
from pydantic import BaseModel
from typing import Optional
class Person(BaseModel):
name: str
email: str
age: Optional[int] = None
# Full type hints and IDE autocomplete
person = b.ExtractPerson(text)
person.name # str - autocomplete works
person.age # Optional[int]Union Type Handling
from baml_client.types import GetWeather, SearchWeb, Calculator
result = b.SelectTool(query)
# Pattern matching (Python 3.10+)
match result:
case GetWeather(location=loc, units=u):
return fetch_weather(loc, u)
case SearchWeb(query=q):
return search(q)
case Calculator(expression=expr):
return eval_safe(expr)
# isinstance (all Python versions)
if isinstance(result, GetWeather):
return fetch_weather(result.location, result.units)
elif isinstance(result, SearchWeb):
return search(result.query)
elif isinstance(result, Calculator):
return eval_safe(result.expression)Multimodal Inputs
Image Handling
from baml_py import Image
from baml_client import b
# From URL
result = b.DescribeImage(
img=Image.from_url("https://example.com/image.png")
)
# From local file
result = b.DescribeImage(
img=Image.from_url("file:///path/to/image.jpg")
)
# From base64
result = b.DescribeImage(
img=Image.from_base64("image/png", base64_string)
)BAML Function Definition
function DescribeImage(img: image) -> string {
client "openai/gpt-4o"
prompt #"
{{ _.role("user") }}
Describe this image in detail:
{{ img }}
"#
}TypeBuilder (Dynamic Types)
Modify output schemas at runtime - useful for dynamic categories from databases.
Setup: Mark Types as @@dynamic
enum Category {
RED
BLUE
@@dynamic // Allows runtime modification
}
class User {
name string
age int
@@dynamic // Allows adding properties at runtime
}Add Values/Properties at Runtime
from baml_client.type_builder import TypeBuilder
from baml_client import b
tb = TypeBuilder()
# Add enum values dynamically
tb.Category.add_value('GREEN')
tb.Category.add_value('YELLOW')
# Add class properties dynamically
tb.User.add_property('email', tb.string())
tb.User.add_property('address', tb.string().optional())
# Pass TypeBuilder when calling function
result = b.Categorize("The sun is bright", {"tb": tb})Create New Types at Runtime
tb = TypeBuilder()
# Create a new enum
hobbies = tb.add_enum("Hobbies")
hobbies.add_value("Soccer")
hobbies.add_value("Reading")
hobbies.add_value("Gaming")
# Create a new class
address = tb.add_class("Address")
address.add_property("street", tb.string())
address.add_property("city", tb.string())
address.add_property("zip", tb.string().optional())
# Attach to existing type
tb.User.add_property("hobbies", hobbies.type().list())
tb.User.add_property("address", address.type())
result = b.ExtractUser(data, {"tb": tb})TypeBuilder Methods
| Method | Description |
|---|---|
tb.string() | String type |
tb.int() | Integer type |
tb.float() | Float type |
tb.bool() | Boolean type |
tb.string().list() | List of strings |
tb.string().optional() | Optional string |
tb.add_class("Name") | Create new class |
tb.add_enum("Name") | Create new enum |
.add_property(name, type) | Add property to class |
.add_value(name) | Add value to enum |
.description("...") | Add description |
ClientRegistry (Dynamic Client Selection)
Modify LLM clients at runtime - useful for A/B testing, user-specific API keys, or dynamic model selection.
from baml_py import ClientRegistry
from baml_client import b
import os
cr = ClientRegistry()
# Add a new client at runtime
cr.add_llm_client(
name='CustomGPT4',
provider='openai',
options={
"model": "gpt-4o",
"temperature": 0.7,
"api_key": os.environ.get('CUSTOM_OPENAI_KEY')
}
)
# Set as the primary client for this call
cr.set_primary('CustomGPT4')
# Use the custom client
result = b.ExtractResume(resume_text, {"client_registry": cr})ClientRegistry Methods
| Method | Description |
|---|---|
add_llm_client(name, provider, options) | Add a new LLM client |
set_primary(name) | Set which client to use |
Note: Using the same name as a BAML-defined client overwrites it for that call.
Validation and Checks
Check Results
BAML provides access to validation checks:
result = b.ExtractData(text)
# Access individual checks (from @check attributes)
if result.__baml_checks__.has_source.passed:
process(result.source)
else:
log_warning("Missing source")
# Check all validations
all_passed = all(
check.passed
for check in result.__baml_checks__.__dict__.values()
)Error Handling
from baml_client.errors import BamlValidationError
try:
result = b.ExtractInvoice(doc)
process_invoice(result)
except BamlValidationError as e:
logger.error(f"Validation failed: {e}")
result = fallback_extraction(doc)Testing
Run BAML Tests
# Run all tests defined in .baml files
baml-cli test
# Run specific test
baml-cli test -i "ExtractPerson:TestJohnSmith"
# See all options
baml-cli test --helpPython Unit Tests
import pytest
from baml_client import b
def test_person_extraction():
text = "John Smith, john@example.com, age 30"
result = b.ExtractPerson(text)
assert result.name == "John Smith"
assert result.email == "john@example.com"
assert result.age == 30
@pytest.mark.asyncio
async def test_async_extraction():
text = "Jane Doe, jane@example.com"
result = await b.ExtractPerson(text)
assert result.name == "Jane Doe"
assert result.email == "jane@example.com"
def test_streaming():
stream = b.stream.ExtractData(large_doc)
partials = list(stream)
assert len(partials) > 0
final = stream.get_final_response()
assert final is not NoneEnvironment Variables
# .env file
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
GOOGLE_API_KEY=...
# Load environment variables before importing baml_client
from dotenv import load_dotenv
load_dotenv()
from baml_client import b
# BAML functions will automatically use environment variables
# referenced in clients.baml with env.OPENAI_API_KEY syntaxProject Setup Examples
With uv (Recommended)
# Create project
uv init my-baml-project
cd my-baml-project
# Add BAML
uv add baml-py
# Initialize BAML structure
baml-cli init
# Generate client code
baml-cli generate
# Run your code
uv run python main.pyWith pip
# Create virtual environment
python -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
# Install BAML
pip install baml-py
# Initialize and generate
baml-cli init
baml-cli generateWith Poetry
# Create project
poetry new my-baml-project
cd my-baml-project
# Add BAML
poetry add baml-py
# Initialize and generate
baml-cli init
baml-cli generate
# Run
poetry run python main.pyComplete Example
from baml_client import b
from baml_client.types import Resume, WorkExperience
from baml_py import Image
from baml_client.errors import BamlValidationError
import asyncio
def sync_example():
"""Synchronous extraction"""
resume_text = """
John Smith
Software Engineer at Google (2020-2023)
Python, TypeScript, Go
"""
try:
resume = b.ExtractResume(resume_text)
print(f"Name: {resume.name}")
for job in resume.work_experience:
print(f" {job.title} at {job.company}")
except BamlValidationError as e:
print(f"Extraction failed: {e}")
async def async_example():
"""Async extraction with concurrency"""
resumes = [resume1, resume2, resume3]
results = await asyncio.gather(
*[b.ExtractResume(r) for r in resumes]
)
return results
def streaming_example():
"""Stream results for large documents"""
stream = b.stream.ExtractResume(large_resume)
for partial in stream:
# Show partial results as they arrive
if partial.name:
print(f"Found name: {partial.name}")
if partial.work_experience:
print(f"Jobs so far: {len(partial.work_experience)}")
final = stream.get_final_response()
return final
def image_example():
"""Vision model example"""
result = b.AnalyzeScreenshot(
img=Image.from_url("https://example.com/screenshot.png")
)
return result
if __name__ == "__main__":
sync_example()
asyncio.run(async_example())
streaming_example()Best Practices
1. Always run `baml-cli generate` - After ANY change to .baml files 2. Use type hints - Generated code has full type hints for IDE support 3. Handle validation errors - Wrap in try/except for production code 4. Stream for large documents - Better UX and memory efficiency 5. Use async for high throughput - Better concurrency than threads 6. Use uv for dependency management - Faster and more reliable than pip 7. Load environment variables first - Before importing baml_client 8. Leverage Pydantic features - Generated models support .dict(), .json(), validation
Documentation
For detailed documentation, visit: https://docs.boundaryml.com
Key pages:
- Python Client:
docs.boundaryml.com/ref/baml-client/python - TypeBuilder:
docs.boundaryml.com/ref/baml-client/typebuilder - ClientRegistry:
docs.boundaryml.com/guide/baml-advanced/client-registry - Streaming:
docs.boundaryml.com/guide/baml-basics/streaming
TypeScript + BAML Reference
TypeScript-specific patterns for BAML with generated interfaces and type-safe LLM extraction.
Installation
# Install the package
npm install @boundaryml/baml # or: pnpm add / yarn add / bun add
# Initialize BAML in your project
npx baml-cli init
# Generate the client (REQUIRED after any .baml file changes)
npx baml-cli generateCRITICAL: You MUST run npx baml-cli generate every time you modify any .baml file.
Add to your build process:
// package.json
{
"scripts": {
"build": "npx baml-cli generate && tsc --build"
}
}Generator Configuration
The generator block in baml_src/generators.baml configures TypeScript code generation:
// Standard CommonJS
generator target {
output_type "typescript"
output_dir "../"
version "0.76.2"
module_format "cjs" // CommonJS (default)
}
// ES Modules
generator target_esm {
output_type "typescript"
output_dir "../"
version "0.76.2"
module_format "esm" // ES modules
}
// React/Next.js integration
generator target_react {
output_type "typescript/react" // Auto-generates React hooks
output_dir "../"
version "0.76.2"
}Generator Options
| Option | Description | Values |
|---|---|---|
output_type | Target language/framework | "typescript", "typescript/react" |
output_dir | Output directory (relative to baml_src/) | Any path |
version | Runtime version (match installed package) | e.g., "0.76.2" |
module_format | Module system (TypeScript only) | "cjs" (CommonJS), "esm" (ES modules) |
on_generate | Shell command to run after generation | e.g., "prettier --write ." |
Project Structure
my-project/
├── baml_src/
│ ├── generators.baml # Generator configuration
│ ├── clients.baml # LLM client definitions
│ ├── types.baml # Classes and enums
│ ├── functions.baml # BAML functions
│ └── tests.baml # Test cases
├── baml_client/ # Generated - don't edit
│ ├── index.ts
│ ├── types.ts # TypeScript interfaces
│ └── client.ts
├── package.json
├── tsconfig.json
└── .envBasic Usage
Import Pattern
import { b } from './baml_client'
import type { Person, Invoice, LineItem } from './baml_client/types'Async Usage (Default)
TypeScript BAML functions are async by default and return Promises:
// Simple extraction
const person: Person = await b.ExtractPerson(text)
console.log(person.name) // Type-safe
// With error handling
try {
const invoice = await b.ExtractInvoice(doc)
console.log(invoice.total)
} catch (e) {
if (e instanceof BamlValidationError) {
console.error('Validation failed:', e.message)
}
}Streaming
Use for await to iterate over streaming responses:
// Async iteration
const stream = b.stream.ExtractData(largeDoc)
for await (const partial of stream) {
console.log(`Progress: ${partial.items?.length ?? 0} items`)
updateUI(partial) // Update UI with partial results
}
// Get final validated result
const final = await stream.getFinalResponse()Type Safety
Generated Interfaces
BAML generates TypeScript interfaces from your classes:
// baml_src/types.baml
class Person {
name string
email string
age int?
}// baml_client/types.ts (generated)
interface Person {
name: string
email: string
age?: number // Optional fields use `?`
}
// Full IntelliSense in IDE
const person = await b.ExtractPerson(text)
person.name // string
person.age // number | undefinedUnion Type Handling
BAML union types generate discriminated unions in TypeScript:
import type { GetWeather, SearchWeb, Calculator } from './baml_client/types'
const result = await b.SelectTool(query)
// Type guards
function isGetWeather(r: typeof result): r is GetWeather {
return 'location' in r && 'units' in r
}
if (isGetWeather(result)) {
return fetchWeather(result.location, result.units)
}Image Handling
BAML provides an Image class for multimodal inputs:
import { Image } from '@boundaryml/baml'
// From URL
const result = await b.AnalyzeImage(
Image.fromUrl('https://example.com/image.png')
)
// From base64
const result = await b.AnalyzeImage(
Image.fromBase64('image/jpeg', base64String)
)
// From file (Node.js)
import { readFileSync } from 'fs'
const buffer = readFileSync('image.jpg')
const result = await b.AnalyzeImage(
Image.fromBase64('image/jpeg', buffer.toString('base64'))
)TypeBuilder (Dynamic Types at Runtime)
TypeBuilder allows modifying output schemas at runtime:
import { TypeBuilder } from './baml_client/type_builder'
import { b } from './baml_client'
const tb = new TypeBuilder()
// Add enum values
tb.Category.addValue('GREEN')
tb.Category.addValue('YELLOW')
// Add class properties
tb.User.addProperty('email', tb.string())
tb.User.addProperty('address', tb.string().optional())
// Pass TypeBuilder when calling function
const result = await b.Categorize("The sun is bright", { tb })Create New Types at Runtime
const tb = new TypeBuilder()
// Create a new enum
const hobbies = tb.addEnum("Hobbies")
hobbies.addValue("Soccer")
hobbies.addValue("Reading")
// Create a new class
const address = tb.addClass("Address")
address.addProperty("street", tb.string())
address.addProperty("city", tb.string())
// Attach to existing type
tb.User.addProperty("hobbies", hobbies.type().list())
tb.User.addProperty("address", address.type())TypeBuilder Methods
| Method | Description |
|---|---|
tb.string() | String type |
tb.int() | Integer type |
tb.float() | Float type |
tb.bool() | Boolean type |
tb.string().list() | List of strings |
tb.string().optional() | Optional string |
tb.addClass("Name") | Create new class |
tb.addEnum("Name") | Create new enum |
.addProperty(name, type) | Add property to class |
.addValue(name) | Add value to enum |
.description("...") | Add description |
ClientRegistry (Dynamic Client Selection)
ClientRegistry allows modifying LLM clients at runtime:
import { ClientRegistry } from '@boundaryml/baml'
import { b } from './baml_client'
const cr = new ClientRegistry()
// Add a new client
cr.addLlmClient('MyClient', 'openai', {
model: "gpt-4o",
temperature: 0.7,
api_key: process.env.OPENAI_API_KEY
})
// Set as the primary client
cr.setPrimary('MyClient')
// Use the registry
const result = await b.ExtractResume("...", { clientRegistry: cr })ClientRegistry Methods
| Method | Description |
|---|---|
addLlmClient(name, provider, options) | Add a new LLM client |
setPrimary(name) | Set which client to use |
React / Next.js Integration
BAML provides first-class React/Next.js integration with auto-generated hooks. Requires Next.js 15+.
Installation
# Install packages
npm install @boundaryml/baml @boundaryml/baml-nextjs-plugin
# Initialize BAML
npx baml-cli initConfigure Next.js
// next.config.ts
import { withBaml } from '@boundaryml/baml-nextjs-plugin';
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
// ... existing config
};
export default withBaml()(nextConfig);Configure Generator for React
// baml_src/generators.baml
generator typescript {
output_type "typescript/react" // Enable React hooks generation
output_dir "../"
version "0.76.2"
}Then run npx baml-cli generate.
Auto-Generated Hooks
For each BAML function, a React hook is auto-generated with the pattern use{FunctionName}:
// baml_src/story.baml
class Story {
title string
content string
}
function WriteMeAStory(input: string) -> Story {
client "openai/gpt-4o"
prompt #"
Tell me a story about {{ input }}
{{ ctx.output_format }}
"#
}// app/components/story-form.tsx
'use client'
import { useWriteMeAStory } from "@/baml_client/react/hooks";
export function StoryForm() {
const story = useWriteMeAStory();
return (
<div>
<button
onClick={() => story.mutate("a brave robot")}
disabled={story.isLoading}
>
{story.isLoading ? 'Generating...' : 'Generate Story'}
</button>
{story.data && (
<div>
<h4>{story.data.title}</h4>
<p>{story.data.content}</p>
</div>
)}
{story.error && <div>Error: {story.error.message}</div>}
</div>
);
}Hook Options
// Streaming (default)
const hook = useWriteMeAStory();
// Non-streaming
const hook = useWriteMeAStory({ stream: false });
// With callbacks
const hook = useWriteMeAStory({
onStreamData: (partial) => console.log('Streaming:', partial),
onFinalData: (final) => console.log('Complete:', final),
onError: (error) => console.error('Error:', error),
});Hook Return Values
| Property | Type | Description |
|---|---|---|
data | `T \ | Partial<T>` |
streamData | Partial<T> | Latest streaming update |
finalData | T | Final complete response |
isLoading | boolean | Request in progress |
isPending | boolean | Waiting to start |
isStreaming | boolean | Currently streaming |
isSuccess | boolean | Completed successfully |
isError | boolean | Failed |
error | Error | Error details |
mutate(args) | function | Execute the BAML function |
reset() | function | Reset hook state |
Chatbot Example
// baml_src/chat.baml
class Message {
role "user" | "assistant"
content string
}
function Chat(messages: Message[]) -> string {
client "openai/gpt-4o"
prompt #"
You are a helpful assistant.
{% for m in messages %}
{{ _.role(m.role) }}
{{ m.content }}
{% endfor %}
"#
}'use client'
import { useChat } from "@/baml_client/react/hooks";
import { useState, useEffect } from "react";
import type { Message } from "@/baml_client/types";
export function ChatInterface() {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
const chat = useChat();
// Add assistant response to history when complete
useEffect(() => {
if (chat.isSuccess && chat.finalData) {
setMessages(prev => [...prev, { role: "assistant", content: chat.finalData! }]);
}
}, [chat.isSuccess, chat.finalData]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim() || chat.isLoading) return;
const newMessages = [...messages, { role: "user" as const, content: input }];
setMessages(newMessages);
setInput("");
await chat.mutate(newMessages);
};
return (
<div>
{messages.map((m, i) => (
<div key={i}><strong>{m.role}:</strong> {m.content}</div>
))}
{chat.isLoading && <div><strong>assistant:</strong> {chat.data ?? "..."}</div>}
<form onSubmit={handleSubmit}>
<input value={input} onChange={e => setInput(e.target.value)} />
<button type="submit" disabled={chat.isLoading}>Send</button>
</form>
</div>
);
}Server Component (API Route)
// app/api/extract/route.ts
import { b } from '@/baml_client'
export async function POST(req: Request) {
const { text } = await req.json()
const result = await b.ExtractPerson(text)
return Response.json(result)
}Environment Variables
# .env file
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...// Use dotenv for Node.js
import 'dotenv/config'
import { b } from './baml_client'
// In BAML files, reference with env.VAR_NAMEclient<llm> MyClient {
provider openai
options {
api_key env.OPENAI_API_KEY
}
}Testing
Jest
import { b } from './baml_client'
describe('Person Extraction', () => {
it('extracts name and email', async () => {
const text = 'John Smith, john@example.com'
const result = await b.ExtractPerson(text)
expect(result.name).toBe('John Smith')
expect(result.email).toBe('john@example.com')
})
})Vitest
import { describe, it, expect } from 'vitest'
import { b } from './baml_client'
describe('extraction', () => {
it('works', async () => {
const result = await b.ExtractPerson(text)
expect(result.name).toBeDefined()
})
})BAML Test Runner
Run tests defined in .baml files:
npx baml-cli test # Run all tests
npx baml-cli test -i "MyFunction:TestName" # Run specific test// baml_src/tests.baml
test TestClassify {
functions [ClassifyTweets]
args {
tweets ["Hello world!", "Buy now! Limited offer!"]
}
}Best Practices
1. Use strict TypeScript - Enable "strict": true in tsconfig.json 2. Import types separately - Use import type { ... } for interfaces 3. Handle async properly - Always await or handle promises correctly 4. Stream for UX - Better perceived performance in interactive applications 5. Server-side only - Don't expose API keys in browser code 6. Run baml-cli generate - After ANY change to .baml files 7. Use generated types - Leverage TypeScript's type system for safety 8. Handle errors - Wrap BAML calls in try/catch blocks
Common Patterns
Conditional Imports
// Use dynamic imports for server-side only code
if (typeof window === 'undefined') {
const { b } = await import('./baml_client')
// Use b here
}Error Handling
import { BamlValidationError } from '@boundaryml/baml'
try {
const result = await b.ExtractData(input)
} catch (error) {
if (error instanceof BamlValidationError) {
console.error('BAML validation failed:', error.message)
console.error('Prompt:', error.prompt)
console.error('Raw response:', error.rawOutput)
} else {
console.error('Unexpected error:', error)
}
}Type Narrowing
// Use type guards for union types
function processResult(result: GetWeather | SearchWeb | Calculator) {
if ('location' in result) {
// result is GetWeather
console.log(result.location)
} else if ('query' in result) {
// result is SearchWeb
console.log(result.query)
} else {
// result is Calculator
console.log(result.expression)
}
}Documentation
For detailed TypeScript documentation, visit:
- React/Next.js: https://docs.boundaryml.com/guide/framework-integration/react-next-js
- TypeBuilder: https://docs.boundaryml.com/ref/baml-client/typebuilder
- ClientRegistry: https://docs.boundaryml.com/guide/baml-advanced/client-registry
- Streaming: https://docs.boundaryml.com/guide/baml-basics/streaming