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.
Quick Start
Section titled “Quick Start”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 minionconst note = await minions.create('note', { title: 'Hello', fields: { content: 'World' } });await minions.save(note.data);
// Load it backconst loaded = await minions.load(note.data.id);console.log(loaded?.title); // > "Hello"from minions import Minions, JsonFileStorageAdapter
storage = await JsonFileStorageAdapter.create("./data/minions")minions = Minions(storage=storage)
# Create and persist a minionnote = await minions.create("note", {"title": "Hello", "fields": {"content": "World"}})await minions.save(note.data)
# Load it backloaded = await minions.load(note.data.id)print(loaded.title) # > "Hello"Built-In Adapters
Section titled “Built-In Adapters”MemoryStorageAdapter
Section titled “MemoryStorageAdapter”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 });from minions import Minions, MemoryStorageAdapter
storage = MemoryStorageAdapter()minions = Minions(storage=storage)JsonFileStorageAdapter
Section titled “JsonFileStorageAdapter”Persists each minion as a pretty-printed JSON file in a sharded directory layout:
<rootDir>/<id[0:2]>/<id[2:4]>/<id>.jsonThe 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 });from minions import Minions, JsonFileStorageAdapter
storage = await JsonFileStorageAdapter.create("./data/minions")minions = Minions(storage=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.
YamlFileStorageAdapter
Section titled “YamlFileStorageAdapter”Persists each minion as a human-readable YAML file in the same sharded directory layout:
<rootDir>/<id[0:2]>/<id[2:4]>/<id>.yamlIdentical 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 });from minions import Minions, YamlFileStorageAdapter
storage = await YamlFileStorageAdapter.create("./data/minions")minions = Minions(storage=storage)[!TIP] The
toYaml()andparseYaml()utilities are also exported fromminions-sdk/nodefor standalone YAML conversion outside of the storage layer.
Client Storage Methods
Section titled “Client Storage Methods”When a storage adapter is configured on the Minions client, these methods become available:
| Method | Description |
|---|---|
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.
Filtering and Sorting
Section titled “Filtering and Sorting”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});from minions import StorageFilter
results = await minions.list_minions(StorageFilter( minion_type_id="note", # Only notes status="active", # Only active tags=["ai", "research"], # Must have all tags include_deleted=False, # Exclude soft-deleted (default) sort_by="title", # Sort by 'title', 'createdAt', or 'updatedAt' sort_order="asc", # 'asc' or 'desc' limit=10, # Page size offset=0, # Skip N results))Full-Text Search
Section titled “Full-Text Search”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');# Find minions mentioning both "quantum" and "computing"results = await minions.search_minions("quantum computing")[!TIP] Search automatically excludes soft-deleted minions. An empty query returns all non-deleted minions.
Storage Hooks (withHooks)
Section titled “Storage Hooks (withHooks)”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 });from minions import Minions, MemoryStorageAdapterfrom minions.storage import with_hooks, StorageHooks
async def stamp_save(minion): import dataclasses return dataclasses.replace(minion, fields={**minion.fields, "saved_at": "now"})
async def log_load(id, result): if result: print("Loaded:", result.title)
storage = with_hooks(MemoryStorageAdapter(), StorageHooks( before_set=stamp_save, after_get=log_load,))
minions = Minions(storage=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.
Custom Adapters
Section titled “Custom Adapters”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 });from minions.storage import StorageAdapter, StorageFilterfrom minions.types import Minionfrom typing import Optional
class PostgresStorageAdapter(StorageAdapter): async def get(self, id: str) -> Optional[Minion]: ... async def set(self, minion: Minion) -> None: ... async def delete(self, id: str) -> None: ... async def list(self, filter: Optional[StorageFilter] = None) -> list[Minion]: ... async def search(self, query: str) -> list[Minion]: ...
# Use it like any other adapter:storage = PostgresStorageAdapter(# connection config)minions = Minions(storage=storage)Methods to Implement
Section titled “Methods to Implement”| Method | Signature | Behavior |
|---|---|---|
get | (id) → Minion? | Return undefined/None if not found |
set | (minion) → void | Upsert — insert or overwrite |
delete | (id) → void | Resolve silently if missing |
list | (filter?) → Minion[] | Apply filtering, sorting, pagination |
search | (query) → Minion[] | Case-insensitive token matching |
[!TIP] Use the shared
applyFilter()utility (exported fromminions-sdk) to handleStorageFilterlogic in your custom adapter — no need to reimplement filtering/sorting.