Documentation

Full reference for botinabox. GitHub · Changelog

Installation

Install the core package. LLM providers and channel adapters are peer dependencies — install only what you use.

bash
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 & Calendar
Requires Node.js 18 or later. The core package depends on latticesql, 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.

typescript
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.

yaml
# 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"
All configuration sections are optional. You can configure entirely in TypeScript if you prefer — the YAML config is just a convenience layer.

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.

typescript
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.

typescript
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 delete

Agents

Agents are the core actors. Each agent has a slug, name, execution adapter (api or cli), model preference, and optional budget.

typescript
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).

typescript
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 },
});
The RunManager enforces one concurrent run per agent. If an agent is already running, new tasks wait in the queue. Retries use exponential backoff (5s → 10s → 20s, capped at 5 minutes).

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.

typescript
// 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.

typescript
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.

typescript
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',
});
The workflow engine detects cycles at creation time. Steps without dependencies run in parallel. Each step's output is available to downstream steps via {{stepId.output}}.

Scheduling

Database-backed scheduling with cron expressions and one-time triggers. Schedules fire hook events that you handle with the HookBus.

typescript
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();
One-time schedules auto-disable after firing. Cron schedules continue until manually disabled or deleted.

Budget Controls

Per-agent and global monthly budget enforcement. Costs are calculated from token usage and model-specific pricing.

typescript
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.

typescript
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.

TablePurpose
agentAgent definitions (slug, name, adapter, model, budget)
taskWork items with priority, assignee, and status
runExecution attempts with timing, output, and token usage
sessionConversation state per agent/channel/peer
messageInbound and outbound messages
userCross-channel user identities
secretEncrypted secrets with rotation tracking
scheduleCron and one-time schedules
workflowWorkflow definitions (DAGs)
workflow_runWorkflow execution state
cost_eventToken usage and cost records
activity_logComplete agent action audit trail
notificationOutbound notification queue

Domain Tables

Optional business-domain tables for common patterns. Call defineDomainTables(db) to register them.

TablePurpose
orgOrganizations (multi-tenant isolation)
projectProjects linked to orgs
clientClients linked to orgs
invoiceInvoices linked to clients
repositoryGit repositories linked to projects
fileFiles linked to projects
channelCommunication channels
ruleAutomation rules
eventDomain 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.

typescript
// 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, rules
Protected tables (users, secrets) are never rendered into other entities' context files. This prevents accidental exposure of sensitive data.

Slack

Full Slack integration with threads, reactions, message editing, media support, and voice message transcription.

typescript
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.

typescript
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.

typescript
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();
Webhook signatures are verified using timing-safe comparison to prevent timing attacks.

Anthropic

Anthropic Claude provider supporting Claude Haiku, Sonnet, and Opus models.

typescript
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-6

OpenAI

OpenAI GPT provider supporting GPT-4o, GPT-4o-mini, and o3-mini models.

typescript
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-mini

Ollama

Ollama provider for local and self-hosted models. Models are discovered dynamically from the running Ollama server.

typescript
import createOllamaProvider from 'botinabox/ollama';

const provider = createOllamaProvider({
  baseUrl: 'http://localhost:11434',  // default
});

providers.register(provider);

// Models are discovered from the running Ollama server

Model Router

The ModelRouter maps aliases and purposes to specific models, with fallback chains for resilience.

typescript
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.

typescript
// 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.

typescript
// 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.

typescript
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.

typescript
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.

typescript
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.

PathExports
botinaboxHookBus, DataStore, defineCoreTables, defineDomainTables, AgentRegistry, TaskQueue, RunManager, BudgetController, WorkflowEngine, Scheduler, SessionManager, UserRegistry, SecretStore, ProviderRegistry, ModelRouter, ChannelRegistry, MessagePipeline, ApiExecutionAdapter, CliExecutionAdapter
botinabox/anthropiccreateAnthropicProvider, AnthropicProvider
botinabox/openaicreateOpenAIProvider, OpenAIProvider
botinabox/ollamacreateOllamaProvider, OllamaProvider
botinabox/slackSlackAdapter, transcribeAudio, downloadAudio, enrichVoiceMessage
botinabox/discordDiscordAdapter, chunkMessage
botinabox/webhookWebhookAdapter, WebhookServer, verifyHmac
botinabox/googleGoogleGmailConnector, 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.