Installation
Install the core package. LLM providers and channel adapters are peer dependencies — install only what you use.
npm install botinabox
# Providers (pick one or more)
npm install @anthropic-ai/sdk # Anthropic Claude
npm install openai # OpenAI GPT
# Ollama requires no extra package — just a running Ollama server
# Connectors (optional)
npm install googleapis # Google Gmail & Calendarlatticesql, uuid, cron-parser, ajv, and yaml — all installed automatically.Quick Start
Create a database, register a provider, set up an agent, and run a task.
import {
HookBus,
DataStore,
defineCoreTables,
AgentRegistry,
TaskQueue,
RunManager,
ProviderRegistry,
ModelRouter,
ApiExecutionAdapter,
} from 'botinabox';
import createAnthropicProvider from 'botinabox/anthropic';
// 1. Initialize
const hooks = new HookBus();
const db = new DataStore({ dbPath: './data/bot.db', wal: true, hooks });
defineCoreTables(db);
await db.init();
// 2. Register a provider
const providers = new ProviderRegistry();
providers.register(
createAnthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY! })
);
const router = new ModelRouter(providers, {
default: 'claude-sonnet-4-6',
});
// 3. Register an agent
const agents = new AgentRegistry(db, hooks);
const agentId = await agents.register({
slug: 'assistant',
name: 'Assistant',
adapter: 'api',
});
// 4. Create and execute a task
const tasks = new TaskQueue(db, hooks);
const runs = new RunManager(db, hooks);
const taskId = await tasks.create({
title: 'Summarize the quarterly report',
assignee_id: agentId,
priority: 3,
});
const runId = await runs.startRun(agentId, taskId, 'api');
const adapter = new ApiExecutionAdapter(router);
const result = await adapter.execute({
agent: { id: agentId, model: 'claude-sonnet-4-6' },
task: { description: 'Summarize the quarterly report.' },
});
await runs.finishRun(runId, {
exitCode: result.exitCode,
output: result.output,
usage: result.usage,
});Configuration
Bot in a Box can be configured with a YAML file. Environment variables are interpolated automatically using ${VAR_NAME} syntax. The config is validated at load time with AJV.
# botinabox.config.yml
data:
path: ./data/bot.db
walMode: true
channels:
slack:
enabled: true
botToken: ${SLACK_BOT_TOKEN}
appToken: ${SLACK_APP_TOKEN}
providers:
anthropic:
enabled: true
apiKey: ${ANTHROPIC_API_KEY}
agents:
- slug: researcher
name: Research Agent
adapter: api
model: smart
budgetMonthlyCents: 5000
models:
default: smart
aliases:
fast: claude-haiku-4-5
smart: claude-sonnet-4-6
powerful: claude-opus-4-6
routing:
conversation: fast
task_execution: smart
classification: fast
fallbackChain: []
budget:
globalMonthlyCents: 100000
warnPercent: 80
schedules:
- slug: daily-report
cron: "0 9 * * *"
timezone: America/New_York
action: agent.wakeup
actionConfig:
agentSlug: researcher
taskTitle: "Generate daily report"HookBus
The HookBus is the central event system. Every layer — channels, orchestration, data, security — communicates through hooks. Handlers are priority-ordered (0–100, lower runs first) and error-isolated.
import { HookBus } from 'botinabox';
const hooks = new HookBus();
// Register a handler (priority 10 = runs early)
hooks.on('task.created', async (ctx) => {
console.log('New task:', ctx.task.title);
}, { priority: 10 });
// Filter-based handler
hooks.on('run.completed', async (ctx) => {
console.log('Run finished with exit code:', ctx.exitCode);
}, { filter: (ctx) => ctx.agentSlug === 'researcher' });
// Fire an event
await hooks.fire('task.created', { task: { title: 'Hello' } });Built-in events include task.created, run.completed, budget.exceeded, message.inbound, agent.wakeup, schedule.fired, and workflow.completed.
DataStore
DataStore wraps SQLite (via latticesql) with schema-driven tables, CRUD operations, and soft deletes.
import { DataStore, defineCoreTables } from 'botinabox';
const db = new DataStore({
dbPath: './data/bot.db',
wal: true, // WAL mode for concurrent reads
hooks, // HookBus for audit events
});
// Register the 20+ core tables
defineCoreTables(db);
await db.init();
// CRUD operations
const id = await db.insert('agent', {
slug: 'assistant',
name: 'Assistant',
adapter: 'api',
});
const agent = await db.get('agent', id);
const all = await db.query('agent', { adapter: 'api' });
await db.update('agent', id, { name: 'Updated' });
await db.delete('agent', id); // soft deleteAgents
Agents are the core actors. Each agent has a slug, name, execution adapter (api or cli), model preference, and optional budget.
import { AgentRegistry } from 'botinabox';
const agents = new AgentRegistry(db, hooks);
const agentId = await agents.register({
slug: 'researcher',
name: 'Research Agent',
adapter: 'api',
model: 'smart', // model alias
budgetMonthlyCents: 10000, // $100/month
systemPrompt: 'You are a research assistant.',
});
// List all agents
const all = await agents.list();
// Get by slug
const agent = await agents.getBySlug('researcher');Tasks & Runs
Tasks represent work to be done. Runs represent a single execution attempt. A task can have multiple runs (retries, followups).
import { TaskQueue, RunManager } from 'botinabox';
const tasks = new TaskQueue(db, hooks);
const runs = new RunManager(db, hooks);
// Create a task (priority 1-10, lower = higher priority)
const taskId = await tasks.create({
title: 'Analyze the dataset',
assignee_id: agentId,
priority: 3,
context: { datasetPath: './data/input.csv' },
});
// Start a run
const runId = await runs.startRun(agentId, taskId, 'api');
// Finish with result
await runs.finishRun(runId, {
exitCode: 0,
output: 'Analysis complete.',
usage: { inputTokens: 500, outputTokens: 200 },
});Task Queue
The task queue orders work by priority (1–10) and creation time. Tasks can trigger followup tasks, forming chains with a maximum depth of 5 to prevent infinite loops.
// Peek at the next task for an agent
const next = await tasks.peek(agentId);
// Claim and process
const claimed = await tasks.claim(agentId);
if (claimed) {
// Execute...
await tasks.complete(claimed.id, { output: 'Done' });
}
// Create a followup task
await tasks.create({
title: 'Follow-up analysis',
assignee_id: agentId,
priority: 5,
parentTaskId: taskId, // links to parent
});Run Manager
The RunManager coordinates execution. It locks per-agent to prevent concurrent runs, handles retries with exponential backoff, and records complete audit trails.
const runs = new RunManager(db, hooks);
// Start with adapter type
const runId = await runs.startRun(agentId, taskId, 'api');
// The API execution adapter handles the LLM loop
const adapter = new ApiExecutionAdapter(router);
const result = await adapter.execute({
agent: { id: agentId, model: 'smart', systemPrompt: '...' },
task: { description: 'Summarize this document.' },
tools: [], // optional tool definitions
maxIterations: 20, // tool-use loop limit
});
// Record the result
await runs.finishRun(runId, {
exitCode: result.exitCode,
output: result.output,
usage: result.usage,
});Workflows
Workflows are directed acyclic graphs (DAGs) of steps. Steps can depend on other steps, enabling parallel execution where dependencies allow. Context flows between steps via interpolation.
import { WorkflowEngine } from 'botinabox';
const workflows = new WorkflowEngine(db, hooks);
const workflowId = await workflows.create({
slug: 'code-review',
name: 'Code Review Pipeline',
steps: [
{
id: 'analyze',
name: 'Static Analysis',
agentSlug: 'analyzer',
taskTemplate: {
title: 'Run static analysis on {{pr_url}}',
description: 'Analyze the PR for issues.',
},
},
{
id: 'review',
name: 'Code Review',
agentSlug: 'reviewer',
dependsOn: ['analyze'],
failurePolicy: 'abort', // abort | skip | retry
taskTemplate: {
title: 'Review PR based on analysis',
description: 'Review using analysis: {{analyze.output}}',
},
},
{
id: 'summarize',
name: 'Summary',
agentSlug: 'writer',
dependsOn: ['review'],
taskTemplate: {
title: 'Write review summary',
description: 'Summarize: {{review.output}}',
},
},
],
});
// Execute with context variables
await workflows.run(workflowId, {
pr_url: 'https://github.com/org/repo/pull/42',
});{{stepId.output}}.Scheduling
Database-backed scheduling with cron expressions and one-time triggers. Schedules fire hook events that you handle with the HookBus.
import { Scheduler } from 'botinabox';
const scheduler = new Scheduler(db, hooks);
// Cron schedule (daily at 9 AM Eastern)
await scheduler.create({
slug: 'daily-report',
cron: '0 9 * * *',
timezone: 'America/New_York',
action: 'agent.wakeup',
actionConfig: {
agentSlug: 'reporter',
taskTitle: 'Generate daily report',
},
});
// One-time schedule
await scheduler.create({
slug: 'deploy-reminder',
runAt: '2026-04-10T17:00:00Z',
action: 'agent.wakeup',
actionConfig: {
agentSlug: 'devops',
taskTitle: 'Deploy v2.0',
},
});
// Start the polling loop (checks every 30s by default)
scheduler.start();Budget Controls
Per-agent and global monthly budget enforcement. Costs are calculated from token usage and model-specific pricing.
import { BudgetController } from 'botinabox';
const budget = new BudgetController(db, hooks, {
globalMonthlyCents: 100000, // $1,000/month total
warnPercent: 80, // emit warning at 80%
});
// Check before execution
const allowed = await budget.canSpend(agentId, estimatedCost);
if (!allowed) {
console.log('Agent over budget — skipping');
}
// Record cost after execution
await budget.recordCost(agentId, {
inputTokens: 1500,
outputTokens: 800,
model: 'claude-sonnet-4-6',
});
// Query spend
const agentSpend = await budget.getAgentSpend(agentId);
const globalSpend = await budget.getGlobalSpend();When a budget is exceeded, a budget.exceeded hook fires. You can use this to send alerts, pause agents, or escalate to a human.
Data Layer Setup
The data layer is powered by latticesql. Call defineCoreTables(db) to register the 20+ core tables, then optionally add domain tables for business data.
import {
DataStore,
defineCoreTables,
defineDomainTables,
} from 'botinabox';
const db = new DataStore({
dbPath: './data/bot.db',
wal: true,
hooks,
});
defineCoreTables(db); // agents, tasks, runs, etc.
defineDomainTables(db); // org, project, client, etc. (optional)
await db.init();Core Tables
Core tables cover the orchestration lifecycle. All tables use TEXT UUIDs, ISO 8601 timestamps, and soft deletes via deleted_at.
| Table | Purpose |
|---|---|
| agent | Agent definitions (slug, name, adapter, model, budget) |
| task | Work items with priority, assignee, and status |
| run | Execution attempts with timing, output, and token usage |
| session | Conversation state per agent/channel/peer |
| message | Inbound and outbound messages |
| user | Cross-channel user identities |
| secret | Encrypted secrets with rotation tracking |
| schedule | Cron and one-time schedules |
| workflow | Workflow definitions (DAGs) |
| workflow_run | Workflow execution state |
| cost_event | Token usage and cost records |
| activity_log | Complete agent action audit trail |
| notification | Outbound notification queue |
Domain Tables
Optional business-domain tables for common patterns. Call defineDomainTables(db) to register them.
| Table | Purpose |
|---|---|
| org | Organizations (multi-tenant isolation) |
| project | Projects linked to orgs |
| client | Clients linked to orgs |
| invoice | Invoices linked to clients |
| repository | Git repositories linked to projects |
| file | Files linked to projects |
| channel | Communication channels |
| rule | Automation rules |
| event | Domain event log |
Context Rendering
DataStore can auto-generate markdown context files from database rows. Each entity gets its own directory with rendered files — so agents always start with accurate, up-to-date state.
// Render all entity context directories
await db.renderAll({ outputDir: './context' });
// Output structure:
// context/
// agents/
// researcher/
// AGENT.md # Agent definition + linked data
// MESSAGES.md # Recent messages
// projects/
// my-project/
// PROJECT.md # Project + linked repos, files, rulesSlack
Full Slack integration with threads, reactions, message editing, media support, and voice message transcription.
import { SlackAdapter } from 'botinabox/slack';
const slack = new SlackAdapter({
botToken: process.env.SLACK_BOT_TOKEN!,
appToken: process.env.SLACK_APP_TOKEN!,
});
// Register with the channel registry
channels.register(slack);
// Send a message
await slack.send({
channel: '#general',
text: 'Hello from Bot in a Box!',
threadTs: '1234567890.123456', // optional thread
});Voice messages are automatically transcribed via whisper.cpp (requires whisper-node and ffmpeg). Slack's mrkdwn format is handled natively.
Discord
Discord adapter with automatic message chunking for the 2,000-character limit.
import { DiscordAdapter } from 'botinabox/discord';
const discord = new DiscordAdapter({
token: process.env.DISCORD_TOKEN!,
});
channels.register(discord);Webhooks
Generic HTTP webhook adapter with HMAC-SHA256 signature verification.
import { WebhookAdapter, WebhookServer } from 'botinabox/webhook';
const webhook = new WebhookAdapter({
secret: process.env.WEBHOOK_SECRET!,
});
channels.register(webhook);
// Start a webhook server
const server = new WebhookServer({
port: 3000,
secret: process.env.WEBHOOK_SECRET!,
onMessage: async (msg) => {
await pipeline.process(msg);
},
});
server.start();Anthropic
Anthropic Claude provider supporting Claude Haiku, Sonnet, and Opus models.
import createAnthropicProvider from 'botinabox/anthropic';
const provider = createAnthropicProvider({
apiKey: process.env.ANTHROPIC_API_KEY!,
});
providers.register(provider);
// Available models:
// claude-haiku-4-5, claude-sonnet-4-6, claude-opus-4-6OpenAI
OpenAI GPT provider supporting GPT-4o, GPT-4o-mini, and o3-mini models.
import createOpenAIProvider from 'botinabox/openai';
const provider = createOpenAIProvider({
apiKey: process.env.OPENAI_API_KEY!,
});
providers.register(provider);
// Available models:
// gpt-4o, gpt-4o-mini, o3-miniOllama
Ollama provider for local and self-hosted models. Models are discovered dynamically from the running Ollama server.
import createOllamaProvider from 'botinabox/ollama';
const provider = createOllamaProvider({
baseUrl: 'http://localhost:11434', // default
});
providers.register(provider);
// Models are discovered from the running Ollama serverModel Router
The ModelRouter maps aliases and purposes to specific models, with fallback chains for resilience.
import { ModelRouter } from 'botinabox';
const router = new ModelRouter(providers, {
default: 'claude-sonnet-4-6',
// Aliases
aliases: {
fast: 'claude-haiku-4-5',
smart: 'claude-sonnet-4-6',
powerful: 'claude-opus-4-6',
},
// Purpose-based routing
routing: {
conversation: 'fast',
task_execution: 'smart',
classification: 'fast',
synthesis: 'powerful',
},
// Fallback chain (tried in order if primary fails)
fallbackChain: ['gpt-4o', 'claude-sonnet-4-6'],
});
// Resolve by alias
const model = router.resolve('smart');
// => 'claude-sonnet-4-6'
// Resolve by purpose
const model2 = router.resolveForPurpose('classification');
// => 'claude-haiku-4-5'Input Sanitization
All input is sanitized before storage. Null bytes and control characters are stripped, field lengths are enforced, and unknown columns are silently dropped on write.
// Sanitization happens automatically on all DataStore writes.
// No manual intervention needed.
// Field length limits are defined per-table in the schema.
// Values exceeding the limit are truncated.
// Unknown columns are stripped on write (silent),
// but cause errors on read (fail-fast).Audit Logging
Fire-and-forget audit events for tracked tables. The activity log records every agent action with timestamps, context, and results.
// Audit events are emitted automatically via HookBus.
// Listen for them to build custom audit pipelines:
hooks.on('audit.write', async (ctx) => {
console.log(`${ctx.table} ${ctx.action}: ${ctx.rowId}`);
});
// Secret access is tracked separately:
hooks.on('secret.accessed', async (ctx) => {
console.log(`Secret ${ctx.key} accessed by ${ctx.agentId}`);
});HMAC Verification
Webhook payloads are verified with HMAC-SHA256 signatures using timing-safe comparison.
import { verifyHmac } from 'botinabox/webhook';
const isValid = verifyHmac({
payload: requestBody,
signature: request.headers['x-signature'],
secret: process.env.WEBHOOK_SECRET!,
});
if (!isValid) {
throw new Error('Invalid webhook signature');
}Google Gmail
Full and incremental email sync via Gmail API. Supports OAuth2 and service account authentication. Can send emails.
import { GoogleGmailConnector } from 'botinabox/google';
const gmail = new GoogleGmailConnector({
credentials: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
refreshToken: process.env.GOOGLE_REFRESH_TOKEN!,
},
});
// Full sync
const emails = await gmail.sync();
// Incremental sync (uses historyId cursor)
const cursor = await db.loadCursor('gmail');
const newEmails = await gmail.sync({ cursor });
await db.saveCursor('gmail', newEmails.nextCursor);
// Send email
await gmail.send({
to: 'user@example.com',
subject: 'Hello',
body: 'Sent from Bot in a Box.',
});Google Calendar
Calendar event sync with syncToken-based incremental updates. Supports domain-wide delegation for Google Workspace.
import { GoogleCalendarConnector } from 'botinabox/google';
const calendar = new GoogleCalendarConnector({
credentials: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
refreshToken: process.env.GOOGLE_REFRESH_TOKEN!,
},
calendarId: 'primary',
});
// Sync events
const events = await calendar.sync();
// Incremental sync
const cursor = await db.loadCursor('calendar');
const updates = await calendar.sync({ cursor });
await db.saveCursor('calendar', updates.nextCursor);Import Paths
All exports are organized by subpath. The core package has no heavy dependencies — providers and adapters bring their own.
| Path | Exports |
|---|---|
| botinabox | HookBus, DataStore, defineCoreTables, defineDomainTables, AgentRegistry, TaskQueue, RunManager, BudgetController, WorkflowEngine, Scheduler, SessionManager, UserRegistry, SecretStore, ProviderRegistry, ModelRouter, ChannelRegistry, MessagePipeline, ApiExecutionAdapter, CliExecutionAdapter |
| botinabox/anthropic | createAnthropicProvider, AnthropicProvider |
| botinabox/openai | createOpenAIProvider, OpenAIProvider |
| botinabox/ollama | createOllamaProvider, OllamaProvider |
| botinabox/slack | SlackAdapter, transcribeAudio, downloadAudio, enrichVoiceMessage |
| botinabox/discord | DiscordAdapter, chunkMessage |
| botinabox/webhook | WebhookAdapter, WebhookServer, verifyHmac |
| botinabox/google | GoogleGmailConnector, GoogleCalendarConnector |
Core Classes
Quick reference for the main classes and their constructor signatures.
new HookBus()
Central event bus. No constructor arguments.
new DataStore({ dbPath, wal?, hooks? })
SQLite database wrapper. Requires dbPath, optionally enables WAL mode and hooks.
new AgentRegistry(db, hooks)
Agent CRUD and lookup. Requires DataStore and HookBus.
new TaskQueue(db, hooks)
Priority task queue with claim/complete lifecycle.
new RunManager(db, hooks)
Run lifecycle with per-agent locking and retry backoff.
new ProviderRegistry()
LLM provider registry. Register providers, then pass to ModelRouter.
new ModelRouter(providers, config)
Model resolution by alias, purpose, or fallback chain.
new BudgetController(db, hooks, config)
Cost tracking and budget enforcement.
new WorkflowEngine(db, hooks)
DAG workflow creation and execution.
new Scheduler(db, hooks)
Cron and one-time scheduling with database persistence.
new SessionManager(db)
Conversation state per agent/channel/peer.
new UserRegistry(db, hooks)
Cross-channel user identity management.
new SecretStore(db, hooks, { encryptionKey? })
Encrypted secret storage with rotation tracking.