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/sdk

Client

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
OptionDescription
tokenA pk_ API key, workspace-wide or scoped, or a local root token. Omit it for primitives dev without --token.
baseUrlDefaults to https://api.useprimitives.com for pk_ keys, otherwise PRIMITIVES_URL or http://localhost:7777.
maxRetriesRetries for 429 responses, and for 5xx on GET, PUT, DELETE and HEAD, with exponential backoff that honours Retry-After. Default 3.
fetchA 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.

MethodReturns
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:

MethodReturns
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 id

Every handle has the same lifecycle methods:

MethodReturns
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)

MethodReturns
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)

MethodReturns
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)

MethodReturns
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)

MethodReturns
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)

MethodReturns
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)

MethodReturns
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.