Adapter Interface
August 22, 2026 · View on GitHub
An ATS adapter wraps a storage backend (TickTick, Notion, Obsidian, plain markdown, Things, Google Tasks, …) so the ATS Core can list / read / write / link items without knowing the underlying system.
This document defines the contract.
Required methods
interface KnowledgeAdapter {
listProjects(): Promise<Project[]>
listTasksInProject(projectId: string): Promise<Task[]>
getTask(projectId: string, taskId: string): Promise<Task>
createTask(input: TaskInput): Promise<Task>
updateTask(projectId: string, taskId: string, patch: TaskPatch): Promise<Task>
urlFor(ref: { projectId: string, taskId: string }): string
}
Payload shapes
type Project = {
id: string // adapter-stable; used as input to listTasksInProject
name: string // human-visible
kind?: 'tasks' | 'notes' // optional hint; some adapters distinguish
raw?: any // adapter-internal extras, opaque to Core
}
type Task = {
id: string // adapter-stable; used as input to getTask / updateTask
title: string
content: string // markdown body, may be empty
projectId: string // matches Project.id
tags: string[] // empty array if adapter has no tags
dueDate?: string // ISO 8601, optional
modifiedTime: string // ISO 8601 — used for cache invalidation
links?: TaskLink[] // adapter-native read-only relationships
raw?: any // adapter-internal extras
}
type TaskInput = {
title: string
content?: string
projectId?: string // adapter may default to inbox/root
tags?: string[]
dueDate?: string
}
type TaskPatch = Partial<TaskInput> & { title?: string }
Method contracts
listProjects() — returns every project visible to the active user. Cheap, called once per corpus refresh. Adapters with no project concept (e.g. plain markdown vault) can return a single synthetic project.
listTasksInProject(projectId) — tasks visible in the project. Most task-app adapters return active work; history-oriented local adapters may also include completed items so decisions and prior implementation context remain searchable. Adapters with infinite or paginated lists should return at least the most recently modified N items (configurable) and document their inclusion policy.
getTask(projectId, taskId) — full task including content. Should be O(1) from the adapter's perspective.
createTask(input) — inserts. Returns the canonical Task with its assigned id and modifiedTime. If the adapter requires a project and input.projectId is missing, the adapter chooses (typically inbox).
updateTask(projectId, taskId, patch) — patch-style partial update. Adapter is responsible for preserving omitted fields. Returns the updated Task.
urlFor({ projectId, taskId }) — returns a deep-link URL the user can click to open the task in the adapter's native UI. Used by ats url to generate cross-reference markdown. Format is adapter-specific:
| Adapter | URL pattern (illustrative) |
|---|---|
| ticktick | https://ticktick.com/webapp/#p/<projectId>/tasks/<taskId> |
| notion | https://www.notion.so/<page-id> |
| obsidian | obsidian://open?vault=<vault>&file=<path> |
| things | things:///show?id=<taskId> |
If the adapter has no concept of a deep link (rare), return a stable identifier the adapter can resolve back via the CLI (e.g. ats-ref://<adapter>/<id>).
Adapters may also return a links array on tasks for native relationships such as dependencies. Core merges these links into graph, context, and event reads, but ATS metadata writes remain confined to the managed task-body block. This keeps the storage system authoritative for its own relationships.
Optional methods
The Core works without these, but uses them when present for speed or quality.
interface KnowledgeAdapter {
searchByQuery?(query: string): Promise<Task[]> // adapter's native search
bulkFetch?(): Promise<Task[]> // single-call corpus refresh
bulkFetchDelta?(opts: { cursor: any, since: number | null }):
Promise<{ tasks: Task[], removedIds?: string[], cursor?: any } | null>
// incremental corpus refresh
listCompletedTasks?(opts: { since?: string, until?: string, projectIds?: string[] }):
Promise<Task[]> // completed-task history
embeddings?(texts: string[]): Promise<number[][]> // adapter-supplied embeddings
}
searchByQuery(query) — if the adapter has a fast native search (e.g. Notion's full-text search, Obsidian's filesystem grep), Core uses it as one branch of ats find. Without it, Core falls back to substring scan on the cached corpus.
bulkFetch() — single-call corpus refresh. Cheaper than per-project iteration when the adapter supports it (Notion's database queries, TickTick's v2 batch sync, filesystem walk). Without it, Core iterates listProjects → listTasksInProject.
bulkFetchDelta({ cursor, since }) — incremental corpus refresh, used by ats cache sync when a cached corpus already exists. Implement it when the backend can answer "what changed since X" (an updated-since query, a sync token, file mtimes). Return changed tasks as whole items plus removedIds; Core applies them as replace-by-id over the prior corpus — never a field merge — and persists your returned cursor for the next call. Return null to request a full refresh. Backends without a changes API (TickTick's open API, for one) simply skip this hook and get the full-refresh path.
listCompletedTasks(opts) — completed-task history for retrospective queries. ats find --include-completed appends these per query (they are never written into the corpus cache), each carrying status: 'completed'. Adapters whose backend cannot list closed items simply omit the method; find then reports "completed history is not supported by this adapter" instead of silently answering from active tasks only.
embeddings(texts) — adapter-supplied vectors. Core uses them for a dense branch, fuses that branch with its token-based sparse branch, and supports generic similar. Without this method, Core stays on keyword/native retrieval; it does not silently start an embedding service.
Authentication
Adapters expose three lifecycle methods that the CLI delegates to:
interface KnowledgeAdapter {
authStatus(): Promise<{ authenticated: boolean, expiresAt?: string }>
authLogin(): Promise<{ url?: string, instructions: string }>
authExchange?(code: string): Promise<void>
}
ats auth login calls authLogin() and prints whatever the adapter returns (typically an OAuth URL + paste-the-code instructions).
Configuration
Each adapter ships its own <adapter>.config.json schema. The CLI loads:
~/.config/ats/config.json— global: which adapter is active, retrieval defaults~/.config/ats/<adapter>.json— adapter-specific: tokens, URLs, project IDs
The reference TickTick adapter stores OAuth tokens here. The Obsidian adapter stores the vault path. Notion stores the integration token + database IDs.
Worked example — the simplest possible adapter
// adapter-readonly-json.js — reads tasks from a static JSON file. Useful for testing.
import fs from 'fs';
const DATA = JSON.parse(fs.readFileSync('./tasks.json', 'utf8'));
export default {
async listProjects() {
const projectIds = [...new Set(DATA.map(t => t.projectId))];
return projectIds.map(id => ({ id, name: id }));
},
async listTasksInProject(projectId) {
return DATA.filter(t => t.projectId === projectId);
},
async getTask(projectId, taskId) {
return DATA.find(t => t.projectId === projectId && t.id === taskId);
},
async createTask() { throw new Error('read-only'); },
async updateTask() { throw new Error('read-only'); },
urlFor({ taskId }) { return `file://./tasks.json#${taskId}`; },
};
That's a working adapter. ATS's find / get / url already work against it.
Stability promise
The required-methods contract is stable across v0.x. Optional methods may be added (Core falls back), never required. Breaking changes happen only at major versions with a deprecation notice in CHANGELOG.md.