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
}
}
}| Field | Description |
|---|---|
enabled | Turn storage on/off |
maxSizeBytes | Quota 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 nullset(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.dbThe 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
| Limit | Default | Description |
|---|---|---|
maxSizeBytes | 104857600 (100MB) | Total storage size per Trik |
| Key length | 256 chars | Maximum key length |
| Value size | 10MB | Maximum 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.