packages/docs/architecture.mdx
Eliza is the agent. elizaOS is the agentic operating system it runs on. The same backend ships to CLI, web, desktop (Electrobun), and mobile (Capacitor) through a layered architecture. This page covers entry points, runtime lifecycle, plugin loading, the API surface, and build tooling.
<Info> Source paths like `packages/app-core/src/...` and `packages/agent/src/...` live in the `eliza/` git submodule. Initialize it with `bun run setup:upstreams` or `git submodule update --init --recursive` to browse these files locally. </Info>+---------------------------------------------------------------+
| Frontend Layer |
| React SPA (Vite) + Capacitor (iOS/Android) + Electrobun (Desktop) |
| Tabs: Chat, Character, Wallets, Knowledge, Social, Apps, |
| Settings, Advanced (Plugins, Skills, Triggers, DB, ...) |
+----------------------------+----------------------------------+
| HTTP + WebSocket
v
+---------------------------------------------------------------+
| CLI Layer |
| Commander-based CLI (entry.ts -> run-main.ts) |
| Commands: start, setup, configure, config, update, ... |
| Profile system, dotenv loading, log-level control |
+----------------------------+----------------------------------+
|
v
+---------------------------------------------------------------+
| Runtime Layer (elizaOS) |
| AgentRuntime + Plugin system |
| 12 core plugins + optional plugins + Eliza plugin |
| Providers, Actions, Services, Evaluators |
+----------------------------+----------------------------------+
|
v
+---------------------------------------------------------------+
| API Server Layer |
| Node.js HTTP server |
| Dev: port 31337 (ELIZA_API_PORT) |
| Prod: port 2138 (ELIZA_PORT, shared with dashboard) |
| REST endpoints (/api/*) + WebSocket (/ws) |
| SSE log streaming, static UI assets |
+---------------------------------------------------------------+
|
v
+---------------------------------------------------------------+
| Storage & Config Layer |
| ~/.eliza/eliza.json (primary runtime config) |
| PGLite (default) or PostgreSQL (database) |
| ~/.eliza/workspace/ (agent files, skills) |
| ~/.eliza/plugins/ (custom, ejected, installed) |
+---------------------------------------------------------------+
Note: Source paths like
packages/app-core/src/...andpackages/agent/src/...referenced throughout this page live in the elizaOS submodule ateliza/. Runbun run setup:upstreamsto populate the submodule for local inspection.
Eliza has two primary entry points, plus a dev-server variant:
eliza/packages/app-core/src/entry.ts)The CLI entry point is the bootstrap for all Eliza operations. Built by tsdown into dist/entry.js and invoked by the eliza.mjs wrapper script.
The boot sequence is:
eliza (visible in ps output)--no-color, --debug, --verbose flags by setting NO_COLOR, FORCE_COLOR, and LOG_LEVEL in process.envNODE_LLAMA_CPP_LOG_LEVEL to match the chosen log level, suppressing noisy tokenizer warnings at default verbosity--profile <name> from process.argv via parseCliProfileArgs(), apply the profile environment via applyCliProfileEnv()./cli/run-main.ts and call runCli(process.argv)// eliza/packages/app-core/src/entry.ts — simplified bootstrap
process.title = "eliza";
const parsed = parseCliProfileArgs(process.argv);
if (parsed.profile) applyCliProfileEnv({ profile: parsed.profile });
import("./cli/run-main")
.then(({ runCli }) => runCli(process.argv))
run-main.ts continues by loading .env files via dotenv, normalizing API key environment variables (e.g., Z_AI_API_KEY to ZAI_API_KEY), building the Commander program tree, and dispatching to the requested subcommand.
eliza/packages/app-core/src/index.ts)The public API surface exported by the elizaai npm package. It re-exports:
eliza/packages/app-core/src/config/types.ts -- the full ElizaConfig type and its nested schemasRESTART_EXIT_CODE, requestRestart(), setRestartHandler(), and the RestartHandler typeThis allows other packages (e.g., the Electrobun app, the dev-server) to programmatically control the runtime without importing internal modules.
eliza/packages/app-core/src/runtime/dev-server.ts)A combined dev server that starts the elizaOS runtime in headless mode and wires it into the API server. Used during development (bun run dev):
.env files for parity with CLI modestartEliza({ headless: true })The startEliza() function in eliza/packages/app-core/src/runtime/eliza.ts orchestrates the full lifecycle from config loading through agent operation.
During normal operation, the runtime:
Graceful shutdown is triggered by SIGINT or SIGTERM:
runtime.stop() to close database connections, cancel timers, and clean upRestart is pluggable via setRestartHandler():
| Environment | Handler | Behavior |
|---|---|---|
| CLI (default) | Exit with code 75 | The runner script (eliza/packages/app-core/scripts/run-node.mjs) catches exit code 75, optionally rebuilds if sources changed, and relaunches |
| Dev-server / API | Hot-swap | Stops the current runtime, reloads config from disk, creates a new AgentRuntime, and swaps the reference in the API server |
| Electrobun | AgentManager.restart() | Uses the Electrobun app's native restart mechanism |
Eliza extends elizaOS through several integration points:
The createElizaPlugin() function (eliza/packages/app-core/src/runtime/eliza-plugin.ts) creates a standard elizaOS Plugin that bridges Eliza-specific functionality into the runtime:
Providers (injected into every LLM context):
channelProfileProvider -- channel-specific personality profilesworkspaceProvider -- workspace file contextadminTrustProvider -- admin trust level for privileged operationsautonomousStateProvider -- current autonomous agent statesessionKeyProvider -- session bridge for cross-channel continuityuiCatalogProvider -- available UI components the agent can rendercustomActionsProvider -- user-defined custom action descriptionsActions (agent-invokable operations):
restartAction -- trigger a runtime restartsendMessageAction -- send a proactive message to a channeltriggerTaskAction -- execute a trigger taskemoteAction -- play an emote animation on the 3D avatar~/.eliza/workspace/custom-actions/Init hooks:
PLAY_EMOTE action when character.settings.DISABLE_EMOTES is setEvery plugin is wrapped with error boundaries before registration (wrapPluginWithErrorBoundary()):
init() wrapper -- catches crashes during plugin initialization, logs the error, and allows the agent to continue in degraded modeget() calls (invoked every conversation turn), returns an error marker instead of crashing the agentActions are not wrapped because elizaOS's own action dispatch already has error boundaries.
The runtime applies compatibility patches after creation:
installRuntimeMethodBindings() -- binds getConversationLength() to the runtime instance, fixing private-field access errors when plugins store and later invoke the method without a receiverinstallActionAliases() -- adds backward-compatible action aliases (e.g., CODE_TASK maps to START_CODING_TASK)Plugin resolution (resolvePlugins()) handles three tiers of plugins through a multi-stage pipeline:
collectPluginNames() builds a Set<string> of package names to load:
CORE_PLUGINS)config.plugins.allow) -- these are additive, not exclusiveCHANNEL_PLUGIN_MAPANTHROPIC_API_KEY triggers @elizaos/plugin-anthropic)config.plugins.entries with enabled !== falseconfig.features)config.plugins.installsplugin-shell if config.features.shellEnabled === falseScan filesystem directories for additional plugins:
~/.eliza/plugins/ejected/) -- override npm/core versions~/.eliza/plugins/custom/) -- user-created pluginsconfig.plugins.load.paths -- additional directoriesEach immediate subdirectory is treated as a plugin package. The plugin name comes from package.json or the directory name. Deny-list and core-collision filtering is applied.
All plugins are loaded in parallel via Promise.all(). For each plugin:
node_modulesimport() by package nameThe findRuntimePluginExport() function handles multiple export patterns:
export default plugin (preferred)export const plugin = ...Pluginmodule.exports = { name, description })After loading, the pipeline:
diagnoseNoAIProvider())Two plugins require special ordering to avoid race conditions:
plugin-sql -- registered first so the database adapter is ready before other plugins call runtime.db. Includes PGLite corruption recovery (backs up corrupt data, resets, retries once).plugin-local-embedding -- registered second so its TEXT_EMBEDDING handler (priority 10) is available before the cloud plugin's handler (priority 0), preventing unintended paid API calls during startup.Eliza inherits elizaOS's three-part extension model:
Providers inject context into every LLM prompt. Each provider returns a ProviderResult containing text that the agent sees when composing responses.
interface Provider {
name: string;
description: string;
get(runtime: IAgentRuntime, message: Memory, state: State): Promise<ProviderResult>;
}
Eliza registers 7+ providers through the Eliza plugin, plus any providers from loaded plugins.
Actions are operations the agent can invoke based on conversation context. Each action defines:
name -- unique identifier (e.g., RESTART, MESSAGE, PLAY_EMOTE)similes -- alternative names the agent can usevalidate() -- whether the action can run in the current contexthandler() -- the execution logicexamples -- few-shot examples for the LLMEvaluators run after each conversation turn to assess agent performance and update state. They are defined by plugins and can trigger actions, update memory, or modify agent behavior.
Eliza uses @elizaos/plugin-sql as the database adapter with two backend options:
| Backend | Config | Use Case |
|---|---|---|
| PGLite (default) | database.provider: "pglite" | Zero-config embedded PostgreSQL. Data stored in ~/.eliza/workspace/.eliza/.elizadb/ |
| PostgreSQL | database.provider: "postgres" | External PostgreSQL server for production deployments |
PGLite includes automatic corruption recovery: if initialization fails with a known error pattern (WASM abort, migration schema failure), the data directory is backed up and recreated.
Local embeddings for memory search use @elizaos/plugin-local-inference with configurable parameters:
| Setting | Default | Description |
|---|---|---|
embedding.model | nomic-embed-text-v1.5.Q5_K_M.gguf | GGUF model file name |
embedding.gpuLayers | auto (Apple Silicon), 0 (others) | GPU acceleration layers |
embedding.dimensions | 768 | Embedding vector dimensions |
embedding.contextSize | Auto-detected | Maximum input token context |
On macOS with Apple Silicon, Metal GPU acceleration is enabled by default with mmap disabled to prevent known compatibility issues.
The trajectory logger (trajectories) records agent decision paths for debugging and reinforcement learning. The runtime waits up to 3 seconds for the service to register, then enables it by default.
The API server (eliza/packages/app-core/src/api/server.ts) is a raw Node.js http.createServer() instance -- no Express or framework overhead.
The server maintains a ServerState object tracking:
AgentRuntime reference (swappable on restart)not_started | starting | running | paused | stopped | restarting | errorRoutes are organized into domain-specific handler modules:
| Module | Prefix | Purpose |
|---|---|---|
agent-admin-routes.ts | /api/agent/* | Agent name, bio, system prompt, style, examples |
agent-lifecycle-routes.ts | /api/agent/* | Start, stop, restart, status, export/import |
auth-routes.ts | /api/auth/* | Token validation, pairing flow |
autonomy-routes.ts | /api/agent/autonomy | Autonomous mode state |
character-routes.ts | /api/character/* | Character CRUD, AI-assisted generation |
cloud-routes.ts | /api/cloud/* | Eliza Cloud integration, billing, agents |
cloud-compat-routes.ts | /api/cloud/compat/*, /api/cloud/v1/* | Cloud backend proxy |
connector-routes.ts | /api/connectors/* | Messaging connector configuration |
conversation-routes.ts | /api/conversations/* | Web-chat conversations and messaging |
inbox-routes.ts | /api/inbox/* | Canonical cross-channel inbox |
knowledge-routes.ts | /api/documents/* | Document upload, search, RAG management |
memory-routes.ts | /api/memory/*, /api/context/* | Memory persistence and context search |
models-routes.ts | /api/models/* | Model provider discovery and selection |
onboarding-routes.ts | /api/onboarding/* | First-run setup flow |
plugin-routes.ts | /api/plugins/* | Plugin management and installation |
registry-routes.ts | /api/registry/* | Plugin registry browsing |
sandbox-routes.ts | /api/sandbox/* | Container-based code execution |
secrets-routes.ts | /api/secrets/* | API key and credential management |
skills-routes.ts | /api/skills/* | Skill catalog and marketplace |
training-routes.ts | /api/training/* | Fine-tuning orchestration |
trigger-routes.ts | /api/triggers/* | Trigger task management |
workbench-routes.ts | /api/workbench/* | Tasks, todos, canonical dashboard |
automations-routes.ts | /api/automations/*, /api/workflow/* | Canonical automations and workflows |
wallet-routes.ts | /api/wallet/* | Wallet addresses, balances, trading |
apps-routes.ts | /api/apps/* | elizaOS app marketplace |
When ELIZA_API_TOKEN is set (or auto-generated for non-loopback binds), all API requests must include a valid bearer token:
Authorization: Bearer <token>
If the API is bound to a non-loopback address (ELIZA_API_BIND is not 127.0.0.1 or localhost) and no token is set, the server auto-generates a temporary token and logs it to stderr.
The API server exposes a WebSocket endpoint at /ws on the same port as the API (31337 by default, configurable via ELIZA_API_PORT).
| Event Type | Direction | Description |
|---|---|---|
status | Server -> Client | Agent status broadcast (every 5 seconds) |
agent_event | Server -> Client | Runtime autonomy loop events with run/seq tracking |
heartbeat_event | Server -> Client | Agent heartbeat with status, duration, channel info |
training_event | Server -> Client | Fine-tuning progress updates |
proactive-message | Server -> Client | Agent-initiated messages |
conversation-updated | Server -> Client | Conversation metadata changed |
ping / pong | Bidirectional | Keepalive mechanism |
active-conversation | Client -> Server | Tell the server which conversation the UI is viewing |
All server-sent events use a standard envelope:
{
type: "agent_event" | "heartbeat_event" | "training_event",
version: 1,
eventId: string,
ts: number,
runId?: string,
seq?: number,
stream?: string,
sessionKey?: string,
agentId?: string,
roomId?: UUID,
payload: object
}
Events are buffered in a ring buffer on the server so newly connected clients can receive recent history.
Eliza's eliza/packages/app-core/src/services/ directory contains standalone service modules:
| Service | File | Purpose |
|---|---|---|
| Plugin Installer | plugin-installer.ts | Install, uninstall, and manage plugins from npm and git |
| Self-Updater | self-updater.ts | Detect install method and run the appropriate upgrade command |
| Update Checker | update-checker.ts | Query npm registry for new versions on the configured channel |
| Update Notifier | update-notifier.ts | Fire-and-forget background check that prints a one-line notice |
| App Manager | app-manager.ts | Launch and manage elizaOS marketplace apps |
| Sandbox Engine | sandbox-engine.ts | Container-based code execution sandboxing |
| Sandbox Manager | sandbox-manager.ts | Docker container lifecycle for sandboxed execution |
| Registry Client | registry-client.ts | elizaOS plugin registry API client |
| Skill Marketplace | skill-marketplace.ts | Search, install, and manage skills from the marketplace |
| Training Service | training-service.ts | Fine-tuning orchestration with Ollama integration |
| Agent Export | agent-export.ts | Export agent state (character, knowledge, config) as portable archives |
| Version Compat | version-compat.ts | Detect version skew between @elizaos/core and plugins |
| Core Eject | core-eject.ts | Eject core plugins for local modification |
| Plugin Eject | plugin-eject.ts | Eject installed plugins for local overrides |
| Skill Catalog | skill-catalog-client.ts | Skills registry API client for browsing and installing |
| MCP Marketplace | mcp-marketplace.ts | MCP server discovery and marketplace integration |
Eliza resolves configuration through a priority cascade:
Highest priority
1. CLI flags (--debug, --verbose, --no-color, --profile)
2. Environment variables (ELIZA_*, ANTHROPIC_API_KEY, etc.)
3. .env file (loaded by dotenv in run-main.ts)
4. Config file (~/.eliza/eliza.json)
5. Profile-specific overrides (--profile <name>)
6. Built-in defaults (hardcoded in Zod schemas)
Lowest priority
The state directory (~/.eliza/) is resolved via:
ELIZA_STATE_DIR environment variable (if set)~/.eliza/ ($HOME/.eliza/)The config file path follows a similar pattern:
ELIZA_CONFIG_PATH environment variable (if set)<stateDir>/eliza.jsonEliza uses a Zod-validated JSON configuration file (eliza.json):
ELIZA_API_TOKEN (bearer token on all /api/* requests)auth message after connection (or via a bearer token in the upgrade handshake header)Three sandbox modes control code execution isolation:
| Mode | Description |
|---|---|
off | No sandboxing |
non-main | Sandbox all sessions except the main session (default when enabled) |
all | Sandbox all sessions including main |
The sandbox uses Docker container isolation with configurable image, memory limits, CPU caps, and network restrictions. Sandbox events are recorded to a SandboxAuditLog for security review.
config.plugins.deny) blocks specific plugins from loadingELIZA_API_TOKEN is stripped from config patches)config.env are hydrated into process.env at startup**** or sk-a...z123)@elizaos/core secrets stack (ENABLE_SECRETS_MANAGER / SECRETS service) provides runtime secret storage and scopingThe same backend serves all platforms:
| Platform | Delivery | Notes |
|---|---|---|
| CLI | eliza start | Direct Node.js process, runner script handles restart exit codes |
| Web | Browser at http://localhost:2138 (UI port) | Vite dev proxy forwards API calls to port 31337 |
| Desktop | Electrobun wrapper | Bundles the API server + UI, uses the Electrobun updater for native updates |
| iOS | Capacitor | Connects to local/remote API |
| Android | Capacitor | Connects to local/remote API |
The frontend API client (apps/app/src/api-client.ts) is a thin fetch wrapper plus WebSocket for real-time chat and events.
The Gateway runs on port 18789 (default) and provides:
/v1/chat/completions, /v1/responses)| Tool | Purpose |
|---|---|
| tsdown | TypeScript bundler (builds src/ into dist/) |
| Vite | Frontend dev server and production bundler |
| Biome | Linter and formatter (lint, format scripts) |
| Vitest | Unit and integration testing |
| TypeScript | Type checking (tsc --noEmit) |
The monorepo uses Bun workspaces. Core runtime packages live inside the eliza/ git submodule:
eliza/
eliza/ # elizaOS submodule (core runtime packages)
packages/
app-core/ # Core runtime, CLI, API, services (source of truth)
src/
cli/ # Commander CLI commands
api/ # Dashboard API server
runtime/ # Agent loader, plugin system
config/ # Plugin auto-enable, config schemas
connectors/ # Connector integration code
services/ # Business logic (plugin installer, updater, etc.)
agent/ # Upstream elizaOS agent (core plugins, auto-enable maps)
ui/ # Shared UI component library
shared/ # Shared utilities
plugins/ # elizaOS plugins (git submodules)
apps/
app/ # React SPA frontend (Vite)
electrobun/ # Electrobun desktop wrapper
browser-bridge/ # Browser extension bridge
homepage/ # Marketing site
deploy/
systemd/ # Bare-metal systemd deployment
docs/ # Mintlify documentation
scripts/ # Build, dev, and utility scripts
skills/ # Agent skills directory
test/ # E2E and integration tests
# Full build: TypeScript + build info + frontend
bun run build
# → tsdown (src/ → dist/)
# → scripts/write-build-info.ts (version metadata)
# → cd apps/app && bun run build (Vite frontend)
The eliza/packages/app-core/scripts/rt.mjs helper resolves the best available runtime (bun or Node.js with tsx) for running TypeScript files directly during development.
Eliza requires Node.js 22+ (specified in package.json engines field). The node-llama-cpp dependency for local embeddings benefits from native compilation on the target platform.