# TypeScript reference

> The primitives-sdk client: groups and every primitive's methods.

Source: https://useprimitives.com/docs/sdk/typescript

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

```bash
npm i primitives-sdk
```

## Client

```ts
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. Omit it for `primitives dev`, which takes no credentials. |
| `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 integer milliseconds.

## 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`. Workspace keys only |
| `p.groups.get(name)` | `Group` |
| `p.groups.list({ limit?, cursor? })` | `Page<Manifest>`. Workspace keys only |
| `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`. Workspace keys only |
| `group.audit({ type?, primitive?, action?, actor?, since?, until?, limit?, cursor? })` | `Page<AuditEntry>` for the group. Workspace keys only; cloud (or BYOC) only, not on Starter |

## 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?, limit?, cursor? })` | `Page<PrimitiveInfo>`. A scoped API key sees only its own group |
| `p.primitives.all({ type?, group? })` | `AsyncGenerator<PrimitiveInfo>` across every page |
| `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."`. Workspace keys only; cloud (or BYOC) only, not on Starter. See [Audit log](/docs/concepts/audit) |
| `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:

```ts
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:

| Method | Returns |
| --- | --- |
| `info()` | `PrimitiveInfo`: `{ id, name, type, group, created_at, paused, size_bytes, forked_from }` |
| `delete()` | `void`. Deletes the primitive and its data. Workspace keys only |
| `rename(name)` | `PrimitiveInfo`. Same id, same group. Workspace keys only |
| `snapshot(name?)` / `snapshots()` | `SnapshotInfo` / `SnapshotInfo[]`, newest first. Not on Starter (`plan_limit`) |
| `restore(snapshot)` | `SnapshotInfo`. Takes a snapshot name or id. Not on Starter |
| `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. Not on Starter |
| `pause()` / `resume()` | `PrimitiveInfo`. Data calls on a paused primitive throw `paused`. Workspace keys 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. Workspace keys only; cloud (or BYOC) only, not on Starter |

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({ branch? })` / `table(name, { branch? })` | `TableInfo[]` / `TableDetail`: `{ name, type, columns, sql }`, where `columns` are `ColumnInfo`: `{ name, type, notnull, default, pk }`. `branch` inspects a branch instead of main |
| `branch(name)` | `Branch`, with `query`, `sql`, `exec`, `batch`, `tables()`, `table(name)`, `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:

```ts
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`. Omitted fields carry over from the old memory, and `owner` always does; `tags: []` clears the tags |
| `consolidate({ threshold? })` | `{ merged, superseded }` |

## Vault: `p.vault(id)`

| Method | Returns |
| --- | --- |
| `set(name, value)` / `unset(name)` | `SecretInfo` / `void`. Workspace keys only |
| `list()` | `SecretInfo[]`: `{ name, version, created_at, updated_at, grant }`, never values. Every secret, across all pages |
| `grant(name, { allow: { hosts, methods? }, ttl? })` | `Grant`. Workspace keys only |
| `grants()` / `revoke(name)` | `Grant[]`, every grant across all pages / `void`. `revoke` is workspace keys only |
| `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 }`; `list` and `entries` leave `value` out unless `values: true`. A failed `ifVersion` throws `precondition_failed`. See [KV](/docs/primitives/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](/docs/primitives/storage).

## Errors

Every API error is thrown as a `PrimitivesError` with `status`, `code`, `message` and `requestId`:

```ts
import { PrimitivesError } from 'primitives-sdk';

const trial = await db.branch('trial');

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](/docs/api/errors).
