Quickstart
Build your first BeamWeaver agent and graph in minutes. BeamWeaver gives Elixir applications a native model/tool loop, explicit workflow graphs, streaming, memory, checkpointing, and tracing.
Use an agent when you want the standard model/tool loop. Use a graph when you want explicit nodes, edges, routing, state reducers, checkpoints, interrupts, or custom workflow control.
Install Dependencies
Add BeamWeaver to your Mix project:
def deps do
[
{:beam_weaver, "~> 0.1.16"}
]
end
Then run:
mix deps.get
mix compile
Set Up API Keys
Live provider calls require credentials in BeamWeaver application config. A typical config/runtime.exs loads them from the OS environment:
import Config
config :beam_weaver,
openai: [api_key: System.fetch_env!("OPENAI_API_KEY")],
anthropic: [api_key: System.fetch_env!("ANTHROPIC_API_KEY")],
xai: [api_key: System.fetch_env!("XAI_API_KEY")]
Build A Basic Agent
Start with a small weather tool and a model using BeamWeaver's Elixir tool and agent APIs.
alias BeamWeaver.Agent
alias BeamWeaver.Core.{Message, Tool}
get_weather =
Tool.from_function!(
name: "get_weather",
description: "Get weather for a given city.",
input_schema: %{
"type" => "object",
"properties" => %{
"city" => %{"type" => "string", "description" => "City name"}
},
"required" => ["city"]
},
handler: fn input, _opts ->
city = input["city"] || input[:city]
"It's always sunny in #{city}."
end
)
model =
BeamWeaver.Models.init_chat_model!("openai:gpt-5.4",
temperature: 0.2,
timeout: 30_000
)
{:ok, agent} =
Agent.build(
name: "weather_agent",
model: model,
tools: [get_weather],
system_prompt: "You are a helpful assistant."
)
{:ok, state} =
Agent.invoke(agent, %{
messages: [Message.user("What's the weather in San Francisco?")]
})
state.messages
|> List.last()
|> Message.text()
You can also define the same agent as an application module:
defmodule MyApp.Tools.GetWeather do
use BeamWeaver.Tool
name "get_weather"
description "Get weather for a given city."
schema do
field :city, :string, required: true, description: "City name"
end
@impl true
def invoke(_tool, input, _opts) do
city = Map.get(input, :city) || Map.get(input, "city")
{:ok, "It's always sunny in #{city}."}
end
end
defmodule MyApp.WeatherAgent do
use BeamWeaver.Agent
model BeamWeaver.Models.init_chat_model!("openai:gpt-5.4",
temperature: 0.2,
timeout: 30_000
)
tools do
tool MyApp.Tools.GetWeather
end
system_prompt "You are a helpful assistant."
end
alias BeamWeaver.Core.Message
{:ok, state} =
MyApp.WeatherAgent.invoke(%{
messages: [Message.user("What's the weather in San Francisco?")]
})
state.messages
|> List.last()
|> Message.text()
Add Conversation Memory
Short-term memory is graph state persisted by a checkpointer. Reuse the same checkpointer and thread_id across invocations of the same agent module:
alias BeamWeaver.Checkpoint.ETS, as: CheckpointETS
alias BeamWeaver.Core.Message
checkpointer = CheckpointETS.new()
config = %{"configurable" => %{"thread_id" => "weather-thread-1"}}
{:ok, _first_state} =
MyApp.WeatherAgent.invoke(
%{messages: [Message.user("My name is Ada.")]},
checkpointer: checkpointer,
config: config
)
{:ok, second_state} =
MyApp.WeatherAgent.invoke(
%{messages: [Message.user("What did I tell you my name was?")]},
checkpointer: checkpointer,
config: config
)
second_state.messages
|> List.last()
|> Message.text()
Use BeamWeaver.Checkpoint.ETS for tests and local workflows. Use BeamWeaver.Checkpoint.Ecto with PostgreSQL or SQLite for durable deployments.
Build A Calculator Graph
A calculator workflow is a good fit for explicit graph nodes. For static workflows, define the graph in a module:
defmodule MyApp.CalculatorGraph do
use BeamWeaver.Agent
graph do
node :route, fn state -> %{operation: state.operation} end
node :add, fn state -> %{result: state.a + state.b} end
node :multiply, fn state -> %{result: state.a * state.b} end
node :divide, fn state -> %{result: state.a / state.b} end
edge start(), :route
edge :route, :add, when: %{operation: :add}
edge :route, :multiply, when: %{operation: :multiply}
edge :route, :divide, when: %{operation: :divide}
edge :add, finish()
edge :multiply, finish()
edge :divide, finish()
end
end
{:ok, state} =
MyApp.CalculatorGraph.invoke(%{
operation: :add,
a: 3,
b: 4
})
state.result
The same graph can stream typed events:
{:ok, events} =
MyApp.CalculatorGraph.stream_events(%{operation: :multiply, a: 6, b: 7})
Enum.each(events, fn envelope -> IO.inspect(envelope.event) end)
Use BeamWeaver.Graph builder functions when the topology is generated from configuration at runtime instead of written as an application module.
For a graph-backed model/tool loop, use Agents . Agents compile to graphs, so you can embed an agent graph inside a larger workflow when you need a hybrid deterministic and agentic system.
Functional Style
Use ordinary Elixir functions for local control flow, and switch to BeamWeaver.Graph when you need checkpointing, interrupts, streaming, state history, or orchestration metadata.
defmodule MyApp.Calculator do
def run(%{operation: :add, a: a, b: b}), do: {:ok, %{result: a + b}}
def run(%{operation: :multiply, a: a, b: b}), do: {:ok, %{result: a * b}}
def run(%{operation: :divide, a: a, b: b}), do: {:ok, %{result: a / b}}
end
Build A Research Agent
For a more realistic agent, add a tool that fetches approved URLs and reuse a checkpointer for conversation state:
alias BeamWeaver.Checkpoint.ETS, as: CheckpointETS
alias BeamWeaver.Core.Message
defmodule MyApp.Tools.FetchTextFromURL do
use BeamWeaver.Tool
name "fetch_text_from_url"
description "Fetch UTF-8 text from an approved URL."
schema do
field :url, :string, required: true
end
@impl true
def invoke(_tool, input, _opts) do
url = Map.get(input, :url) || Map.get(input, "url")
case URI.parse(url) do
%URI{scheme: "https", host: "www.gutenberg.org"} ->
{:ok, response} = Req.get(url, receive_timeout: 120_000)
{:ok, response.body}
_other ->
{:error, "URL is not allowed"}
end
end
end
defmodule MyApp.ResearchAgent do
use BeamWeaver.Agent
model BeamWeaver.Models.init_chat_model!("anthropic:claude-sonnet-4-6",
temperature: 0.5,
timeout: 120_000
)
tools do
tool MyApp.Tools.FetchTextFromURL
end
system_prompt """
You are a literary data assistant.
Use fetch_text_from_url when you need source text. Do not invent exact line
counts or positions unless a tool or graph node computed them.
"""
end
checkpointer = CheckpointETS.new()
config = %{"configurable" => %{"thread_id" => "great-gatsby"}}
{:ok, state} =
MyApp.ResearchAgent.invoke(
%{
messages: [
Message.user("""
Fetch https://www.gutenberg.org/files/64317/64317-0.txt and summarize the text.
If you cannot verify exact counts, say so.
""")
]
},
checkpointer: checkpointer,
config: config
)
state.messages
|> List.last()
|> Message.text()
Exact line counting over a large file should be done by deterministic code, a retriever/indexing workflow, or a graph node, not by asking a model to count tokens in its context window.