Skip to main content
Version: 3.0

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:

PackagePurpose
@operaide/databaseCore SQL operations, inserts, queries, and a virtual file system
@operaide/vectorVector 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,
});
note

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

  1. Use aktorSetting() for the database ID so administrators can configure it per App Instance without code changes.

  2. Use aktorInMemoryDatabase() during development and testing. It behaves identically to a persistent database but does not require platform infrastructure.

  3. Define Zod schemas for all queries and inserts. This gives you runtime validation and compile-time type inference in one step.

  4. Chain operations through the client return value. aktorInsert returns the client specifically to support sequential composition — pass it to the next operation to enforce execution order.

  5. Keep vector operations in @operaide/vector. Use @operaide/database for structured SQL and file storage; use @operaide/vector when you need embeddings or semantic search.


Summary

AktorPackagePurposeReturns
aktorDatabase@operaide/databaseGet a persistent database clientClient
aktorInMemoryDatabase@operaide/databaseGet an ephemeral in-memory clientClient
aktorQuery@operaide/databaseType-safe SQL queries with Zod validationT[]
aktorInsert@operaide/databaseValidated insertsClient
aktorExtractResultSet@operaide/databaseConvert ResultSet to JSON stringstring
aktorInitSqlarSchema@operaide/databaseInitialize the virtual file systemClient
aktorWriteFile@operaide/databaseWrite a file to the VFSvoid
aktorReadFile@operaide/databaseRead a file as binaryUint8Array
aktorReadTextFile@operaide/databaseRead a file as UTF-8 textstring
aktorListDir@operaide/databaseList directory contentsstring[]
aktorStat@operaide/databaseGet file/directory metadataFileStat
aktorDeleteFile@operaide/databaseDelete a file from the VFSvoid
aktorVektorDatabase@operaide/vectorGet a vector-enabled database clientClient

For vector embeddings and semantic search, see Vectorization Functions. For AI model integration, see LLM Calls.