Get Started with ash_baml
View SourceIn this tutorial, you'll create your first AI-powered Ash resource using BAML. By the end, you'll have a working resource that calls an LLM and returns structured output.
Prerequisites
- Elixir and Erlang installed
- Familiarity with Ash Framework (resources, actions, domains)
- A text editor and terminal
Goals
- Install ash_baml and BAML
- Define a simple BAML function
- Create an Ash resource that calls it
- See structured LLM output in action
Installation
Add ash_baml to your dependencies in mix.exs:
def deps do
[
{:ash, "~> 3.0"},
{:ash_baml, "~> 0.1"}
]
endThen run:
mix deps.get
Set Up BAML
Create a baml_src/ directory in your project root for BAML files:
mkdir -p baml_src
This directory will contain your BAML schema definitions (classes, enums, functions, and client configurations).
Define Your First BAML Function
Create baml_src/functions.baml:
class Reply {
content string @description("The AI's response")
confidence float @description("Confidence score 0.0-1.0")
}
client GPT4 {
provider openai
options {
model gpt-4
api_key env.OPENAI_API_KEY
}
}
function SayHello(name: string) -> Reply {
client GPT4
prompt #"
Say a friendly hello to {{ name }}.
Be enthusiastic!
{{ ctx.output_format }}
"#
}Note: See BAML documentation for more on BAML syntax, clients, and prompts.
Configure BAML Client
Add client configuration to config/config.exs:
config :ash_baml,
clients: [
default: {MyApp.BamlClient, baml_src: "baml_src"}
]This tells ash_baml to auto-generate a MyApp.BamlClient module at compile-time that reads from your baml_src/ directory.
Generate Ash Types
Generate Ash-compatible types from your BAML schemas:
mix ash_baml.gen.types MyApp.BamlClient
This creates lib/my_app/baml_client/types/reply.ex:
defmodule MyApp.BamlClient.Types.Reply do
use Ash.TypedStruct
typed_struct do
field :content, :string
field :confidence, :float
end
endNote: The BAML client module itself is generated automatically at compile-time based on your config. The
mix ash_baml.gen.typestask generates separate Ash type modules for use in your resources.
Create Your First AI-Powered Resource
Create lib/my_app/assistant.ex:
defmodule MyApp.Assistant do
use Ash.Resource,
domain: MyApp.Domain,
extensions: [AshBaml.Resource]
baml do
client :default
import_functions [:SayHello]
end
# Actions are auto-generated:
# - :say_hello (regular)
# - :say_hello_stream (streaming)
endAdd to your domain (lib/my_app/domain.ex):
defmodule MyApp.Domain do
use Ash.Domain
resources do
resource MyApp.Assistant
end
endCall Your First BAML Function
Start IEx:
iex -S mix
Call the function:
{:ok, reply} = MyApp.Assistant
|> Ash.ActionInput.for_action(:say_hello, %{name: "Alice"})
|> Ash.run_action()You'll get a structured response:
reply
# => %MyApp.BamlClient.Types.Reply{
# content: "Hello Alice! It's wonderful to meet you! 🎉",
# confidence: 0.95
# }What Just Happened?
- AshBaml auto-generated the
MyApp.BamlClientmodule at compile-time from yourbaml_src/directory - You generated Ash types from BAML schemas using
mix ash_baml.gen.types - AshBaml.Resource extension auto-generated two actions:
:say_hello- Regular action returningReply:say_hello_stream- Streaming action returningStream
- Ash executed the action, calling the BAML function
- Structured output was returned as an Ash TypedStruct
Try Streaming
For long-running LLM calls, use the streaming action:
{:ok, stream} = MyApp.Assistant
|> Ash.ActionInput.for_action(:say_hello_stream, %{name: "Bob"})
|> Ash.run_action()
stream
|> Stream.each(&IO.inspect/1)
|> Stream.run()Next Steps
- Tutorial 2: Learn about structured output with complex types
- Tutorial 3: Implement tool calling with union types
- Tutorial 4: Build a multi-step agentic loop
See also:
- Type Generation - Deep dive into BAML → Ash type mapping
- Actions - Understanding auto-generated vs manual actions