Overview
Most agent frameworks today are built in TypeScript or Python. They are mature, well-supported, and familiar. But there is a growing argument that the BEAM VM, the runtime behind Erlang and Elixir, is a better foundation for running agent systems at scale.
I came across Jido, an open-source Elixir agent framework that shipped version 2.0 in March 2026. It makes some strong claims about why the BEAM is the right runtime for agents. Some of those claims are worth understanding, even if you have no plans to leave TypeScript.
TLDR: Jido is an Elixir agent framework built on the BEAM VM. The BEAM gives agents multi-core concurrency, per-process fault isolation, and supervision out of the box. Jido adds a command-pattern architecture that separates agent decisions from side effects. That last idea is the most useful takeaway for TypeScript developers.
Sections
- What Is Jido
- Why These Patterns Matter for Agent Systems
- The BEAM Runtime: Why Jido Chose Elixir
- So Why Use Jido?
- Official References
What Is Jido
Jido is an Elixir framework for orchestrating agents, built on tried and tested patterns and designed to take advantage of the BEAM VM. It shipped version 2.0 in March 2026. Here are the core ideas from their release post.
Agents as Data
A Jido agent is a struct with state, actions, and tools. There is no hidden mutation or implicit state management. The agent is just data.
The Command Pattern
All agent logic flows through a single function, cmd/2. You pass in an agent and an action. You get back an updated agent and a list of directives, typed data structures that describe side effects. The agent never executes side effects directly. It describes what should happen, and a separate runtime layer executes it.
This is the command pattern. Decisions are pure. Execution is separate.
Pluggable Strategies
How an agent processes actions is controlled by a strategy module. Jido ships with Direct (sequential execution) and FSM (state machines with transition guards). Jido AI adds LLM-based strategies like ReAct, Chain-of-Thought, and Tree-of-Thoughts.
The key point is that the strategy is swappable. The agent’s logic, tools, and state stay the same. Only the execution pattern changes.
Schema-Validated Actions as LLM Tools
Actions are defined once with a typed schema. They work both as executable code and as tool definitions the LLM can call. There is no separate tool definition to maintain.
Why These Patterns Matter for Agent Systems
The most useful ideas from Jido are not Elixir-specific. They are architectural patterns that can improve any agent system. Here is what they look like in TypeScript and why you should care.
Separate Decisions from Side Effects
This is the biggest takeaway. In a typical TypeScript agent, the tool call happens inside the agent loop. The decision and the execution are interleaved.
Jido’s approach is different. The agent’s decision function is pure. It takes state and an action, returns new state and a list of commands. A separate layer executes the commands.
Note: This is simplified pseudocode to illustrate the concept, not actual implementation.
interface Directive {
type: string;
payload: Record<string, unknown>;
}
interface CommandResult {
state: AgentState;
directives: Directive[];
}
// Pure function. No network calls, no database writes, no side effects.
function decide(state: AgentState, action: Action): CommandResult {
const newState = { ...state, orderCount: state.orderCount + 1 };
const directives: Directive[] = [
{ type: "send_email", payload: { to: "[email protected]", subject: "Order confirmed" } },
{ type: "update_database", payload: { table: "orders", id: "123", status: "confirmed" } },
];
return { state: newState, directives };
}
// Separate runtime executes directives after the decision is made.
async function execute(directives: Directive[]): Promise<void> {
for (const directive of directives) {
await handlers[directive.type](directive.payload);
}
}
This separation buys you several things.
Testability Without Mocks
When agent decisions are pure functions, testing does not require mocking HTTP clients, databases, or LLM providers. You pass in state and an action. You assert on the returned state and directives. No setup, no teardown, no flaky network dependencies.
// Test the decision, not the execution
test("processes order and increments count", () => {
const state: AgentState = { orderCount: 0 };
const result = decide(state, { type: "process_order", orderId: "123" });
expect(result.state.orderCount).toBe(1);
expect(result.directives).toContainEqual({
type: "send_email",
payload: expect.objectContaining({ to: "[email protected]" }),
});
});
Compare this to testing an agent where the tool call triggers a real HTTP request inside the loop. You need to mock the dependency, which couples your test to the tool’s implementation rather than the agent’s decision.
Observability for Free
When side effects are described as data before execution, you get observability properties without extra work:
- Log every decision before execution. Every directive is a data structure. Log it, inspect it, or require approval before it runs.
- Replay agent decisions. Feed the same state and action into the decision function. Get the same directives back. Deterministic replay without hitting external systems.
- Dry-run mode. Run the decision logic, collect the directives, skip execution. Useful for testing new strategies against production-like input.
- Audit trails. Every directive is a record of what the agent intended to do. Combined with execution results, you get a complete decision history.
Interleaved (typical TypeScript agent)
┌─────────────────────────────────────────────────┐
│ Agent Loop │
│ Think ──▶ Call Tool ──▶ Get Result ──▶ Think │
│ ▲ │
│ Side effect happens here. │
│ Decision and execution are coupled.│
└─────────────────────────────────────────────────┘
Separated (command pattern)
┌──────────────────────┐ ┌─────────────────────┐
│ Decision Layer │ │ Execution Layer │
│ │ │ │
│ decide(state, action)│───▶│ Execute directives │
│ Returns: │ │ Log each one │
│ new state │ │ Approve if needed │
│ directives (data) │ │ Then execute │
│ │ │ │
│ Pure. Testable. │ │ Observable. Audited.│
└──────────────────────┘ └─────────────────────┘
This connects directly to the observability requirements from Part 4 of the agent series. The same primitives, decision logging, replay, audit trails, become easier to implement when the architecture enforces separation.
The BEAM Runtime: Why Jido Chose Elixir
The patterns above work in any language. But Jido chose Elixir for a reason. The BEAM VM, the runtime behind Erlang and Elixir, was built by Ericsson in the 1980s for telecom switching systems. It has properties that are hard to replicate in Node.js.
Where Node.js Is Strong
Node.js handles I/O-bound concurrent work well. When an agent calls an LLM API or queries a database, await yields control back to the event loop. Other agents continue running. A single Node process can manage thousands of concurrent agents all waiting on network responses.
For most agent workloads today, where the majority of time is spent waiting on LLM API responses, Node’s concurrency model works fine. TypeScript also has the largest ecosystem of AI/LLM libraries, the widest provider support, and a far larger developer community than Elixir.
Where the BEAM Has a Structural Advantage
Multi-core utilization. Node.js runs JavaScript on a single core by default. Using additional cores requires worker_threads or the cluster module, which add complexity around state sharing and lifecycle management. The BEAM runs a scheduler per CPU core and distributes processes across all of them automatically. No application code changes needed.
This matters when agents do non-trivial local computation: parsing large LLM responses, running complex decision trees, serializing state. These are CPU-bound operations that block the Node event loop but get time-sliced across cores on the BEAM.
Fault isolation. In Node, all agents share a single process. An unhandled error, a memory leak in one agent’s closure, or an out-of-memory condition affects every agent in that process. On the BEAM, each agent runs in its own lightweight process with its own heap and garbage collector. One agent crashing does not affect others.
Node.js
┌────────────────────────────────────────────────┐
│ Single Node Process │
│ │
│ Agent A Agent B Agent C Agent D │
│ (promise) (promise) (promise) (promise) │
│ │
│ Shared event loop, shared memory, shared fate │
│ One OOM or unhandled error kills all agents │
└────────────────────────────────────────────────┘
BEAM
┌────────────────────────────────────────────────┐
│ BEAM VM (all cores) │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │Agent A │ │Agent B │ │Agent C │ ... │
│ │Own heap │ │Own heap │ │Own heap │ │
│ │Own GC │ │Own GC │ │Own GC │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ Agent B crashes. Supervisor restarts it. │
│ Agents A and C are unaffected. │
└────────────────────────────────────────────────┘
Supervision trees. Processes are organized into hierarchies where parent processes monitor children and restart them on failure. In Node, you would need something like PM2 or custom retry logic. On the BEAM, this is built into the runtime.
Built-in distribution. The BEAM natively supports connecting VMs across machines. Processes on different nodes communicate directly without an external message broker. In TypeScript, scaling agents across machines typically requires a distributed data store (Redis, DynamoDB), a message queue, or a messaging protocol like gRPC.
An Important Caveat
Whether these advantages matter in practice depends on the workload. If your agents spend 95% of their time waiting on LLM API responses, the multi-core and CPU scheduling benefits are largely theoretical. Without instrumenting a real agent system and measuring where time is actually spent, the runtime comparison is an architectural argument, not an empirical one.
So Why Use Jido?
If you already know Elixir and are comfortable with its concurrency primitives, Jido looks like a well-designed framework. The architecture is clean, the separation of concerns is strong, and the BEAM gives you runtime properties that would take significant effort to replicate in Node.js. For teams already invested in the Elixir ecosystem, it is a compelling choice for building agent systems.
If you are coming from TypeScript, the calculus is different. Node.js handles I/O-bound agent workloads well, the ecosystem of AI libraries is far larger, and the developer community around TypeScript agents is more active. Switching runtimes is a big commitment, and for most agent systems today, the bottleneck is not the runtime. It is the LLM API latency, the quality of your prompts, and the reliability of your orchestration logic.
The reasons to seriously consider Jido and the BEAM come down to scale and resilience. If you are building a system with hundreds of concurrent agents, need hard fault isolation between them, want automatic multi-core utilization without infrastructure complexity, or require agents that stay up for months without restarts, those are problems the BEAM was designed to solve.
For everyone else, the architectural ideas are the real value. The command pattern, pluggable strategies, and schema-validated actions are patterns you can adopt in TypeScript today to make your agents more testable, observable, and maintainable.