***
title: Dynamic Types - TypeBuilder
slug: guide/baml-advanced/dynamic-types
---------------------------------------
Sometimes you have **output schemas that change at runtime** -- for example if
you have a list of Categories that you need to classify that come from a
database, or your schema is user-provided.
`TypeBuilder` is used to create or modify dynamic types at runtime to achieve this.
### Dynamic BAML Enums
Imagine we want to make a categorizer prompt, but the list of categories to output come from a database.
1. Add `@@dynamic` to the class or enum definition to mark it as dynamic in BAML.
```rust baml
enum Category {
VALUE1 // normal static enum values that don't change
VALUE2
@@dynamic // this enum can have more values added at runtime
}
// The Category enum can now be modified at runtime!
function DynamicCategorizer(input: string) -> Category {
client GPT4
prompt #"
Given a string, classify it into a category
{{ input }}
{{ ctx.output_format }}
"#
}
```
2. Import the `TypeBuilder` from baml\_client in your runtime code and modify `Category`. All dynamic types you
define in BAML will be available as properties of `TypeBuilder`. Think of the
typebuilder as a registry of modified runtime types that the baml function will
read from when building the output schema in the prompt.
```python
from baml_client.type_builder import TypeBuilder
from baml_client import b
async def run():
tb = TypeBuilder()
tb.Category.add_value('VALUE3')
tb.Category.add_value('VALUE4')
# Pass the typebuilder in the baml_options argument -- the last argument of the function.
res = await b.DynamicCategorizer("some input", { "tb": tb })
# Now res can be VALUE1, VALUE2, VALUE3, or VALUE4
print(res)
```
```typescript
import TypeBuilder from '../baml_client/type_builder'
import {
b
} from '../baml_client'
async function run() {
const tb = new TypeBuilder()
tb.Category.addValue('VALUE3')
tb.Category.addValue('VALUE4')
const res = await b.DynamicCategorizer("some input", { tb: tb })
// Now res can be VALUE1, VALUE2, VALUE3, or VALUE4
console.log(res)
}
```
```ruby
require_relative '../baml_client'
def run
tb = Baml::TypeBuilder.new
tb.Category.add_value('VALUE3')
tb.Category.add_value('VALUE4')
res = Baml.Client.dynamic_categorizer(input: "some input", baml_options: {tb: tb})
# Now res can be VALUE1, VALUE2, VALUE3, or VALUE4
puts res
end
```
```go
package main
import (
"context"
"fmt"
b "example.com/baml_client"
)
func main() {
ctx := context.Background()
tb := b.NewTypeBuilder()
_, err := tb.Category.AddValue("VALUE3")
if err != nil {
panic(fmt.Sprintf("Failed to add value: %v", err))
}
_, err = tb.Category.AddValue("VALUE4")
if err != nil {
panic(fmt.Sprintf("Failed to add value: %v", err))
}
// Pass the typebuilder
res, err := b.DynamicCategorizer(ctx, "some input", b.WithTypeBuilder(tb))
if err != nil {
panic(fmt.Sprintf("Failed to categorize: %v", err))
}
// Now res can be VALUE1, VALUE2, VALUE3, or VALUE4
fmt.Printf("Result: %v\n", res)
}
```
```rust
use myproject::baml_client::sync_client::B;
use myproject::baml_client::type_builder::TypeBuilder;
fn main() {
let tb = TypeBuilder::new();
tb.Category().inner().add_value("VALUE3").unwrap();
tb.Category().inner().add_value("VALUE4").unwrap();
// Pass the typebuilder
let res = B.DynamicCategorizer.with_type_builder(&tb).call("some input").unwrap();
// Now res can be VALUE1, VALUE2, VALUE3, or VALUE4
println!("Result: {:?}", res);
}
```
Dynamic types are not yet supported when used via OpenAPI.
Please let us know if you want this feature, either via [Discord] or [GitHub][openapi-feedback-github-issue].
[Discord]: https://discord.gg/BTNBeXGuaS
[openapi-feedback-github-issue]: https://github.com/BoundaryML/baml/issues/892
### Dynamic BAML Classes
Now we'll add some properties to a `User` class at runtime using @@dynamic.
```rust 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 }}
"#
}
```
We can then modify the `User` schema at runtime. Since we marked `User` with `@@dynamic`, it'll be available as a property of `TypeBuilder`.
```python
from baml_client.type_builder import TypeBuilder
from baml_client import b
async def run():
tb = TypeBuilder()
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 })
# Now res can have email and address fields
print(res)
```
```typescript
import TypeBuilder from '../baml_client/type_builder'
import {
b
} from '../baml_client'
async function run() {
const tb = new TypeBuilder()
tb.User.add_property('email', tb.string())
tb.User.add_property('address', tb.string()).description("The user's address")
const res = await b.DynamicUserCreator("some user info", { tb: tb })
// Now res can have email and address fields
console.log(res)
}
```
```ruby
require_relative 'baml_client/client'
def run
tb = Baml::TypeBuilder.new
tb.User.add_property('email', tb.string)
tb.User.add_property('address', tb.string).description("The user's address")
res = Baml::Client.dynamic_user_creator(input: "some user info", baml_options: { tb: tb })
# Now res can have email and address fields
puts res
end
```
```go
package main
import (
"context"
"fmt"
b "example.com/baml_client"
)
func main() {
ctx := context.Background()
tb := b.NewTypeBuilder()
_, err := tb.User.AddProperty("email", tb.String())
if err != nil {
panic(fmt.Sprintf("Failed to add property: %v", err))
}
address, err := tb.User.AddProperty("address", tb.String())
if err != nil {
panic(fmt.Sprintf("Failed to add property: %v", err))
}
err = address.SetDescription("The user's address")
if err != nil {
panic(fmt.Sprintf("Failed to set description: %v", err))
}
res, err := b.DynamicUserCreator(ctx, "some user info", b.WithTypeBuilder(tb))
if err != nil {
panic(fmt.Sprintf("Failed to create user: %v", err))
}
// Now res can have email and address fields
fmt.Printf("Result: %+v\n", res)
}
```
```rust
use myproject::baml_client::sync_client::B;
use myproject::baml_client::type_builder::TypeBuilder;
fn main() {
let tb = TypeBuilder::new();
tb.User().inner().add_property("email", &tb.string()).unwrap();
tb.User().inner().add_property("address", &tb.string()).unwrap();
let res = B.DynamicUserCreator.with_type_builder(&tb).call("some user info").unwrap();
// Now res can have email and address fields
println!("Result: {:?}", res);
}
```
### Add existing BAML types to a property (e.g. you want to add a subset of tools)
Imagine you have a `ChatResponse` type in a function that you want to modify with a set of tools.
```baml {3}
class ChatResponse {
answer string?
@@dynamic
}
function Chat(messages: Message[]) -> ChatResponse {
...
}
```
You want to add a `tool_calls` property to the `ChatResponse` type that can be a list of `GetWeather` or `GetNews` types, that are completely defined in BAML.
```baml {11,12}
class GetWeather {
location string
}
class GetNews {
topic string
}
class ChatResponse {
answer string?
// We want to add this property at runtime!
tools (GetWeather | GetNews)[]?
@@dynamic
}
function Chat(messages: Message[]) -> ChatResponse {
...
}
```
You can modify the set of tools that can be used in the `ChatResponse` type at runtime like this:
```python
tb = TypeBuilder()
tb.ChatResponse.add_property(
"tools",
tb.union([
# we could comment one of these if we wanted!
tb.GetWeather.type(),
tb.GetNews.type()
]).list()
).description("The tool calls in the response")
```
```typescript
const tb = new TypeBuilder()
tb.ChatResponse.addProperty("tools",
tb.union([
// we could comment one of these if we wanted!
tb.GetWeather.type(),
tb.GetNews.type()
]).list()).description("The tool calls in the response")
```
```ruby
tb = Baml::TypeBuilder.new
tb.ChatResponse.add_property("tools", tb.union([tb.GetWeather.type(), tb.GetNews.type()]).list).description("The tool calls in the response")
```
```go
package main
import (
"context"
"fmt"
b "example.com/baml_client"
)
func main() {
ctx := context.Background()
tb := b.NewTypeBuilder()
toolsField, err := tb.Union([]baml.FieldType{
// we could comment one of these if we wanted!
tb.GetWeather.Type(),
tb.GetNews.Type()
}).List()
toolsField, err := tb.ChatResponse.AddProperty("tools", toolsField)
if err != nil {
panic(fmt.Sprintf("Failed to add property: %v", err))
}
err = toolsField.SetDescription("The tool calls in the response")
if err != nil {
panic(fmt.Sprintf("Failed to set description: %v", err))
}
// Example usage would depend on having a Chat function defined
// res, err := b.Chat(ctx, messages, b.WithTypeBuilder(tb))
// if err != nil {
// panic(fmt.Sprintf("Failed to chat: %v", err))
// }
// fmt.Printf("Result: %+v\n", res)
}
```
```rust
use myproject::baml_client::type_builder::TypeBuilder;
let tb = TypeBuilder::new();
let weather_type = tb.GetWeather().r#type();
let news_type = tb.GetNews().r#type();
let tools_type = tb.union(&[&weather_type, &news_type]);
let tools_list = tb.list(&tools_type);
tb.ChatResponse().inner().add_property("tools", &tools_list).unwrap();
```
### Creating new dynamic classes or enums not in BAML
The previous examples showed how to modify existing types. Here we create a new `Hobbies` enum, and a new class called `Address` without having them defined in BAML.
Note that you must attach the new types to the existing Return Type of your BAML function(in this case it's `User`).
```python Python
from baml_client.type_builder import TypeBuilder
from baml_client.async_client import b
async def run():
tb = TypeBuilder()
hobbies_enum = tb.add_enum("Hobbies")
hobbies_enum.add_value("Soccer")
hobbies_enum.add_value("Reading")
address_class = tb.add_class("Address")
address_class.add_property("street", tb.string()).description("The user's street address")
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})
# Now res might have the hobby property, which can be Soccer or Reading
print(res)
```
```typescript TypeScript
import TypeBuilder from '../baml_client/type_builder'
import { b } from '../baml_client'
async function run() {
const tb = new TypeBuilder()
const hobbiesEnum = tb.addEnum('Hobbies')
hobbiesEnum.addValue('Soccer')
hobbiesEnum.addValue('Reading')
const addressClass = tb.addClass('Address')
addressClass.addProperty('street', tb.string()).description("The user's street address")
tb.User.addProperty('hobby', hobbiesEnum.type().optional())
tb.User.addProperty('address', addressClass.type())
const res = await b.DynamicUserCreator("some user info", { tb: tb })
# Now res might have the hobby property, which can be Soccer or Reading
console.log(res)
}
```
```ruby Ruby
require_relative 'baml_client/client'
def run
tb = Baml::TypeBuilder.new
hobbies_enum = tb.add_enum('Hobbies')
hobbies_enum.add_value('Soccer')
hobbies_enum.add_value('Reading')
address_class = tb.add_class('Address')
address_class.add_property('street', tb.string)
tb.User.add_property('hobby', hobbies_enum.type.optional)
tb.User.add_property('address', address_class.type.optional)
res = Baml::Client.dynamic_user_creator(input: "some user info", baml_options: { tb: tb })
# Now res might have the hobby property, which can be Soccer or Reading
puts res
end
```
```go Go
package main
import (
"context"
"fmt"
b "example.com/baml_client"
)
func main() {
ctx := context.Background()
tb := b.NewTypeBuilder()
hobbiesEnum, err := tb.AddEnum("Hobbies")
if err != nil {
panic(fmt.Sprintf("Failed to add enum: %v", err))
}
_, err = hobbiesEnum.AddValue("Soccer")
if err != nil {
panic(fmt.Sprintf("Failed to add value: %v", err))
}
_, err = hobbiesEnum.AddValue("Reading")
if err != nil {
panic(fmt.Sprintf("Failed to add value: %v", err))
}
addressClass, err := tb.AddClass("Address")
if err != nil {
panic(fmt.Sprintf("Failed to add class: %v", err))
}
addressClass.AddProperty("street", tb.String()).Description("The user's street address")
if err != nil {
panic(fmt.Sprintf("Failed to add property: %v", err))
}
_, err = tb.User.AddProperty("hobby", hobbiesEnum.Type().Optional())
if err != nil {
panic(fmt.Sprintf("Failed to add property: %v", err))
}
_, err = tb.User.AddProperty("address", addressClass.Type().Optional())
if err != nil {
panic(fmt.Sprintf("Failed to add property: %v", err))
}
res, err := b.DynamicUserCreator(ctx, "some user info", b.WithTypeBuilder(tb))
if err != nil {
panic(fmt.Sprintf("Failed to create user: %v", err))
}
// Now res might have the hobby property, which can be Soccer or Reading
fmt.Printf("Result: %+v\n", res)
}
```
```rust Rust
use myproject::baml_client::sync_client::B;
use myproject::baml_client::type_builder::TypeBuilder;
fn main() {
let tb = TypeBuilder::new();
let hobbies_enum = tb.add_enum("Hobbies").unwrap();
hobbies_enum.add_value("Soccer").unwrap();
hobbies_enum.add_value("Reading").unwrap();
let address_class = tb.add_class("Address").unwrap();
address_class.add_property("street", &tb.string()).unwrap();
let hobbies_type = hobbies_enum.as_type().unwrap();
let optional_hobbies = tb.optional(&hobbies_type);
tb.User().inner().add_property("hobby", &optional_hobbies).unwrap();
let address_type = address_class.as_type().unwrap();
let optional_address = tb.optional(&address_type);
tb.User().inner().add_property("address", &optional_address).unwrap();
let res = B.DynamicUserCreator
.with_type_builder(&tb)
.call("some user info")
.unwrap();
// Now res might have the hobby property, which can be Soccer or Reading
println!("Result: {:?}", res);
}
```
TypeBuilder provides methods for building different kinds of types:
| Method | Returns | Description | Example |
| --------------------------------------- | -------------- | -------------------------------- | ----------------------------------- |
| `string()` | `FieldType` | Creates a string type | `tb.string()` |
| `int()` | `FieldType` | Creates an integer type | `tb.int()` |
| `float()` | `FieldType` | Creates a float type | `tb.float()` |
| `bool()` | `FieldType` | Creates a boolean type | `tb.bool()` |
| `literal_string(value: string)` | `FieldType` | Creates a literal string type | `tb.literal_string("hello")` |
| `literal_int(value: int)` | `FieldType` | Creates a literal integer type | `tb.literal_int(123)` |
| `literal_bool(value: boolean)` | `FieldType` | Creates a literal boolean type | `tb.literal_bool(true)` |
| `list(type: FieldType)` | `FieldType` | Makes a type into a list | `tb.list(tb.string())` |
| `union(types: FieldType[])` | `FieldType` | Creates a union of types | `tb.union([tb.string(), tb.int()])` |
| `map(key: FieldType, value: FieldType)` | `FieldType` | Creates a map type | `tb.map(tb.string(), tb.int())` |
| `add_class(name: string)` | `ClassBuilder` | Creates a new class | `tb.add_class("User")` |
| `add_enum(name: string)` | `EnumBuilder` | Creates a new enum | `tb.add_enum("Category")` |
| `MyClass` | `FieldType` | Reference an existing BAML class | `tb.MyClass.type()` |
### Adding descriptions to dynamic types
```python
tb = TypeBuilder()
tb.User.add_property("email", tb.string()).description("The user's email")
```
```typescript
const tb = new TypeBuilder()
tb.User.addProperty("email", tb.string()).description("The user's email")
```
```ruby
tb = Baml::TypeBuilder.new
tb.User.add_property("email", tb.string).description("The user's email")
```
```go
tb := b.NewTypeBuilder()
email, err := tb.User.AddProperty("email", tb.String())
if err != nil {
panic(fmt.Sprintf("Failed to get property: %v", err))
}
err = email.SetDescription("The user's email")
if err != nil {
panic(fmt.Sprintf("Failed to set description: %v", err))
}
```
```rust
use myproject::baml_client::type_builder::TypeBuilder;
let tb = TypeBuilder::new();
tb.User().inner().add_property("email", &tb.string()).unwrap()
.description("The user's email");
```
### Creating dynamic classes and enums at runtime with BAML syntax
Ok, what if you just want to write some actual baml code to modify the types at runtime?
The `TypeBuilder` has a higher level API `add_baml` to do this:
```python Python
tb = TypeBuilder()
tb.add_baml("""
// Creates a new class Address that does not exist in the BAML source.
class Address {
street string
city string
state string
}
// Modifies the existing @@dynamic User class to add the new address property.
dynamic class User {
address Address
}
// Modifies the existing @@dynamic Category enum to add a new variant.
dynamic enum Category {
VALUE5
}
""")
```
```typescript TypeScript
const tb = new TypeBuilder()
tb.addBaml(`
// Creates a new class Address that does not exist in the BAML source.
class Address {
street string
city string
state string
}
// Modifies the existing @@dynamic User class to add the new address property.
dynamic class User {
address Address
}
// Modifies the existing @@dynamic Category enum to add a new variant.
dynamic enum Category {
VALUE5
}
`)
```
```ruby Ruby
tb = Baml::TypeBuilder.new
tb.add_baml("
// Creates a new class Address that does not exist in the BAML source.
class Address {
street string
city string
state string
}
// Modifies the existing @@dynamic User class to add the new address property.
dynamic class User {
address Address
}
// Modifies the existing @@dynamic Category enum to add a new variant.
dynamic enum Category {
VALUE5
}
")
```
```go Go
tb := b.NewTypeBuilder()
tb.AddBaml(`
// Creates a new class Address that does not exist in the BAML source.
class Address {
street string
city string
state string
}
// Modifies the existing @@dynamic User class to add the new address property.
dynamic class User {
address Address
}
// Modifies the existing @@dynamic Category enum to add a new variant.
dynamic enum Category {
VALUE5
}
`)
```
```rust Rust
use myproject::baml_client::type_builder::TypeBuilder;
let tb = TypeBuilder::new();
tb.add_baml(r#"
// Creates a new class Address that does not exist in the BAML source.
class Address {
street string
city string
state string
}
// Modifies the existing @@dynamic User class to add the new address property.
dynamic class User {
address Address
}
// Modifies the existing @@dynamic Category enum to add a new variant.
dynamic enum Category {
VALUE5
}
"#).unwrap();
```
### Building dynamic types from JSON schema
JSON Schema is a declarative language for validating JSON data structures, often derived from language-native type definitions such as Python classes, TypeScript interfaces, or Java classes.
BAML supports converting JSON schemas into dynamic BAML types, allowing you to automatically use your existing data models with BAML's LLM functions. This feature enables seamless integration between your application's type system and BAML's structured output capabilities.
We have a working implementation of this feature, but are waiting for concrete use cases to merge it into the main codebase. For a detailed explanation of this functionality, see our [article on dynamic JSON schemas](https://www.boundaryml.com/blog/dynamic-json-schemas). You can also explore the [source code and examples](https://github.com/BoundaryML/baml-examples/tree/main/json-schema-to-baml) to understand how to implement this in your projects.
Please chime in on [the GitHub issue](https://github.com/BoundaryML/baml/issues/771) if this is something you'd like to use.
### Testing dynamic types in BAML
When testing dynamic types there are two different cases:
1. Injecting properties into dynamic types returned by the tested function.
2. Injecting values into dynamic types received as arguments by the tested function.
The first case requires using the `type_builder` and `dynamic` blocks in the
test, whereas the second case only requires specifying the values in the `args`
block.
#### Testing return types
##### Dynamic classes
Suppose we have a dynamic class `Resume` and we want to add a property that
stores the user's work experience when we testing a specific function. We can
do that by specifying the types and properties that we need in the
`type_builder` block.
```baml {4, 14-27}
class Resume {
name string
skills string[]
@@dynamic // Marked as @@dynamic.
}
// Function that returns a dynamic class.
function ExtractResume(from_text: string) -> Resume {
// Prompt
}
test ReturnDynamicClassTest {
functions [ExtractResume]
type_builder {
// Defines a new type available only within this test block.
class Experience {
title string
company string
start_date string
end_date string
}
// Injects new properties into the `@@dynamic` part of the Resume class.
dynamic class Resume {
experience Experience[]
}
}
args {
from_text #"
John Doe
Experience
- Software Engineer, Boundary, Sep 2022 - Sep 2023
Skills
- Python
- Java
"#
}
}
```
The rendered prompt for `ExtractResume` will now include the `experience` field
defined in the `dynamic` block and the LLM will correctly extract the experience
in the input text.
##### Dynamic enums
Dynamic enums can be included in the `type_builder` block just like classes. The
only difference is that we inject new variants in the `dynamic` block instead of
properties.
```baml {7, 17-22}
enum Category {
Refund
CancelOrder
TechnicalSupport
AccountIssue
Question
@@dynamic // Marked as @@dynamic.
}
// Function that returns a dynamic enum.
function ClassifyMessage(message: string) -> Category {
// Prompt
}
test ReturnDynamicEnumTest {
functions [ClassifyMessage]
type_builder {
// Injects new variants into the `@@dynamic` part of the Category enum.
dynamic enum Category {
Feedback
}
}
args {
message "I think the product is great!"
}
}
```
The `Feedback` variant will be rendered in the prompt for `ClassifyMessage`
during the test execution.
#### Testing parameter types
When a dynamic type is used as an input parameter of a function, we can simply
pass any value in the `args` block of the test and the value will be rendered in
the prompt.
##### Dynamic classes
```baml {4, 17-24}
class Resume {
name string
skills string[]
@@dynamic // Marked as @@dynamic.
}
function WriteResume(resume: Resume) -> string {
// Prompt
}
test DynamicClassAsInputTest {
functions [WriteResume]
args {
resume {
name "John Doe"
skills ["C++", "Java"]
experience [
{
title "Software Engineer"
company "Boundary"
start_date "2023-09-01"
end_date "2024-09-01"
}
]
}
}
}
```
##### Dynamic enums
Enums work the same way, any variant defined in the `args` block will be
rendered normally.
```baml {7, 17}
enum Category {
Refund
CancelOrder
TechnicalSupport
AccountIssue
Question
@@dynamic // Marked as @@dynamic.
}
function WriteCustomerMessage(category: Category) -> string {
// Prompt
}
test DynamicEnumAsInputTest {
functions [WriteCustomerMessage]
args {
category Feedback // The enum is dynamic so it accepts a new variant.
}
}
```
For more information about dynamic types, see [Type Builder](/ref/baml_client/type-builder).