Persistent Storage

Storage enables Triks to persist data across sessions and restarts.

The Problem

Without storage, every session starts fresh:

User: Set my preferred theme to dark Agent: Done! Your theme is now dark. [Later, new session] User: What's my preferred theme? Agent: I don't know - I have no memory of previous sessions.

With storage:

User: Set my preferred theme to dark Agent: Done! Your theme is now dark. [Later, new session] User: What's my preferred theme? Agent: Your preferred theme is dark.

How Storage Works

1. Enable in Manifest

{ "capabilities": { "storage": { "enabled": true, "maxSizeBytes": 10485760 } } }
FieldDescription
enabledTurn storage on/off
maxSizeBytesQuota limit (default: 100MB)

2. Access in Your Graph

When storage is enabled, a storage context is passed to your graph:

export const graph = { async invoke(input) { const { action, storage } = input; if (action === "set-preference") { await storage.set("theme", input.input.theme); return { agentData: { success: true } }; } if (action === "get-preference") { const theme = await storage.get("theme"); return { agentData: { theme: theme ?? "light" } }; } } };

Storage API

All methods are async and return Promises (TypeScript) or awaitables (Python).

get(key: string): Promise<unknown | null>

Retrieve a value by key. Returns null if the key doesn’t exist.

const theme = await storage.get("user-theme"); // Returns: "dark" or null

set(key: string, value: unknown, ttl?: number): Promise<void>

Store a value. Optionally set a TTL (time-to-live) in milliseconds.

// Store permanently await storage.set("user-theme", "dark"); // Store for 1 hour await storage.set("cache-key", data, 3600000);

delete(key: string): Promise<boolean>

Delete a key. Returns true if the key existed.

const wasDeleted = await storage.delete("old-key");

list(prefix?: string): Promise<string[]>

List all keys, optionally filtered by prefix.

const allKeys = await storage.list(); // Returns: ["theme", "last-run", "counter"] const prefixedKeys = await storage.list("user:"); // Returns: ["user:settings", "user:history"]

getMany(keys: string[]): Promise<Map<string, unknown>>

Get multiple values at once.

const values = await storage.getMany(["theme", "language"]); // Returns: Map { "theme" => "dark", "language" => "en" }

setMany(entries: Record<string, unknown>): Promise<void>

Set multiple values at once.

await storage.setMany({ theme: "dark", language: "en", lastLogin: Date.now() });

Where Data is Stored

Storage persists to a SQLite database on the local filesystem:

~/.trikhub/storage/storage.db

The database uses a single storage table with composite primary key (trik_id, key). Each Trik has isolated storage - it cannot access other Triks’ data.

Quotas and Limits

LimitDefaultDescription
maxSizeBytes104857600 (100MB)Total storage size per Trik
Key length256 charsMaximum key length
Value size10MBMaximum single value size

When a Trik exceeds its quota, set operations will throw an error.

TTL (Time-To-Live)

Set expiration times for cache-like behavior:

// Cache API response for 5 minutes await storage.set("api-cache", response, 300000); // Later: returns null if expired const cached = await storage.get("api-cache");

Expired keys are automatically cleaned up on access.

Example: User Preferences Trik

A complete example showing storage patterns:

export const graph = { async invoke(input) { const { action, storage, input: data } = input; switch (action) { case "set-preferences": { await storage.setMany({ theme: data.theme, language: data.language, notifications: data.notifications }); return { agentData: { success: true, message: "Preferences saved" } }; } case "get-preferences": { const prefs = await storage.getMany([ "theme", "language", "notifications" ]); return { agentData: { theme: prefs.get("theme") ?? "light", language: prefs.get("language") ?? "en", notifications: prefs.get("notifications") ?? true } }; } case "reset-preferences": { const keys = await storage.list(); for (const key of keys) { await storage.delete(key); } return { agentData: { success: true, message: "Preferences reset" } }; } } } };

Storage Patterns

Pattern 1: Simple Key-Value Cache

async function getCached(key: string, fetcher: () => Promise<unknown>) { const cached = await storage.get(`cache:${key}`); if (cached !== null) return cached; const fresh = await fetcher(); await storage.set(`cache:${key}`, fresh, 3600000); // 1 hour TTL return fresh; }

Pattern 2: Counters and Metrics

async function incrementCounter(name: string) { const current = (await storage.get(`counter:${name}`)) ?? 0; await storage.set(`counter:${name}`, current + 1); return current + 1; }

Pattern 3: State Machine

type State = "idle" | "processing" | "complete"; async function transition(newState: State) { const current = await storage.get("state") as State ?? "idle"; // Validate transition if (current === "complete" && newState !== "idle") { throw new Error("Cannot transition from complete"); } await storage.set("state", newState); await storage.set("stateChangedAt", Date.now()); }

Testing Storage

Use an in-memory storage provider for tests:

import { InMemoryStorageProvider } from "@trikhub/gateway"; const storage = new InMemoryStorageProvider(); const context = storage.forTrik("@test/my-trik"); // Test your storage logic await context.set("key", "value"); expect(await context.get("key")).toBe("value");

For integration tests, use a temporary directory:

import { SqliteStorageProvider } from "@trikhub/gateway"; import { mkdtemp, rm } from "fs/promises"; import { tmpdir } from "os"; import { join } from "path"; let tempDir: string; beforeEach(async () => { tempDir = await mkdtemp(join(tmpdir(), "storage-test-")); }); afterEach(async () => { await rm(tempDir, { recursive: true }); }); it("persists data", async () => { const storage = new SqliteStorageProvider(tempDir); const context = storage.forTrik("@test/trik"); await context.set("key", "value"); expect(await context.get("key")).toBe("value"); });

Next: Learn about the Security Model.