The BEAM Was Ready for AI Agents. Were We?
Elixir's concurrency model didn't anticipate AI agents — it just described them, thirty years early.
The language model doesn’t know it’s running on the BEAM. It doesn’t know that the process supervising it has a restart strategy, or that the message queue sitting between it and the outside world is the same primitive Joe Armstrong was reaching for in the late 1980s when he needed telephone switches to stop failing. It doesn’t know any of this. But it benefits from all of it.
That’s the thing nobody is saying loudly enough in 2026: Elixir didn’t need to be retrofitted for the age of AI agents. The primitives were already there. We just needed to notice.
What Even Is an AI Agent?
Before we get into the Elixir angle, a quick level-set — because “AI agent” has been stretched to cover everything from a chatbot with a system prompt to fully autonomous software that browses the web, executes code, and coordinates sub-tasks.
For our purposes: an AI agent is a process that receives messages, does work (usually involving calls to a language model), maintains some state, and can spawn or coordinate with other processes. It may run for seconds or months. It needs to fail gracefully when the upstream API times out. And it needs someone to notice when it goes quiet.
This is not a new problem. This is OTP.
GenServer Is the Agent Loop
When you strip away the hype, an agent is a stateful process with a message-driven interface. In Elixir, that’s a GenServer. Here’s a functional agent loop in about twenty lines:
defmodule ConversationAgent do
use GenServer
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def init(opts) do
{:ok, %{history: [], system_prompt: Keyword.get(opts, :system, "You are helpful.")}}
end
def handle_call({:chat, user_message}, _from, state) do
messages = state.history ++ [%{role: "user", content: user_message}]
{:ok, response} = MyLLM.complete(messages, system: state.system_prompt)
new_history = messages ++ [%{role: "assistant", content: response}]
{:reply, response, %{state | history: new_history}}
end
end
What you get for free: backpressure (the caller blocks until the LLM responds), state isolation (no shared mutable state), and a clean testable interface. What OTP layers on top: supervision, hot code reloading, and distributed process registration across a cluster.
There’s one thing worth being clear about upfront: when a supervised process crashes and restarts, it calls init/1 fresh. Conversation history doesn’t survive. OTP’s position is that state recovery is your responsibility — persist to ETS, Postgres, or a state store and reload in init/1. What you don’t get is what Python frameworks often give you silently: the illusion of persistent in-memory state that evaporates on any deploy or crash. OTP is honest about the tradeoff. That honesty is a feature.
Supervision Trees for Agent Reliability
The more interesting story is supervision. An agent that runs in production will fail. The LLM API will time out. The network will blip. Some edge case in your prompt formatting will crash the process. The question isn’t whether your agent will fail — it’s whether your system notices and recovers.
OTP’s supervision trees give you a structured answer. A Supervisor with :one_for_one means each agent process is independently monitored; a crash in one doesn’t cascade. A DynamicSupervisor lets you spin up agents on demand — one per user session, one per incoming webhook, one per document in a processing queue:
DynamicSupervisor.start_child(AgentSupervisor, {
ConversationAgent,
name: {:via, Registry, {AgentRegistry, user_id}},
system: "You assist #{user_id}."
})
Each agent is independently supervised. Kill one — the others keep running. The supervisor notices, logs the crash, and restarts. This is the multi-tenant AI agent infrastructure that Python teams are currently building from scratch on top of asyncio, under deadline pressure, hoping they got the edge cases right.
The Nx and Bumblebee Layer
There’s a legitimate counterargument here: Elixir’s ML ecosystem is thin. PyTorch runs the world. If you need to fine-tune a model or run frontier research, you’re in Python. That’s not changing.
But Nx and Bumblebee change the calculus for inference. Nx compiles to XLA and runs on GPU and TPU. Bumblebee provides pre-trained model support — transformers, embeddings, image classification — integrated naturally with Elixir’s process model. You can run an embedding model inside a supervised process, query it from a Phoenix controller, and have it automatically restart on failure, without leaving the Elixir runtime.
This isn’t PyTorch. It’s not trying to be. But for the 80% of AI production work that is inference against existing models — not training, not research — the Nx/Bumblebee stack is increasingly viable.
The Take
Here’s where I’ll plant a flag: the BEAM’s advantage in the agentic AI era is about orchestration durability, not inference performance. The hard part of running AI agents in production isn’t calling the LLM — it’s keeping hundreds or thousands of stateful, long-running processes alive, recovering from failure without losing context, and scaling horizontally without reimplementing distributed systems primitives you should already have.
Elixir has those primitives. Python doesn’t. The Python ecosystem is building them now, under time pressure, into frameworks that will accrete technical debt proportional to their popularity.
The engineers who understand OTP — who can reach for a supervision tree the way most people reach for a try/catch — are building something genuinely hard to replicate. The BEAM was ready. The question is whether we had the clarity to notice.
Enjoy this issue?
Get ElixirLens every Monday. Sharp Elixir editorial + the tech trends shaping it.
Free. No spam. Unsubscribe anytime.