← Back to Blog
January 31, 2026

Building Custom Adapters for SaveState: A Developer's Guide

Learn how to extend SaveState to support any AI platform

Introduction

SaveState ships with adapters for ChatGPT, Claude, Gemini, and OpenAI Assistants API. But what if you're using a custom AI agent, an enterprise bot, or a platform we don't support yet?

That's where custom adapters come in.

This guide walks you through building a complete SaveState adapter from scratch. By the end, you'll have a working adapter that can:

  • Detect your AI platform
  • Extract conversations, memories, and configurations
  • Restore snapshots back to the platform
  • Integrate seamlessly with the SaveState CLI
Code editor showing adapter interface with icons of various AI platforms surrounding it

Prerequisites

Before we start, make sure you have:

  • Node.js 18+ (20+ recommended)
  • TypeScript knowledge (intermediate level)
  • SaveState CLI installed (npm install -g savestate)
  • Familiarity with your target AI platform's data format
# Verify your setup
node --version  # v20.x.x or higher
savestate --version  # v0.2.x

Understanding the Adapter Interface

Every SaveState adapter implements the Adapter interface:

interface Adapter {
  // Metadata
  readonly id: string;          // Unique identifier
  readonly name: string;        // Human-readable name
  readonly platform: string;    // Platform category
  readonly version: string;     // Adapter version

  // Detection
  detect(): Promise<boolean>;
  identify(): Promise<PlatformMeta>;

  // Core operations
  extract(): Promise<Snapshot>;
  restore(snapshot: Snapshot): Promise<void>;

  // Capabilities (optional)
  capabilities(): AdapterCapabilities;
}
Method Purpose
detect() Returns true if this adapter can handle the current workspace
identify() Returns metadata about the platform/account being backed up
extract() Pulls all data from the platform into a Snapshot object
restore() Pushes a Snapshot back to the platform
capabilities() Declares what this adapter can and cannot do
Flow diagram showing detect → identify → extract → Snapshot → restore

Project Setup

Let's create a new adapter project:

# Create project directory
mkdir savestate-adapter-myagent
cd savestate-adapter-myagent

# Initialize npm package
npm init -y

# Install dependencies
npm install typescript @types/node --save-dev
npm install savestate --save-peer

# Initialize TypeScript
npx tsc --init

Update your package.json:

{
  "name": "@savestate/adapter-myagent",
  "version": "0.1.0",
  "description": "SaveState adapter for MyAgent platform",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "type": "module",
  "keywords": [
    "savestate",
    "savestate-adapter",
    "ai-backup",
    "myagent"
  ],
  "peerDependencies": {
    "savestate": "^0.2.0"
  }
}

Important: The savestate-adapter keyword enables auto-discovery by the CLI.


Step 1: Define the Adapter Shell

Create src/index.ts:

import type { Adapter, Snapshot, PlatformMeta } from 'savestate';

export class MyAgentAdapter implements Adapter {
  readonly id = 'myagent';
  readonly name = 'MyAgent Platform';
  readonly platform = 'myagent';
  readonly version = '0.1.0';

  async detect(): Promise<boolean> {
    // TODO: Implement detection logic
    return false;
  }

  async identify(): Promise<PlatformMeta> {
    return {
      platform: this.platform,
      version: 'unknown',
      accountId: 'local',
    };
  }

  async extract(): Promise<Snapshot> {
    // TODO: Implement extraction
    throw new Error('Not implemented');
  }

  async restore(snapshot: Snapshot): Promise<void> {
    // TODO: Implement restoration
    throw new Error('Not implemented');
  }

  capabilities() {
    return {
      extract: ['identity', 'memory', 'conversations'],
      restore: ['identity', 'memory'],
      incremental: true,
      search: true
    };
  }
}

export default MyAgentAdapter;

Step 2: Implement Detection

The detect() method tells SaveState whether this adapter can handle the current workspace.

import { access, readFile } from 'fs/promises';
import { join } from 'path';

async detect(): Promise<boolean> {
  try {
    // Check for MyAgent's signature file
    const configPath = join(process.cwd(), '.myagent', 'config.json');
    await access(configPath);
    
    // Optionally verify file contents
    const config = JSON.parse(await readFile(configPath, 'utf-8'));
    return config.platform === 'myagent';
  } catch {
    return false;
  }
}

Best practices for detection:

  • Check for platform-specific files or directories
  • Verify file contents when possible (not just existence)
  • Return false gracefully on any error
  • Don't throw exceptions—just return false

Step 3: Implement Extraction

This is the core of your adapter. The extract() method pulls all data into a Snapshot:

async extract(): Promise<Snapshot> {
  // 1. Extract identity
  const identity = await this.extractIdentity();
  
  // 2. Extract memory
  const memory = await this.extractMemory();
  
  // 3. Extract conversations
  const conversations = await this.extractConversations();
  
  // 4. Build metadata
  const meta = await this.buildMeta();
  
  return {
    identity,
    memory,
    conversations,
    meta
  };
}

private async extractIdentity() {
  const personalityPath = join(process.cwd(), '.myagent', 'personality.md');
  const configPath = join(process.cwd(), '.myagent', 'settings.json');
  
  const personality = await this.safeReadFile(personalityPath, '');
  const config = await this.safeReadJson(configPath, {});
  
  return { personality, config, tools: {} };
}

private async safeReadFile(path: string, fallback: string) {
  try {
    return await readFile(path, 'utf-8');
  } catch {
    return fallback;
  }
}

Handling Different Data Sources

File-based agents:

const data = await readFile('/path/to/data.json', 'utf-8');

API-based platforms:

const response = await fetch('https://api.platform.com/v1/conversations', {
  headers: { 'Authorization': `Bearer ${apiKey}` }
});
const data = await response.json();

Database-backed agents:

import Database from 'better-sqlite3';
const db = new Database('/path/to/agent.db');
const rows = db.prepare('SELECT * FROM conversations').all();
Diagram showing three data source types (Files, API, Database) all flowing into extract()

Step 4: Implement Restoration

The restore() method writes a snapshot back to the platform:

async restore(snapshot: Snapshot): Promise<void> {
  const basePath = join(process.cwd(), '.myagent');
  
  // Ensure directory exists
  await mkdir(basePath, { recursive: true });
  
  // Restore identity
  if (snapshot.identity.personality) {
    await writeFile(
      join(basePath, 'personality.md'),
      snapshot.identity.personality
    );
  }
  
  // Restore config
  if (snapshot.identity.config) {
    await writeFile(
      join(basePath, 'settings.json'),
      JSON.stringify(snapshot.identity.config, null, 2)
    );
  }
  
  // Restore memory
  if (snapshot.memory?.core) {
    await mkdir(join(basePath, 'memory'), { recursive: true });
    await writeFile(
      join(basePath, 'memory', 'core.json'),
      JSON.stringify(snapshot.memory.core, null, 2)
    );
  }
}

Handling Partial Restore

Not all platforms support full restoration. Declare your capabilities honestly:

capabilities() {
  return {
    extract: ['identity', 'memory', 'conversations'],
    restore: ['identity', 'memory'],  // Can only restore these
    incremental: true,
    search: true
  };
}

Step 5: Publishing Your Adapter

Once your adapter is working, publish it to npm:

# Build
npm run build

# Test locally
npm link
savestate adapters  # Should show your adapter

# Publish
npm publish --access public

Naming Convention

For automatic discovery, use one of these naming patterns:

  • @savestate/adapter-<name> (official namespace)
  • savestate-adapter-<name> (community)
  • Any package with the savestate-adapter keyword

Contributing Back

Built an adapter for a popular platform? Consider contributing it to the SaveState organization:

  1. Fork savestatedev/savestate
  2. Add your adapter to packages/adapters/<name>/
  3. Add tests
  4. Submit a PR

We're especially looking for adapters for:

  • Microsoft Copilot
  • Poe
  • Character.ai
  • Local LLMs (Ollama, LM Studio)

Your adapter could help thousands

The plugin system is designed to be simple, flexible, and discoverable. We can't wait to see what you build.

View on GitHub

Questions? Join our Discord community or open an issue on GitHub.