Skip to main content
The TypeBuilder API enables runtime type modification, allowing you to adapt your output schemas dynamically based on database content, user input, or other runtime conditions.

When to Use Dynamic Types

Use dynamic types when:
  • Categories or classifications come from a database
  • Schema fields are user-configurable
  • You need to subset available tools based on context
  • Output structure varies by request

Dynamic Enums

Mark an enum as dynamic to add values at runtime.

Basic Dynamic Enum

Define the enum with @@dynamic in BAML:
main.baml
enum Category {
  VALUE1  // Static base values
  VALUE2
  @@dynamic  // Allow runtime additions
}

function DynamicCategorizer(input: string) -> Category {
  client GPT4
  prompt #"
    Given a string, classify it into a category
    {{ input }}
    {{ ctx.output_format }}
  "#
}
Add values at runtime:
from baml_client.type_builder import TypeBuilder
from baml_client import b

async def run():
    tb = TypeBuilder()
    
    # Add runtime values
    tb.Category.add_value('VALUE3')
    tb.Category.add_value('VALUE4')
    
    # Category can now be VALUE1, VALUE2, VALUE3, or VALUE4
    res = await b.DynamicCategorizer("some input", {"tb": tb})
    print(res)

Database-Driven Categories

A real-world example loading categories from a database:
from baml_client.type_builder import TypeBuilder
from baml_client import b
import asyncpg

async def categorize_with_db_categories(text: str):
    # Load categories from database
    conn = await asyncpg.connect('postgresql://...')
    categories = await conn.fetch('SELECT name FROM categories WHERE active = true')
    await conn.close()
    
    # Build dynamic type
    tb = TypeBuilder()
    for cat in categories:
        tb.Category.add_value(cat['name'])
    
    # Categorize with current categories
    return await b.DynamicCategorizer(text, {"tb": tb})

Dynamic Classes

Add properties to classes at runtime using @@dynamic.

Basic Dynamic Class

main.baml
class User {
  name string
  age int
  @@dynamic
}

function DynamicUserCreator(user_info: string) -> User {
  client GPT4
  prompt #"
    Extract the information from this chunk of text:
    "{{ user_info }}"
    {{ ctx.output_format }}
  "#
}
Add properties at runtime:
from baml_client.type_builder import TypeBuilder
from baml_client import b

async def run():
    tb = TypeBuilder()
    
    # Add properties with types
    tb.User.add_property('email', tb.string())
    tb.User.add_property('address', tb.string()).description("The user's address")
    
    res = await b.DynamicUserCreator("some user info", {"tb": tb})
    # res now has email and address fields
    print(res)

Advanced Patterns

Dynamic Tool Selection

Add a subset of available tools to a response type:
main.baml
class GetWeather {
  location string
}

class GetNews {
  topic string
}

class SearchWeb {
  query string
}

class ChatResponse {
  answer string?
  @@dynamic
}

function Chat(messages: Message[]) -> ChatResponse {
  client GPT4
  prompt #"
    {{ messages }}
    {{ ctx.output_format }}
  "#
}
Select tools based on user permissions:
from baml_client.type_builder import TypeBuilder
from baml_client import b

async def chat_with_tools(messages, user_permissions):
    tb = TypeBuilder()
    
    # Build union of available tools
    available_tools = []
    
    if 'weather' in user_permissions:
        available_tools.append(tb.GetWeather.type())
    
    if 'news' in user_permissions:
        available_tools.append(tb.GetNews.type())
    
    if 'search' in user_permissions:
        available_tools.append(tb.SearchWeb.type())
    
    # Add tools property if any tools available
    if available_tools:
        tb.ChatResponse.add_property(
            "tools",
            tb.union(available_tools).list().optional()
        ).description("Available tool calls")
    
    return await b.Chat(messages, {"tb": tb})

Creating New Types at Runtime

Create entirely new classes and enums without defining them in BAML:
from baml_client.type_builder import TypeBuilder
from baml_client import b

async def run():
    tb = TypeBuilder()
    
    # Create new enum
    hobbies_enum = tb.add_enum("Hobbies")
    hobbies_enum.add_value("Soccer")
    hobbies_enum.add_value("Reading")
    hobbies_enum.add_value("Gaming")
    
    # Create new class
    address_class = tb.add_class("Address")
    address_class.add_property("street", tb.string()).description("Street address")
    address_class.add_property("city", tb.string())
    address_class.add_property("zip", tb.string())
    
    # Attach to existing dynamic type
    tb.User.add_property("hobby", hobbies_enum.type().optional())
    tb.User.add_property("address", address_class.type().optional())
    
    res = await b.DynamicUserCreator("some user info", {"tb": tb})
    print(res)

TypeBuilder API

Type Methods

MethodReturnsDescriptionExample
string()FieldTypeString typetb.string()
int()FieldTypeInteger typetb.int()
float()FieldTypeFloat typetb.float()
bool()FieldTypeBoolean typetb.bool()
literal_string(value)FieldTypeLiteral stringtb.literal_string("hello")
literal_int(value)FieldTypeLiteral integertb.literal_int(123)
literal_bool(value)FieldTypeLiteral booleantb.literal_bool(true)
list(type)FieldTypeList typetb.list(tb.string())
union(types)FieldTypeUnion of typestb.union([tb.string(), tb.int()])
map(key, value)FieldTypeMap typetb.map(tb.string(), tb.int())
optional()FieldTypeMakes type optionaltb.string().optional()

Registry Methods

MethodReturnsDescription
add_class(name)ClassBuilderCreate new class
add_enum(name)EnumBuilderCreate new enum
MyClass.type()FieldTypeReference existing BAML class

Using BAML Syntax for Dynamic Types

For complex type modifications, use raw BAML syntax:
tb = TypeBuilder()
tb.add_baml("""
  // Create new class
  class Address {
    street string
    city string
    state string
    zip string
  }

  // Modify existing dynamic class
  dynamic class User {
    address Address
    phone_number string
  }

  // Modify existing dynamic enum
  dynamic enum Category {
    VALUE5
    VALUE6
  }
""")

Testing Dynamic Types

Testing Return Types

Use the type_builder block in tests:
test.baml
class Resume {
  name string
  skills string[]
  @@dynamic
}

function ExtractResume(from_text: string) -> Resume {
  client GPT4
  prompt #"Extract resume data from: {{ from_text }}"#
}

test ReturnDynamicClassTest {
  functions [ExtractResume]
  type_builder {
    // Define test-only types
    class Experience {
      title string
      company string
      start_date string
      end_date string
    }

    // Inject into dynamic class
    dynamic class Resume {
      experience Experience[]
    }
  }
  args {
    from_text #"
      John Doe
      
      Experience
      - Software Engineer, Boundary, Sep 2022 - Sep 2023
      
      Skills
      - Python
      - Java
    "#
  }
}

Testing Parameter Types

Pass dynamic values directly in test args:
test.baml
class Resume {
  name string
  skills string[]
  @@dynamic
}

function WriteResume(resume: Resume) -> string {
  client GPT4
  prompt #"Write a resume for {{ resume }}"#
}

test DynamicClassAsInputTest {
  functions [WriteResume]
  args {
    resume {
      name "John Doe"
      skills ["C++", "Java"]
      // Dynamic fields work directly in args
      experience [
        {
          title "Software Engineer"
          company "Boundary"
        }
      ]
    }
  }
}

JSON Schema Integration

BAML supports converting JSON schemas to dynamic types. This feature is functional but awaiting user feedback before merging. See the blog post and examples for implementation details. Please comment on GitHub issue #771 if this interests you.

Best Practices

  1. Base Types: Define base structure in BAML, use dynamic only for runtime variations
  2. Validation: Validate dynamic values before adding them to avoid LLM confusion
  3. Caching: Cache TypeBuilder instances when using the same dynamic structure across calls
  4. Documentation: Use .description() extensively to help the LLM understand dynamic fields
  5. Testing: Test dynamic types thoroughly with various combinations of runtime additions

Build docs developers (and LLMs) love