Agent DB
Agent DB is Operaide's persistent storage layer for Reaktors. It gives your workflows a SQLite database to store, query, and manage structured data — from simple key-value lookups to full-text search and vector embeddings. Databases are managed by the platform and survive across Reaktor executions.
Overview
Each Agent DB is a LibSQL database (a SQLite fork with vector extensions). You interact with it through two packages:
| Package | Purpose |
|---|---|
@operaide/database | Core SQL operations, inserts, queries, and a virtual file system |
@operaide/vector | Vector embeddings, semantic search, and RAG pipelines |
The platform handles database creation, file storage, snapshots, and cross-instance synchronization. As a developer, you only need a database ID to get started.
Getting a Database Client
Use aktorDatabase() to obtain a database client. The database ID typically comes from aktorSetting(), so administrators can configure it per App Instance without changing code.
import { aktorSetting } from '@operaide/aktor';
import { aktorDatabase } from '@operaide/database';
import { z } from 'zod';
const databaseId = aktorSetting(z.string(), '<database-id>', 'Database ID');
const db = aktorDatabase({ id: databaseId });
For testing, use an ephemeral in-memory database that does not persist beyond the current execution:
import { aktorInMemoryDatabase } from '@operaide/database';
const db = aktorInMemoryDatabase({ id: 'test' });
SQL Operations
Type-Safe Queries with aktorQuery
aktorQuery executes a SQL statement and validates each returned row against a Zod schema. The return type is automatically inferred from the schema.
import { aktorQuery } from '@operaide/database';
import { z } from 'zod';
const UserZ = z.object({
id: z.number(),
name: z.string(),
age: z.number(),
});
const users = aktorQuery({
client: db,
sql: 'SELECT * FROM users WHERE age > ?',
params: [18],
schema: UserZ,
});
// Type: Aktor<UserZ[]>
Parameters are passed as an array of positional values, keeping queries safe from injection.
Inserts with aktorInsert
aktorInsert validates the data against a Zod schema before executing the INSERT statement. It returns the database client, so you can chain further operations.
import { aktorInsert } from '@operaide/database';
const dbAfterInsert = aktorInsert({
client: db,
table: 'users',
data: { id: 1, name: 'alice', age: 30 },
schema: UserZ,
});
// Chain a query after the insert
const allUsers = aktorQuery({
client: dbAfterInsert,
sql: 'SELECT * FROM users',
schema: UserZ,
});
Extracting Raw Results
If you need the raw LibSQL result set as a JSON string (e.g. for forwarding to an LLM), use aktorExtractResultSet:
import { aktorExtractResultSet } from '@operaide/database';
const resultSet = aktorExtractResultSet({ resultSet: rawResult });
// Returns: JSON string of the ResultSet
File Storage (Virtual File System)
Agent DB includes a virtual file system based on the SQLar format. Files are stored as BLOBs inside the database, giving you a self-contained storage layer without external file systems.
Initialize the File System
Before using file operations, initialize the SQLar schema:
import { aktorInitSqlarSchema } from '@operaide/database';
const dbWithFs = aktorInitSqlarSchema({ client: db });
Writing and Reading Files
import { aktorWriteFile, aktorReadFile, aktorReadTextFile } from '@operaide/database';
// Write a file (string or Uint8Array)
aktorWriteFile({
client: dbWithFs,
path: '/reports/summary.md',
data: '# Monthly Summary\n\nAll metrics are on track.',
});
// Read as binary
const binary = aktorReadFile({ client: dbWithFs, path: '/reports/summary.md' });
// Read as UTF-8 text
const text = aktorReadTextFile({ client: dbWithFs, path: '/reports/summary.md' });
If a file already exists at the given path, aktorWriteFile overwrites it.
Listing and Inspecting
import { aktorListDir, aktorStat, aktorDeleteFile } from '@operaide/database';
// List directory contents
const entries = aktorListDir({ client: dbWithFs, path: '/reports' });
// Returns: ['summary.md', ...]
// Get file metadata
const info = aktorStat({ client: dbWithFs, path: '/reports/summary.md' });
// Returns: { type: 'file', size: 47, mtime: 1709654400, mode: 420 }
// Delete a file
aktorDeleteFile({ client: dbWithFs, path: '/reports/summary.md' });
Vector Database
Agent DB also serves as the storage backend for vector embeddings and semantic search. The @operaide/vector package provides an embedding pipeline and a retrieval pipeline that operate on top of Agent DB.
Use aktorVektorDatabase() as a convenience wrapper that initializes the vector schema (files, documents, chunks, vectors tables) and exposes the database ID as a setting:
import { aktorVektorDatabase } from '@operaide/vector';
const vectorDb = aktorVektorDatabase();
For full details on embedding, chunking, retrieval, and RAG patterns, see the Vectorization Functions chapter.
Full Example: Database-Backed Reaktor
This Reaktor creates a table, inserts a row, and queries the result — a minimal pattern for structured data storage:
import { aktorConst, aktorSetting, createAktorComposition, registerReaktorDefinition } from '@operaide/aktor';
import { aktorDatabase, aktorInsert, aktorQuery } from '@operaide/database';
import { z } from 'zod';
const UserZ = z.object({
id: z.number(),
name: z.string(),
age: z.number(),
});
const aktorDbOrmExample = createAktorComposition('aktorDbOrmExample', () => {
const databaseId = aktorSetting(z.string(), '<database-id>', 'Database ID');
const db = aktorDatabase({ id: databaseId });
const dbAfterInsert = aktorInsert({
client: db,
table: 'users',
data: aktorConst({ id: 1, name: 'max', age: 42 }),
schema: UserZ,
});
return aktorQuery({
client: dbAfterInsert,
sql: 'SELECT * FROM users',
schema: UserZ,
});
});
registerReaktorDefinition({
reaktorDefinitionId: 'db-reaktor-orm',
label: 'Database ORM Example',
description: 'Insert and query data with type-safe schemas',
inputSchema: z.object({}),
outputSchema: UserZ.array(),
aktor: aktorDbOrmExample,
});
The table must already exist in the database. You can create tables using raw SQL via the LibSQL client during initialization, or manage the schema through the platform UI.
Full Example: AI Chat with Database Tools
This pattern exposes database operations as LLM tools, letting the AI decide when to query the database during a conversation:
import {
aktorAICall,
aktorAISettingProviderModel,
aktorPatchMessages,
aktorToolSet,
aktorToTool,
registerChatReaktorDefinition,
} from '@operaide/ai';
import { aktorConst, aktorSetting, createAktorFunction, createAktorComposition } from '@operaide/aktor';
import { aktorDatabase } from '@operaide/database';
import { z } from 'zod';
import type { LlmOptions } from '@operaide/ai';
const aktorQueryDatabase = createAktorFunction(
'aktorQueryDatabase',
async ({ client, sql }: { client: any; sql: string }) => {
if (!sql) return [];
const result = await client.execute(sql);
return result.rows;
}
);
const aktorDatabaseChat = createAktorComposition('aktorDatabaseChat', ({ messages }) => {
const databaseId = aktorSetting(z.string(), '<database-id>', 'Database ID');
const db = aktorDatabase({ id: databaseId });
const queryTool = aktorToTool({
aktor: aktorQueryDatabase,
description: 'Execute a read-only SQL query against the database.',
parameters: z.object({
sql: z.string().describe('The SQL SELECT query to execute'),
}),
dependencies: { client: db },
});
const tools = aktorToolSet({ queryTool });
return aktorAICall({
messages: aktorPatchMessages({
messages,
system: aktorConst(
'You are a database assistant. Use the queryTool to answer questions about the data.'
),
}),
providerModel: aktorAISettingProviderModel(),
tools,
llmOptions: aktorConst<LlmOptions>({ max_steps: 10 }),
});
});
registerChatReaktorDefinition({
reaktorDefinitionId: 'database-chat',
label: 'Database Chat',
description: 'Chat with your database using natural language',
aktor: aktorDatabaseChat,
});
Best Practices
-
Use
aktorSetting()for the database ID so administrators can configure it per App Instance without code changes. -
Use
aktorInMemoryDatabase()during development and testing. It behaves identically to a persistent database but does not require platform infrastructure. -
Define Zod schemas for all queries and inserts. This gives you runtime validation and compile-time type inference in one step.
-
Chain operations through the client return value.
aktorInsertreturns the client specifically to support sequential composition — pass it to the next operation to enforce execution order. -
Keep vector operations in
@operaide/vector. Use@operaide/databasefor structured SQL and file storage; use@operaide/vectorwhen you need embeddings or semantic search.
Summary
| Aktor | Package | Purpose | Returns |
|---|---|---|---|
aktorDatabase | @operaide/database | Get a persistent database client | Client |
aktorInMemoryDatabase | @operaide/database | Get an ephemeral in-memory client | Client |
aktorQuery | @operaide/database | Type-safe SQL queries with Zod validation | T[] |
aktorInsert | @operaide/database | Validated inserts | Client |
aktorExtractResultSet | @operaide/database | Convert ResultSet to JSON string | string |
aktorInitSqlarSchema | @operaide/database | Initialize the virtual file system | Client |
aktorWriteFile | @operaide/database | Write a file to the VFS | void |
aktorReadFile | @operaide/database | Read a file as binary | Uint8Array |
aktorReadTextFile | @operaide/database | Read a file as UTF-8 text | string |
aktorListDir | @operaide/database | List directory contents | string[] |
aktorStat | @operaide/database | Get file/directory metadata | FileStat |
aktorDeleteFile | @operaide/database | Delete a file from the VFS | void |
aktorVektorDatabase | @operaide/vector | Get a vector-enabled database client | Client |
For vector embeddings and semantic search, see Vectorization Functions. For AI model integration, see LLM Calls.