mcpField
B
baml

baml

github.com/ouachitalabsDevelopment256 installsUpdated 39w ago
Repository

About baml

BAML is a DSL and quick reference for building LLM applications with structured, typed outputs. It generates client code for Python, TypeScript, Go, and Ruby, with syntax, testing, and CLI guidance.

Key features

  1. 01Typed structured output schemas
  2. 02Client generation for four languages
  3. 03Jinja-based prompt templates
  4. 04Lenient schema-aligned parsing
  5. 05Inline tests and CLI commands

Use cases

  1. 01Extracting structured data with LLMs
  2. 02Classifying input into enums
  3. 03Building multi-turn chat or multimodal flows
  4. 04Testing BAML functions

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/ouachitalabs/skills --skill baml

After installing, type/bamlin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

examples/
chat.baml
classification.baml
extraction.baml
multimodal.baml
usage.py

Details

From the project README

BAML Quick Reference

BAML (Boundary AI Markup Language) is a DSL for building LLM applications with structured, type-safe outputs. It generates client code for Python, TypeScript, Go, and Ruby.

How BAML Works

  • All .baml files in baml_src/ are globally accessible to each other
  • Generate the client with baml-cli generate
  • The generated baml_client/ provides type-safe functions you import and call
  • BAML types become Pydantic models (Python), TypeScript types, Go structs, or Sorbet types (Ruby)

Essential Syntax

Types
// Primitives
string, int, float, bool, null

// Composite
Type?           // optional (nullable)
Type[]          // array
Type1 | Type2   // union
map<K, V>       // dictionary

// Literals
"spam" | "ham"  // literal string union

// Multimodal
image, audio, video, pdf
Classes

Define structured data shapes. No colons between field name and type.

class Resume {
    name string
    email string?                           // optional
    skills string[]                         // array
    experience Experience[]                 // nested type
    seniority SeniorityLevel               // enum
}

class Experience {
    company string
    role string @description("Job title")   // hint for the LLM
    years int @alias("duration_years")      // JSON key mapping
}
Enums

Fixed set of values. Great for classification tasks.

enum SeniorityLevel {
    JUNIOR @description("0-2 years experience")
    MID @description("2-5 years experience")
    SENIOR @description("5+ years experience")
}

enum Category {
    SPAM
    HAM
    UNKNOWN @skip  // excluded from prompts
}
Functions

Define the LLM interaction. Function names must start with a capital letter.

function ExtractResume(resume_text: string) -> Resume {
    client "openai/gpt-4o"
    prompt #"
        Extract structured information from this resume.

        {{ ctx.output_format }}

        Resume:
        ---
        {{ resume_text }}
        ---
    "#
}
Clients

Configure LLM providers. Two styles:

// Shorthand (uses env vars automatically)
client "openai/gpt-4o"
client "anthropic/claude-sonnet-4-20250514"

// Named client (full control)
client<llm> GPT4 {
    provider openai
    options {
        model "gpt-4o"
        api_key env.OPENAI_API_KEY
        temperature 0.0
    }
}

// Fallback chain
client<llm> Resilient {
    provider fallback
    options {
        clients [GPT4, Claude, GPT4Mini]
    }
}
Generator

Configure code generation:

generator target {
    output_type "python/pydantic"  // or "typescript", "go", "ruby/sorbet"
    output_dir "../"
    default_client_mode "sync"     // or "async"
    version "0.203.1"
}

You may set up codegen for multiple locations in multiple languages. For example, you may want to keep backend and frontend types aligned for your respective BAML clients. You can do this by initializing two generator blocks.


The Two Critical Concepts

1. {{ ctx.output_format }}

This Jinja macro must be included in every prompt. It renders the return type schema so the LLM knows what structure to produce.

function ClassifyEmail(email: string) -> Category {
    client GPT4
    prompt #"
        Classify this email.

        {{ ctx.output_format }}

        Email: {{ email }}
    "#
}

For a Category enum, this renders something like:

Answer with any of the categories:
SPAM
HAM

For a class, it renders the JSON schema with field descriptions.

2. Schema-Aligned Parsing (SAP)

BAML's parser is intentionally lenient. It automatically fixes common LLM output issues:

  • Missing quotes around strings
  • Trailing commas
  • Comments in JSON
  • Incomplete sequences
  • Unescaped characters

This means you get 87-93% better accuracy than strict JSON parsing or function calling.


Prompt Syntax (Jinja)

BAML prompts use Jinja templating:

prompt #"
    {# Comments don't appear in output #}

    {{ _.role("system") }}
    You are a helpful assistant.

    {{ _.role("user") }}
    {% for msg in messages %}
        {{ msg.content }}
    {% endfor %}

    {% if verbose %}
        Be detailed in your response.
    {% endif %}

    {{ ctx.output_format }}
"#

Key constructs:

  • {{ variable }} - interpolate values
  • {% for item in list %}...{% endfor %} - loops
  • {% if cond %}...{% endif %} - conditionals
  • {{ _.role("system"|"user"|"assistant") }} - set message role
  • {{ value|filter }} - apply filters (e.g., |upper, |length, |join(","))

Calling BAML Functions

Python
from baml_client import b
from baml_client.types import Resume

# Sync
resume = b.ExtractResume(resume_text)
print(resume.name, resume.skills)

# Async
from baml_client.async_client import b
resume = await b.ExtractResume(resume_text)

# Streaming
stream = b.stream.ExtractResume(resume_text)
for partial in stream:
    print(partial)  # partial Resume object
final = stream.get_final_response()
TypeScript
import { b } from './baml_client'

// Async (default)
const resume = await b.ExtractResume(resumeText)
console.log(resume.name, resume.skills)

// Streaming
const stream = b.stream.ExtractResume(resumeText)
for await (const partial of stream) {
    console.log(partial)
}
const final = await stream.getFinalResponse()
Go
import b "example.com/myproject/baml_client"

resume, err := b.ExtractResume(ctx, resumeText, nil)
if err != nil {
    log.Fatal(err)
}
fmt.Println(resume.Name, resume.Skills)

Testing

Define tests directly in BAML files:

test SimpleExtraction {
    functions [ExtractResume]
    args {
        resume_text "John Doe, Software Engineer at Acme Corp for 5 years"
    }
    @@assert({{ this.name == "John Doe" }})
}

Run tests:

baml-cli test                    # run all
baml-cli test -i "ExtractResume" # filter by function
baml-cli test --parallel 5       # parallel execution

Attributes Reference

Field-level:

  • @alias("name") - rename field in JSON output
  • @description("...") - add context for the LLM
  • @skip - exclude from prompts (enums only)

Block-level:

  • @@dynamic - allow runtime modification of class/enum

Validation:

  • @check(expr, name) - soft validation (returns result, doesn't fail)
  • @assert(expr, name) - hard validation (throws on failure)

Streaming:

  • @@stream.done - object only appears when complete
  • @stream.not_null - field must have value before parent streams
  • @stream.with_state - include completion state metadata

CLI Commands

baml-cli init          # initialize new project
baml-cli generate      # generate client code
baml-cli dev           # dev server with hot reload
baml-cli test          # run tests
baml-cli serve         # start REST API server (port 2024)
baml-cli fmt           # format BAML files

Common Patterns

See the examples/ directory for complete working examples:

  • extraction.baml - structured data extraction
  • classification.baml - enum-based classification
  • chat.baml - multi-turn chat with message history
  • multimodal.baml - image/audio/pdf inputs
  • usage.py - Python calling patterns

Further Resources

Getting Started

Language Reference

LLM Providers

Prompt Engineering

Advanced

Testing

Comments

Markdown supported