Reference
TypeScript SDK
The @primitives/sdk client: groups and every primitive's methods.
@primitives/sdk is the official TypeScript client. It ships ESM and CommonJS builds with full types, uses the global fetch, and needs Node.js 18 or later.
npm i @primitives/sdkClient
import { Primitives } from '@primitives/sdk';
const local = new Primitives(); // PRIMITIVES_URL or http://localhost:7777
const cloud = new Primitives({ token: process.env.PRIMITIVES_API_KEY }); // pk_… → https://api.useprimitives.com
const agent = new Primitives({ token: process.env.AGENT_API_KEY }); // a pk_ key scoped to one group and some tools| Option | Description |
|---|---|
token | A pk_ API key, workspace-wide or scoped, or a local root token. Omit it for primitives dev without --token. |
baseUrl | Defaults to https://api.useprimitives.com for pk_ keys, otherwise PRIMITIVES_URL or http://localhost:7777. |
maxRetries | Retries for 429 responses, and for 5xx on GET, PUT, DELETE and HEAD, with exponential backoff that honours Retry-After. Default 3. |
fetch | A custom fetch, for tests or proxies. |
Durations are strings such as '250ms', '30s', '5m', '1h' or '7d', or numbers of milliseconds. Grant ttls must be strings.
Groups
A group is a collection of primitives reached with one key. It starts empty: create the primitives you need inside it.
| Method | Returns |
|---|---|
p.groups.create({ name }) | Group |
p.groups.get(name) | Group |
p.groups.list({ limit?, cursor? }) | Page<Manifest> |
p.groups.all() | AsyncGenerator<Manifest> across every page |
p.group(name) | Group handle, without a request |
group.info() | GroupInfo: { id, namespace, name, created_at, primitives } |
group.primitives.list({ type? }) | PrimitiveInfo[], every primitive in the group |
group.destroy({ force? }) | void. Deletes an empty group; with force: true, also every primitive in it. Otherwise 409 conflict |
group.audit({ type?, primitive?, action?, actor?, since?, until?, limit?, cursor? }) | Page<AuditEntry> for the group. Account callers only |
Primitives
Every primitive has an immutable id. Create, list and open primitives by id from the client:
| Method | Returns |
|---|---|
p.primitives.create({ name, type, group }) | A typed handle (Database, Filesystem, …) with .id. 409 already_exists if the name is taken in that group for that type |
p.primitives.list({ type?, group? }) | PrimitiveInfo[]. A scoped API key sees only its own group |
p.primitives.get(id) | PrimitiveInfo |
p.primitive(id) | Untyped handle with the lifecycle methods, without a request |
p.audit({ group?, type?, primitive?, action?, actor?, since?, until?, limit?, cursor? }) | Page<AuditEntry> for the whole workspace, newest first. since/until take ms or a Date; action is exact or a prefix like "db.". Account callers only. See Audit log |
p.auditAll(filters?) | AsyncGenerator<AuditEntry> across every page |
p.db(id) / p.fs(id) / p.queue(id) / p.memory(id) / p.vault(id) / p.kv(id) / p.storage(id) | Typed handle, without a request |
group.db, group.fs, group.queue, group.memory, group.vault, group.kv and group.storage are shortcuts that work by name inside one group:
const g = await p.groups.create({ name: 'agent-42' });
const orders = await g.db.create('orders'); // create in this group; orders.id is its id
const cache = g.db('cache'); // resolved by name in the group on first use
const all = await g.db.list(); // PrimitiveInfo[] for every database
const same = p.db(orders.id); // the same database, by idEvery handle has the same lifecycle methods:
| Method | Returns |
|---|---|
info() | PrimitiveInfo: { id, name, type, group, created_at, paused, size_bytes, forked_from } |
delete() | void. Deletes the primitive and its data. Account callers only |
rename(name) / moveTo(group) | PrimitiveInfo. Same id. Account callers only |
snapshot(name?) / snapshots() | SnapshotInfo / SnapshotInfo[], newest first |
restore(snapshot) | SnapshotInfo. Takes a snapshot name or id |
fork({ name?, group?, snapshot? }) | A handle of the same kind for the copy, with a new id. group defaults to the source's and must exist (404 not_found otherwise); snapshot forks from a snapshot instead of the live data |
pause() / resume() | PrimitiveInfo. Data calls on a paused primitive throw paused. Account callers only |
stats() | PrimitiveStats: { id, name, type, size_bytes, items, paused } |
audit({ action?, actor?, since?, until?, limit?, cursor? }) | Page<AuditEntry>: every event about this primitive, data and lifecycle. Account callers only |
A data method that doesn't fit the primitive's type throws wrong_type.
Database: p.db(id)
| Method | Returns |
|---|---|
sql`…${value}…` | QueryResult. Interpolations become bound parameters |
query(sql, params?) / exec(sql, params?) | QueryResult. One statement per call |
batch(statements) / transaction(statements) | QueryResult[]. One transaction, all or nothing |
tables() / table(name) | TableInfo[] / { name, columns, sql } |
branch(name) | Branch, with query, sql, exec, batch, promote({ force? }) and discard() |
branchHandle(name) | Branch, without a request |
branches() | BranchInfo[] |
export() | Uint8Array, the database as a SQLite file |
QueryResult is { columns, rows, rows_affected, last_insert_rowid, truncated }. Parameters are a positional array or an object of named values; Uint8Array values are sent as blobs.
Filesystem: p.fs(id)
| Method | Returns |
|---|---|
write(path, content, { mode? }) | FileInfo |
read(path) / readText(path) | Uint8Array / string |
stat(path) | FileInfo |
list(dir?, { recursive? }) | FileInfo[] |
mkdir(path) | FileInfo |
move(from, to, { overwrite? }) | FileInfo |
remove(path, { recursive? }) | void |
glob(pattern, { limit? }) | string[] |
search(query, { path?, limit? }) | SearchHit[]: { path, snippet } |
forkDir({ from, to? }) | { path, files }. Forks a directory inside this filesystem |
FileInfo is { path, kind, size, hash, mode, mtime }. fork() copies the whole filesystem; forkDir() copies one directory within it.
Queue: p.queue(id)
A queue primitive holds any number of named queues and topics. store.queue(name) returns one queue, created on first send:
const jobs = g.queue('jobs');
await jobs.queue('emails').send({ to: 'ada@example.com' });Method on store.queue(name) | Returns |
|---|---|
send(body, { delay?, dedupeKey?, priority?, maxAttempts? }) | JobData & { deduplicated } |
receive({ lease?, wait?, max? }) | Job[] |
consume({ lease?, wait?, max?, signal? }) | AsyncGenerator<Job>, long-polling |
jobs({ status?, limit?, cursor? }) | Page<JobData> |
get(id) / requeue(id) | JobData |
purge('done' | 'dead', { olderThan? }) | { purged } |
Job has id, body, attempts, lastError and data, plus heartbeat({ extend? }), ack(), retry({ in?, error? }), fail(error?) and dead(error?). store.queues() returns per-queue counts, and store.topics has subscribe(topic, queue), unsubscribe(topic, queue) and publish(topic, body).
Memory: p.memory(id)
| Method | Returns |
|---|---|
remember({ kind, text, tags?, owner?, metadata? }) | MemoryRecord |
recall(query, { kinds?, tags?, owner?, limit?, includeSuperseded? }) | RecallHit[]: { memory, score, bm25_rank, vector_rank } |
list({ kind?, tag?, owner?, limit?, cursor? }) | Page<MemoryRecord> |
get(id) | MemoryRecord |
forget(id) | void |
supersede(id, { text, kind?, tags?, metadata? }) | MemoryRecord |
consolidate({ threshold? }) | { merged, superseded } |
Vault: p.vault(id)
| Method | Returns |
|---|---|
set(name, value) / unset(name) | SecretInfo / void. Account callers only |
list() | SecretInfo[]: { name, version, created_at, updated_at, grant }, never values |
grant(name, { allow: { hosts, methods? }, ttl? }) | Grant. Account callers only |
grants() / revoke(name) | Grant[] / void |
fetch(url, { auth, method?, headers?, body? }) | ProxiedResponse: { status, headers, body, body_encoding, truncated } |
KV: p.kv(id)
| Method | Returns |
|---|---|
get<T>(key) / getMany(keys) | KvEntry<T> | null / (KvEntry | null)[] in order |
set(key, value, { ttl?, ifVersion? }) | KvEntry. value is any JSON value, or a Uint8Array for bytes. Without ttl, any expiry is cleared |
setMany(items, { atomic? }) | KvEntry[]. items are { key, value, ttl?, ifVersion? }, up to 100. Atomic by default; with atomic: false, stops at the first failure and keeps earlier items |
delete(key, { ifVersion? }) / deleteMany(keys) | boolean / number deleted. delete() with no key deletes the store |
incr(key, by?, { ttl? }) | KvEntry<number>. by defaults to 1 |
list({ prefix?, cursor?, limit?, values? }) | Page<KvEntry>, in key order |
entries({ prefix?, values? }) | AsyncGenerator<KvEntry> across every page |
export() | Uint8Array, the store as a SQLite file |
KvEntry is { key, value, encoding, version, expires_at, created_at, updated_at }. A failed ifVersion throws precondition_failed. See KV.
Storage: p.storage(id)
| Method | Returns |
|---|---|
put(key, body, { contentType?, metadata?, ifMatch?, ifNoneMatch? }) | ObjectInfo. body is a Uint8Array, a string or a Blob; ifNoneMatch: '*' creates only |
get(key, { range?, ifNoneMatch? }) | StorageObject | null: { key, body, size, etag, contentType, metadata, lastModified, range }. range is { start, end? } or { suffix }; null also when ifNoneMatch matches |
getText(key) / head(key) | string / ObjectInfo |
copy(from, to, { ifNoneMatch? }) | ObjectInfo |
delete(key, { ifMatch? }) | void. delete() with no key deletes the store |
list({ prefix?, delimiter?, cursor?, limit? }) | ObjectPage: { data, prefixes, next_cursor } |
objects({ prefix?, delimiter? }) | AsyncGenerator<ObjectInfo> across every page |
presign(key, { method?, expiresIn?, contentType?, maxSize?, ifNoneMatch? }) | SignedUrl: { url, method, headers, expires_at }. GET and 15 minutes by default |
revokeSignedUrls() | { epoch }. Invalidates every signed URL issued so far |
export() | Uint8Array, the store as a SQLite file |
ObjectInfo is { key, size, etag, content_type, metadata, created_at, updated_at }. Failed conditions throw precondition_failed, and a range past the end range_not_satisfiable. See Storage.
Errors
Every API error is thrown as a PrimitivesError with status, code, message and requestId:
import { PrimitivesError } from '@primitives/sdk';
try {
await trial.promote();
} catch (err) {
if (err instanceof PrimitivesError && err.code === 'branch_conflict') {
await trial.discard();
} else {
throw err;
}
}The error codes are listed in the REST API reference.