Skip to content

Storage Adapters

The Minions SDK includes a pluggable storage abstraction layer that lets you persist minions to virtually any backend. By default, the SDK operates entirely in-memory, but you can plug in a StorageAdapter to write to disk, a database, or a cloud service.

The simplest way to use storage is through the Minions client:

import { Minions, JsonFileStorageAdapter } from 'minions-sdk';
const storage = await JsonFileStorageAdapter.create('./data/minions');
const minions = new Minions({ storage });
// Create and persist a minion
const note = await minions.create('note', { title: 'Hello', fields: { content: 'World' } });
await minions.save(note.data);
// Load it back
const loaded = await minions.load(note.data.id);
console.log(loaded?.title); // > "Hello"

Stores all data in a runtime Map (TypeScript) or dict (Python). Data is lost when the process exits. Ideal for tests and ephemeral scripts.

import { Minions, MemoryStorageAdapter } from 'minions-sdk';
const storage = new MemoryStorageAdapter();
const minions = new Minions({ storage });

Persists each minion as a pretty-printed JSON file in a sharded directory layout:

<rootDir>/<id[0:2]>/<id[2:4]>/<id>.json

The two-level shard prefix keeps directories small even with millions of minions. An in-memory index is built on startup for O(1) lookups.

  • ✅ Writes are atomic (write-to-tmp-then-rename) — safe against crashes
  • ✅ Files are human-readable and git-friendly
  • ⚠️ Server-side / CLI only (uses node:fs)
import { Minions, JsonFileStorageAdapter } from 'minions-sdk';
const storage = await JsonFileStorageAdapter.create('./data/minions');
const minions = new Minions({ storage });

[!NOTE] Use the async factory method create() — the constructor is private (TS) or should not be called directly. create() ensures the directory exists and the in-memory index is populated.

Persists each minion as a human-readable YAML file in the same sharded directory layout:

<rootDir>/<id[0:2]>/<id[2:4]>/<id>.yaml

Identical architecture to JsonFileStorageAdapter but uses YAML for maximum readability and git-friendliness. Includes a built-in zero-dependency YAML serializer — no external YAML library required.

  • ✅ Writes are atomic (write-to-tmp-then-rename)
  • ✅ Human-readable and git-friendly
  • ✅ Zero external dependencies for YAML handling
  • ⚠️ Server-side / CLI only (uses node:fs)
import { Minions } from 'minions-sdk';
import { YamlFileStorageAdapter } from 'minions-sdk/node';
const storage = await YamlFileStorageAdapter.create('./data/minions');
const minions = new Minions({ storage });

[!TIP] The toYaml() and parseYaml() utilities are also exported from minions-sdk/node for standalone YAML conversion outside of the storage layer.


When a storage adapter is configured on the Minions client, these methods become available:

MethodDescription
save(minion)Persist a minion (upsert)
load(id)Load a minion by ID (returns undefined/None if not found)
remove(minion)Delete from storage and remove all relations
listMinions(filter?)List minions with optional filtering
searchMinions(query)Full-text search across minions

All methods throw if no storage adapter is configured.


The listMinions() method accepts a StorageFilter to control the result set:

const results = await minions.listMinions({
minionTypeId: 'note', // Only notes
status: 'active', // Only active
tags: ['ai', 'research'], // Must have all tags
includeDeleted: false, // Exclude soft-deleted (default)
sortBy: 'title', // Sort by 'title', 'createdAt', or 'updatedAt'
sortOrder: 'asc', // 'asc' or 'desc'
limit: 10, // Page size
offset: 0, // Skip N results
});

The searchMinions() method performs case-insensitive, multi-token search against the pre-computed searchableText field (built from the minion’s title, description, and string fields):

// Find minions mentioning both "quantum" and "computing"
const results = await minions.searchMinions('quantum computing');

[!TIP] Search automatically excludes soft-deleted minions. An empty query returns all non-deleted minions.


Wrap any StorageAdapter with before/after hooks for cross-cutting storage concerns like logging, encryption, or metrics — without changing the Minions client configuration.

import { withHooks, MemoryStorageAdapter, Minions } from 'minions-sdk';
const storage = withHooks(new MemoryStorageAdapter(), {
beforeSet: async (minion) => {
// Transform data before writing
return { ...minion, fields: { ...minion.fields, savedAt: new Date().toISOString() } };
},
afterGet: async (id, result) => {
if (result) console.log('Loaded:', result.title);
},
});
const minions = new Minions({ storage });

Available hooks: beforeGet, afterGet, beforeSet (can transform), afterSet, beforeDelete, afterDelete, beforeList, afterList, beforeSearch, afterSearch.

[!TIP] See the Middleware & Hooks guide for the full reference and comparison with the client-level middleware pipeline.


To connect Minions to a different backend (PostgreSQL, MongoDB, Redis, Supabase, etc.), implement the StorageAdapter interface:

import type { StorageAdapter, StorageFilter } from 'minions-sdk';
import type { Minion } from 'minions-sdk';
export class PostgresStorageAdapter implements StorageAdapter {
async get(id: string): Promise<Minion | undefined> { /* ... */ }
async set(minion: Minion): Promise<void> { /* ... */ }
async delete(id: string): Promise<void> { /* ... */ }
async list(filter?: StorageFilter): Promise<Minion[]> { /* ... */ }
async search(query: string): Promise<Minion[]> { /* ... */ }
}
// Use it like any other adapter:
const storage = new PostgresStorageAdapter(/* connection config */);
const minions = new Minions({ storage });
MethodSignatureBehavior
get(id) → Minion?Return undefined/None if not found
set(minion) → voidUpsert — insert or overwrite
delete(id) → voidResolve silently if missing
list(filter?) → Minion[]Apply filtering, sorting, pagination
search(query) → Minion[]Case-insensitive token matching

[!TIP] Use the shared applyFilter() utility (exported from minions-sdk) to handle StorageFilter logic in your custom adapter — no need to reimplement filtering/sorting.