# TypeScript

> TypeScript SDK reference.

Canonical page: https://xmemory.ai/typescript/

The `xmemory` npm package is a lightweight TypeScript client for the xmemory API. Zero dependencies — it uses native `fetch` only.

For MCP-based integration, see the [MCP](https://xmemory.ai/mcp/) guide. For framework-specific setup, see [Mastra AI](https://xmemory.ai/mastra-ai/).

**API key:** To use xmemory APIs or integrations, you need an API key. Get one from the [xmemory console](https://console.xmemory.ai). Copy and securely store the key. Never share your API key publicly.

The full method, data-type, enum, and exception listing is in the [Reference](#reference) at the bottom of this page.

---

## Installation

```bash
npm install xmemory
```

---

## Quick start

```typescript

// Connect (reads XMEM_API_URL and XMEM_API_KEY from env)
const xm = new XmemoryClient({ apiKey: "your-api-key" });

// Get a handle for an existing instance
const inst = xm.instance("your-instance-id");

// Write some information
await inst.write("Alice Johnson works at Acme Corp. Her email is alice@acme.com.");
await inst.write("Bob Lee is a designer at Globex. He joined last Monday.");

// Read it back
const result = await inst.read("What is Alice's email?");
console.log(result.reader_result);
// → {"answer": "alice@acme.com"}
```

---

## Creating a client

Three options, depending on whether you want a health check:

```typescript

// Factory function — runs a health check automatically
const xm = await xmemoryInstance({ url: "https://api.xmemory.ai", apiKey: "..." });

// Static method — same behavior
const xm = await XmemoryClient.create({ url: "https://api.xmemory.ai", apiKey: "..." });

// Constructor — no health check
const xm = new XmemoryClient({ url: "https://api.xmemory.ai", apiKey: "..." });
```

| Option | Env var | Default | Description |
|---|---|---|---|
| `apiKey` | `XMEM_API_KEY` | `undefined` | API key for authentication |
| `url` | `XMEM_API_URL` | `https://api.xmemory.ai` | API base URL |
| `timeoutMs` | — | `60000` | Default request timeout in milliseconds |

All three forms resolve `apiKey` and `url` from environment variables when not passed. Only `xmemoryInstance()` and `XmemoryClient.create()` perform a health check on construction.

Every method also accepts an `options.timeoutMs` (a per-call timeout in milliseconds that overrides the client default); it's omitted from the per-method options in the [Reference](#reference) to keep them focused on the method-specific options.

### Instance handle

Use `xm.instance(id)` to get a scoped handle for data operations:

```typescript
const inst = xm.instance("your-instance-id");

// All data operations go through the handle
await inst.write("Carol is a senior engineer at Initech.");
const result = await inst.read("Who works at Initech?");
console.log(result.reader_result);
```

---

## Writing

Send free-form text — xmemory extracts structured objects according to your schema and merges them into the knowledge graph.

```typescript
const resp = await inst.write("Carol is a senior engineer at Initech.");
console.log(resp.write_id);
console.log(resp.changes); // what the write created / updated / deleted
console.log(resp.trace_id); // request trace id when available
console.log(resp.console_url); // this operation's trace in the console
```

`write()`, `read()`, `writeAsync()`, `writeStatus()` and `extract()` responses all carry `console_url`, a direct link to that operation's trace in the console; every one except `writeStatus()` also carries `trace_id`, which is useful for correlating client calls with API logs. The link points at the call rather than at an individual record, and is `null` when the deployment has no console configured. Requires `xmemory@3.6.0` or later.

### Extraction logic

Control the speed/accuracy tradeoff:

```typescript
await inst.write("...", { extractionLogic: "deep" });
```

| Value | When to use |
|---|---|
| `"fast"` | High-volume, low-stakes writes (default) |
| `"deep"` | Important or complex information |

### Async writes

For latency-sensitive code, enqueue a write and return immediately:

```typescript
const { write_id } = await inst.writeAsync("Dave manages the London office.");
console.log(write_id); // use this to poll for completion
```

The result carries `trace_id` and `console_url` like every other call — see [Writing](#writing).

Then check the status:

```typescript
const status = await inst.writeStatus(write_id);
console.log(status.write_status);
// → "queued" | "processing" | "extracting" | "extracted" | "applying" | "completed" | "failed" | "not_found"
```

Do not call `read` immediately after `writeAsync` — the data may not be committed yet. Poll with `writeStatus` until `"completed"`, or use `write` (synchronous) when you need to read right after.

Writes are applied in the order they were queued — `write`, `writeAsync` and structured writes alike — so a later write never lands ahead of an earlier one. Two calls in flight at once have no defined order; await `writeAsync` before sending a write that must apply after it. See [Write ordering](https://xmemory.ai/api/#write-ordering).

### Structured writes

When you already know exactly what to store, skip extraction: pass a `WriteMutation[]` instead of text (an empty array throws). Each mutation is a deterministic, LLM-free create / update / delete of an object or relation, applied in array order — later mutations may reference objects created earlier in the same batch.

```typescript
const resp = await inst.write([
  {
    object_mutation: {
      object_type: "person",
      create: { key: { email: "carol@initech.com" }, values: { name: "Carol" } },
    },
  },
  {
    relation_mutation: {
      relation_type: "works_at",
      create: {
        endpoints: [
          { object_name: "person", key: { email: "carol@initech.com" } },
          { object_name: "company", key: { name: "Initech" } },
        ],
      },
    },
  },
]);
console.log(resp.changes); // exactly what was created / updated / deleted
```

An `object_mutation` carries exactly one of `create` / `update` / `delete` (enforced at compile time). `key` identifies an object by its user-defined primary-key fields, or (for update/delete) by `{ xuid: ... }`. A `null` value in object `values` **clears** that field. A `relation_mutation` creates or deletes a relation through its endpoints; this caller-addressing format does not mean object primary keys are required for relations extracted from text. XMD v1 relations do not have scalar fields:

```typescript
await inst.write([
  {
    object_mutation: {
      object_type: "person",
      update: { key: { email: "carol@initech.com" }, values: { role: null } },
    },
  },
]);
```

`writeAsync` accepts a `WriteMutation[]` identically — poll with `writeStatus` as usual. See the [API reference](https://xmemory.ai/api/#structured-writes) for the full wire format, relation addressing (`endpoints`, `allow_bulk_delete`), and in-batch ordering rules.

### Scoped writes

A write normally reconciles whatever the extractor produced against every record in the instance — the right default for "here is something new", the wrong one for "here is more about *this* record". Pass a `scope` to anchor a text write to records you already know about:

```typescript

const scope: WriteScope = {
  objects: [{ type: "Person", key: { name: "Alice Johnson" } }],
};
const resp = await inst.write(
  "After her promotion she is a surgeon, and her desk phone is +1-555-0100.",
  { scope }
);
console.log(resp.changes); // Alice updated in place, not duplicated
```

The write is **confined** to the scope: free to modify or delete the scoped objects and to create new objects and relations anchored to them, but by default failing with an `XmemoryAPIError` if it would touch anything else. The confinement is checked against the resulting plan rather than asked of the extractor, so it holds regardless of what the extraction produced. Extraction itself is unchanged by a scope — the text still names the objects it means. See [Scoped writes](https://xmemory.ai/api/#scoped-writes) for the wire format and the full list of rejections.

`WriteScope` takes the same `ScopeObject`s as `ReadScope`, identified the same way. Scope everything the write should be able to change — a record left out may still be linked to, but not edited:

```typescript
await inst.write("The Atlas migration finished on Tuesday and Bob Lee signed it off.", {
  scope: {
    objects: [
      { type: "Project", key: { name: "Atlas" } },
      { type: "Person", key: { name: "Bob Lee" } },
    ],
  },
});
```

Unlike `ReadScope` there is no `relationsScope`: relation *changes* are governed by the confinement rule rather than by a mode of their own.

`writeAsync` accepts the same `scope`; in the default mode a scope violation is reported by `writeStatus` as a failed write.

#### Skip what falls outside instead of failing

`mode: "drop"` turns that failure into a report: the out-of-scope changes are skipped, the rest of the write lands, and only objects the scope names by primary key may be created — so a named object that is **not stored yet** is how you say "create or update exactly this one". Requires `xmemory@3.14.0` or later.

```typescript
const resp = await inst.write("Alice adopted a cat named Luna. Her old dog Rex is fine.", {
  scope: {
    objects: [
      { type: "Person", key: { name: "Alice Johnson" } },
      { type: "Pet", key: { name: "Luna" } }, // not stored yet
    ],
    mode: "drop",
  },
});
for (const skip of resp.changes.skipped_out_of_scope ?? []) {
  console.log(skip.operation, skip.object_type_name, skip.fields, skip.count);
}
```

Luna is created and linked to Alice; anything the text implied about Rex is left alone and comes back under `changes.skipped_out_of_scope`, which is absent when nothing was skipped. Each entry carries the operation, the object type, the field names it would have written, how many records it stands for, and an `identity` that is empty unless your scope named that object. `writeStatus` carries the same summary for an async write.

#### Before you reach for it

- Scope applies to **text writes only**, and the types say so: the `WriteMutation[]` overload takes `{ timeoutMs? }`, so passing a scope alongside structured mutations does not compile. Those bypass extraction and leave a scope nothing to anchor to.
- Only objects of a type with a **user-defined primary key** can be scoped: a scope names records by that key, so a type declared `primary_key: []` has nothing to name its records by.
- The server caps how many objects one scope may name, can have scoped writes disabled entirely, and refuses `mode: "drop"` when the diff engine is off. Those are deployment rules, so they surface as an `XmemoryAPIError` rather than being second-guessed client-side.
- A scoped write additionally requires **read** permission on the instance, on top of write permission: the response carries the previous value of every field the write changed, resolving a scope answers whether each named object is stored, and a drop-mode report says what the skipped changes would have written.

---

## Reading

Ask questions in natural language:

```typescript
const resp = await inst.read("Who works at Acme Corp?");
console.log(resp.reader_result);
console.log(resp.trace_id);
```

### Read modes

```typescript
// Plain-text answer (default)
const resp = await inst.read("What is Alice's email?", { readMode: "single-answer" });

// Structured objects and relations
const resp = await inst.read("Show all contacts", { readMode: "xresponse" });

// Raw SQL result sets
const resp = await inst.read("List all contacts", { readMode: "raw-tables" });
```

### Composite questions

A question holding several independent parts is answered in one call. The server splits it into sub-questions, answers each on its own, and runs them against one shared snapshot. Nothing has to be enabled or passed.

```typescript
const resp = await inst.read("Who owns billing, and when did the last incident close?");

resp.reader_result; // the combined answer
for (const part of resp.reader_results) {
  console.log(part.sub_query, part.reader_result, part.error);
}
```

Each answered part's `reader_result` is in the `readMode` you asked for, so a `raw-tables` composite question gives one table per part.

`reader_results` is always an array — the wire response omits it for a single-intent question and the client normalizes that to `[]`, so an empty array means the question was not split. A part that could not be answered sets its own `error` and leaves the others intact, so check `error` per entry rather than assuming the whole call failed. A sub-question the schema provably cannot represent carries `null` rather than an empty result — that is an answer, meaning this memory cannot hold the concept, and it is worth trying a better-matching instance.

### Related types

Ask with `includeRelatedTypes: "types"` and `related_types` on the result says what else the memory could answer about: the object types the read touched, the fields it did not return, and the object types a declared relation links them to, with a catalog of every named type and relation. The shape is documented on the [REST API](https://xmemory.ai/api/#related-types).

```typescript
const resp = await inst.read("Which courses require an English test?", { includeRelatedTypes: "types" });
for (const touched of resp.related_types?.touched ?? []) {
  console.log(touched.object_type, touched.fields_not_returned, touched.related.map((link) => link.object_type));
}
```

`related_types` is `null` unless the read asked for it. The API key needs `instance.get_own`, the permission the schema endpoints need; without it the read throws `XmemoryAPIError` with `status` `403` and a message naming the permission. Available from `xmemory` 3.10.0.

Add `relatedTypesDepth: 2` (up to 3) to follow the relations further: every entry under `related_types.objects` then carries its `distance` from the touched types and, below the last level, its own `related` edges; `related_types.depth` echoes the depth asked for and `omitted_objects` counts what the server's budgets kept out of the catalog ([shape](https://xmemory.ai/api/#following-relations-further)). On a one-level read these fields are absent. Available from `xmemory` 3.12.0.

```typescript
const deep = await inst.read("Which courses require an English test?", { includeRelatedTypes: "types", relatedTypesDepth: 2 });
console.log(Object.fromEntries(Object.entries(deep.related_types?.objects ?? {}).map(([name, entry]) => [name, entry.distance])));
```

### Scoped reads

Restrict a query to a specific set of objects. Useful for multi-tenant systems, privacy controls, and compliance requirements.

```typescript

const scope: ReadScope = {
  objects: [
    {
      type: "Person",
      key: { name: "Alice Johnson" },
    },
  ],
  relationsScope: "no_relations",
};
const resp = await inst.read("What is their email?", { scope });
console.log(resp.reader_result);
```

Each `ScopeObject` names one object by its `type` — the PascalCase class name or the snake_case table name — plus its user-defined primary `key`, with one entry per primary-key field. Only objects of a type that declares a primary key can be scoped.

#### Multiple objects with relations

```typescript
const scope: ReadScope = {
  objects: [
    { type: "Person", key: { email: "alice@example.com" } },
    { type: "Person", key: { email: "bob@example.com" } },
  ],
  relationsScope: "all_relations",
};
const resp = await inst.read(
  "Which projects do these people work on together?",
  { scope }
);
```

#### Objects that are not there

A `ScopeObject` naming an object the instance does not hold does **not** throw. It drops out of
the scope, and the read answers from whatever is left — an empty result when nothing matched, or
the objects that do exist when only some did. So you can scope to an object before you know
whether it has been written yet, which is the opposite of a scoped *write*, where every named
object must exist.

The trade-off: an aggregate is over the objects that were found, not the ones you listed. If it
matters that every entry resolved, read the objects back rather than trusting a count over them.
See [Scoped reads](https://xmemory.ai/api/#scoped-reads) for the full rules, including the malformed scopes that do
still throw.

#### Scope options

`relationsScope` takes one of two values:

- `"no_relations"` (default): Only the specified objects.
- `"all_relations"`: Also expose the relations among the specified objects.

#### Connecting an instance elsewhere

`admin.getSetupInstructions(instanceId)` and `instance.setupInstructions()` both return an
`AgentSetupResult`: how to reach the same memory from another agent surface, ordered
most-likely-first. On either handle, because the MCP instance connection serves the same tool.

```ts
const setup = await inst.setupInstructions();
for (const surface of setup.surfaces) {
  console.log(surface.label);
  for (const step of surface.steps) console.log(" ", step.description, step.command ?? "");
}
```

The default `SetupFormat.AGENT` answers *what do I run right now, here*. `SetupFormat.PROJECT`
also returns the files a team commits once:

```ts

const setup = await inst.setupInstructions({ format: SetupFormat.PROJECT });
if (setup.format === SetupFormat.PROJECT) {
  for (const f of setup.project?.fragments ?? []) {
    console.log(f.path, f.merge); // a merge, never a file to overwrite
  }
}
```

**Read `setup.format` rather than assuming.** A server older than that parameter ignores it,
answers 200, and names no format at all — so `undefined` means *this deployment predates the
project rendering*. That differs deliberately from the Python client, which applies an `AGENT`
default in its model; there is no runtime normalization point here, and inventing one would report
a format the server never claimed.

Nothing returned carries a credential: the steps tell a reader to sign in themselves, out of band,
so an instance id stays an identifier rather than a key.

Advisory values — `step.kind`, `fragment.merge`, `format` — are widened to admit a value added to
the server after this release, so an additive change does not become a breaking one. A `step.kind`
you do not recognise is not something to execute.

## Error handling

```typescript

try {
  const resp = await inst.read("...", { scope });
} catch (e) {
  if (e instanceof XmemoryAPIError && e.status === 400) {
    console.error(`Invalid scope: ${e.message}`);
  } else {
    throw e;
  }
}
```

---

## Async writes

For latency-sensitive code, enqueue a write and return immediately:

```typescript
const { write_id } = await inst.writeAsync("Dave manages the London office.");
console.log(write_id); // use this to poll for completion
```

The result carries `trace_id` and `console_url` like every other call — see [Writing](#writing).

Then check the status:

```typescript
const status = await inst.writeStatus(write_id);
console.log(status.write_status);
// → "queued" | "processing" | "completed" | "failed" | "not_found"
```

Do not call `read` immediately after `writeAsync` — the data may not be committed yet. Poll with `writeStatus` until `"completed"`, or use `write` (synchronous) when you need to read right after.

Writes are applied in the order they were queued — `write`, `writeAsync` and structured writes alike — so a later write never lands ahead of an earlier one. Two calls in flight at once have no defined order; await `writeAsync` before sending a write that must apply after it. See [Write ordering](https://xmemory.ai/api/#write-ordering).

---

## Extracting (without writing)

Preview what xmemory would extract from a piece of text, without storing anything:

```typescript
const resp = await inst.extract("Dave manages the London office.");
console.log(resp.objects_extracted);
console.log(resp.trace_id);
```

Accepts the same `extractionLogic` option as `write`.

---

## Describing (agent tool discovery)

The `describe()` method returns the agent-facing tool descriptions for an instance, with its schema in `schemaSummary`. Use it to tell an LLM what tools are available and how to call them.

```typescript
const desc = await inst.describe();

// Plain text — inject into a system prompt
console.log(desc.asText());

// Anthropic tool-use format
const tools = desc.asAnthropicTools();

// OpenAI function-calling format
const tools = desc.asOpenaiTools();
```

Results are cached locally for 5 minutes. To force a refresh (e.g. after updating the schema):

```typescript
inst.clearDescribeCache();
const desc = await inst.describe();
```

`asText()` shows tools as method signatures by default. Pass `{ includeHttp: true }` to also show HTTP method and path for raw REST callers.

`describe()` also carries what the instance is *for*:

```typescript
desc.purpose;            // what the memory is for (the instance description)
desc.ownerInstructions;  // the standing preference set for it, verbatim
desc.usageBrief;         // generated from the schema; null until generated
```

`asText()` includes the first two — the purpose under the instance line, the
standing preference above the schema summary. `usageBrief` is left out of it,
since it restates the schema summary already there; read the property if you
want it.

Both are free text set by anyone holding edit permission on the instance, so
`asText()` labels each with where it came from rather than presenting it as the
library's own words. Those labels state provenance; they are not a security
boundary. If you inject this into a system prompt you are still handling text
you do not control.

---

## Cluster and instance management

All management operations live on `xm.admin`: discovering clusters, generating schemas, creating instances, and evolving them safely.

### Clusters

```typescript
const clusters = await xm.admin.listClusters();
const clusterId = clusters[0].id;
```

### Schema generation

Pass a detailed plain-language memory description to the generator when that is easier than
authoring [XMD](https://xmemory.ai/xmd/) directly. Both paths are supported; the generator saves work while producing
the same contract, whose descriptions fine-tune extraction and permit grounded derivation rather
than merely defining domain terminology:

```typescript
const schema = await xm.admin.generateSchema(
  clusterId,
  "Track contacts with name, email, company, and notes.",
);
console.log(schema.data_schema);
```

To evolve an existing schema (rename / remove / type change) rather than create one, retrieve the current XMD and use `enhanceSchema` — it returns a structured migration plan; see [Schema evolution](#schema-evolution).

### Instances

Create an instance from a generated schema:

```typescript

const inst = await xm.admin.createInstance(
  clusterId,
  "contacts",
  schemaYml,
  SchemaType.YML,
  { description: "User contacts" },
);
// inst is a bound InstanceHandle — use inst.write(), inst.read(), etc.
await inst.write("Alice joined the team.");
```

For a purely additive change (new objects/fields/relations), update the schema directly — existing data is preserved:

```typescript
await xm.admin.updateInstanceSchema(instanceId, newSchemaYml, SchemaType.YML);
```

List and delete instances:

```typescript
const instances = await xm.admin.listInstances();
const info = await xm.admin.getInstance(instanceId);

await xm.admin.deleteInstance(instanceId);
```

### Agent-facing instance metadata

An instance can carry metadata that shapes how agents connect to it and what they
do with it. Use `patchInstanceMetadata` to set the advisory hints: every option
is independent — **omitting one leaves the stored value untouched**, and passing
`null` clears it.

```typescript

await xm.admin.patchInstanceMetadata(instanceId, {
  // Advisory hints — they seed what a connect flow proposes, and grant nothing.
  agentSurfaces: [AgentSurface.CLAUDE_CODE, AgentSurface.CODEX],
  agentDefaultBindingTier: BindingTier.AUTOLOAD,
  agentEngagementHints: ["a convention is learned or corrected"],
});
```

Concurrent edits to these three are last-writer-wins by design: they only seed
what a connect flow proposes, so the loser of a race re-applies a suggestion.
`agent_owner_instructions` is not like that.

Read it back from `InstanceInfo`. The hints come back as plain strings, so a
value your server knows and your library version does not is returned rather
than making the instance unreadable:

```typescript
const info = await xm.admin.getInstance(instanceId);
info.agent_owner_instructions;
info.agent_surfaces;              // e.g. ["claude_code", "codex"]
info.agent_default_binding_tier;  // e.g. "autoload"
```

Set `agentOwnerInstructions` through `updateInstanceMetadata`, not
`patchInstanceMetadata`. It is rendered to agents verbatim and a second writer
edits it from the same screen, so a silently lost edit is a rule that stops
being enforced — and `updateInstanceMetadata` is the only call that carries
`expectedOwnerInstructionsEpoch`. Pass the epoch you read the value at and the
losing save is refused instead of applied:

```typescript
const info = await xm.admin.getInstance(instanceId);
await xm.admin.updateInstanceMetadata(instanceId, info.name, info.description ?? "", {
  agentOwnerInstructions: `${info.agent_owner_instructions ?? ""}\nAlso: never paraphrase a rule.`,
  expectedOwnerInstructionsEpoch: info.agent_owner_instructions_epoch,
});
```

### Schema evolution

Non-additive changes (rename / remove / type change) preserve data via a structured **migration plan**. Use `enhanceSchema` to get the plan, `dryRunMigration` to preview the DDL, then `updateInstanceSchema` to apply it. `confirmDestructive: true` is required for ops that drop data.

```typescript

const current = (await xm.admin.getInstanceSchema(instanceId)).data_schema;

// 1. Enhance — new schema + an executor-ready migration plan.
const enhanced = await xm.admin.enhanceSchema(
  clusterId,
  "Rename Person.mail to Person.email.",
  yaml.dump(current),
);
console.log(enhanced.summary, enhanced.migration_plan?.ops);

const newYaml = yaml.dump(enhanced.data_schema);

// 2. Dry-run — preview the DDL, apply nothing.
const preview = await xm.admin.dryRunMigration(instanceId, newYaml, SchemaType.YML, {
  migrationPlan: enhanced.migration_plan ?? undefined,
});
console.log(preview.statements);

// 3. Update — apply. A rename is non-destructive, so confirmDestructive stays false.
const info = await xm.admin.updateInstanceSchema(instanceId, newYaml, SchemaType.YML, {
  migrationPlan: enhanced.migration_plan ?? undefined,
  confirmDestructive: false,
});
console.log(info.migration_id, info.prior_version, "->", info.new_version);
```

Review applied migrations:

```typescript
const page = await xm.admin.listMigrations(instanceId, { limit: 20 });
for (const r of page.items) console.log(r.id, r.source, r.prior_version, "->", r.new_version);

const detail = await xm.admin.getMigration(instanceId, page.items[0].id, { includeYaml: true });
console.log(detail.yaml_before, detail.yaml_after);
```

Migration ops are a discriminated union on `op_type` (`MigrationPlan`,
`AddField`, `RenameField`, `RemoveObject`, …). `ProposalItem.op` and
`MigrationRecord.ops` stay raw for forward compatibility — narrow them to
`MigrationOp` when needed.

### Suggestion engine (review → decide → apply)

xmemory watches read traffic and, on demand, surfaces a single rolling proposal
of schema improvements. The flow is three calls — never wrap them; the
checkpoints are the point.

```typescript

const inst = xm.instance(instanceId);

// 1. Review — proposal + optimistic-concurrency token.
const review = await inst.reviewSuggestions();
if (review.status === "evolution_in_progress") {
  console.log(`Migration in flight; retry in ${review.retry_after_seconds}s`);
} else if (review.proposal) {
  for (const item of review.proposal.items) {
    console.log(item.item_fingerprint, item.rationale, item.op);
  }

  // 2. Decide — accept / reject / defer per item, in bulk. Leave out items
  //    flagged apply_blocked: accepting one fails the whole apply.
  const decisions: DecisionInput[] = review.proposal.items
    .filter((i) => !i.apply_blocked)
    .map((i) => ({
      item_fingerprint: i.item_fingerprint,
      decision: "accept",
    }));
  if (decisions.length > 0) {
    const decided = await inst.decideSuggestions(review.proposal.proposal_version, decisions);

    // 3. Apply — commit accepted decisions as one migration.
    const applied = await inst.applyPendingDecisions(decided.next_proposal_version);
    console.log(applied.status, applied.summary);
  }
}
```

An item with `apply_blocked: true` cannot be applied as proposed — for example a field that another item in the same proposal already adds as part of a new object — and its `rationale` says why. Accepting it fails the whole apply, so a bulk accept leaves it out; the remaining items apply together. When every item is flagged there is nothing to accept: skip the decide call, which refuses an empty batch with `invalid_decision_input`. Requires `xmemory@3.15.0` or later.

---

## Error handling

All errors throw `XmemoryAPIError` with optional `.status` (HTTP status), `.code` (structured error code, when the server returned one), `.details`, and `.retryAfter` (number of seconds parsed from the `Retry-After` header, or `undefined`).

```typescript

try {
  const xm = await XmemoryClient.create({ apiKey: "..." });
} catch (e) {
  if (e instanceof XmemoryHealthCheckError) {
    console.error("Server unreachable:", e.message);
  }
}

try {
  await inst.write("...");
} catch (e) {
  if (e instanceof XmemoryAPIError) {
    console.error(`Error (HTTP ${e.status}): ${e.message}`);
  }
}
```

The schema-evolution methods set `.code` to a structured error type you can match on — e.g. `stale_proposal_version`, `destructive_confirmation_required`, `non_additive_change_requires_plan`:

```typescript
try {
  await inst.applyPendingDecisions(token);
} catch (e) {
  if (e instanceof XmemoryAPIError && e.code === "stale_proposal_version") {
    const review = await inst.reviewSuggestions(); // re-review and retry
  }
}
```

The same `.code` branching separates a spent allowance from a genuine rate limit — match on `.code`, not on the status:

```typescript
try {
  await inst.write("...");
} catch (e) {
  if (e instanceof XmemoryAPIError) {
    if (e.code === "QUOTA_EXCEEDED") {
      // Plan/usage allowance exhausted; e.details.kind is
      // "daily_quota_exceeded" or "monthly_quota_exceeded".
      // Not retryable now — surface "limit reached / upgrade your plan".
      // You may retry after e.details.retry_after_seconds (if not null).
    } else if (e.code === "RATE_LIMITED") {
      // Genuine rate limit — back off and retry, honoring Retry-After.
    }
  }
}
```

For both `RATE_LIMITED` (429) and a resettable `QUOTA_EXCEEDED` (402), use `e.retryAfter` — the seconds parsed from the `Retry-After` header (a `number`, or `undefined` when the header was absent) — as your backoff delay.

---

## Reference

The complete method, data-type, enum, and exception listing. Methods are grouped by where they live; expand a row for its parameters, returns, and details.

### Client

Constructors and client-level accessors.

**`new XmemoryClient(options?)`** — Create a client (no health check)

Parameters:
- `options.apiKey` (`string`) — API key for authentication. Falls back to `XMEM_API_KEY`.
- `options.url` (`string`) — API base URL. Falls back to `XMEM_API_URL`. Defaults to `https://api.xmemory.ai`.
- `options.timeoutMs` (`number`) — Default request timeout in milliseconds. Defaults to `60000`.

Returns: `XmemoryClient`

**`XmemoryClient.create(options?)`** — Create a client with health check

Parameters:
- `options.apiKey` (`string`) — API key for authentication. Falls back to `XMEM_API_KEY`.
- `options.url` (`string`) — API base URL. Falls back to `XMEM_API_URL`. Defaults to `https://api.xmemory.ai`.
- `options.timeoutMs` (`number`) — Default request timeout in milliseconds. Defaults to `60000`.

Returns: `Promise<XmemoryClient>`

**`xmemoryInstance(options?)`** — Factory with health check (same as .create())

Parameters:
- `options.apiKey` (`string`) — API key for authentication. Falls back to `XMEM_API_KEY`.
- `options.url` (`string`) — API base URL. Falls back to `XMEM_API_URL`. Defaults to `https://api.xmemory.ai`.
- `options.timeoutMs` (`number`) — Default request timeout in milliseconds. Defaults to `60000`.

Returns: `Promise<XmemoryClient>`

**`xm.admin`** — Control-plane accessor — clusters, schema generation, instances, schema evolution

Returns: `xm.admin` — the receiver for the admin / control-plane methods (clusters, schema generation, instances, schema evolution).

**`xm.instance(instanceId)`** — Get a handle for data operations on an instance (synchronous)

Parameters:
- `instanceId` (`string`, required) — The instance to scope data operations to.

Returns: A bound `InstanceHandle` — call `write` / `read` / `extract` / … on it. (Synchronous; not a Promise.)

**`xm.checkHealth()`** — Throws XmemoryHealthCheckError if the API is unreachable

Returns: `Promise<void>`

### Admin methods

Control-plane methods on `xm.admin` — clusters, schema generation, instances, and schema evolution.

**`listClusters(options?)`** — List clusters

Parameters:
- `options.ids` (`string[]`) — Filter to specific cluster IDs.

Returns: Each item is a [`ClusterInfo`](#ClusterInfo).

**`getCluster(clusterId, options?)`** — Get a cluster by ID

Parameters:
- `clusterId` (`string`, required) — The cluster to fetch.

Returns: A [`ClusterInfo`](#ClusterInfo).

**`generateSchema(clusterId, schemaDescription, options?)`** — Generate a schema from a description

Parameters:
- `clusterId` (`string`, required) — The cluster to generate the schema for.
- `schemaDescription` (`string`, required) — Natural-language description of what to track.
- `options.currentYmlSchema` (`string`) — Existing schema to extend additively.

Returns: `GenerateSchemaResult` — `{ data_schema }`.

**`enhanceSchema(clusterId, schemaDescription, currentYmlSchema, options?)`** — Evolve a schema → new schema + migration plan

Parameters:
- `clusterId` (`string`, required) — The cluster the instance belongs to.
- `schemaDescription` (`string`, required) — Natural-language description of the change.
- `currentYmlSchema` (`string`, required) — The current schema as YAML.

Returns: `EnhanceSchemaResult` — `{ data_schema, migration_plan: MigrationPlan | null, summary, warnings, repair_log }`. Pass `migration_plan` to `dryRunMigration` / `updateInstanceSchema`.

**`createInstance(clusterId, name, schemaText, schemaType, options?)`** — Create an instance; resolves to a bound handle

Parameters:
- `clusterId` (`string`, required) — The cluster to create the instance in.
- `name` (`string`, required) — Instance name.
- `schemaText` (`string`, required) — The schema source.
- `schemaType` (`SchemaType`, required) — Schema format (`SchemaType.YML` or `SchemaType.JSON`).
- `options.description` (`string`) — Instance description.
- `options.schemaDescription` (`string`) — Plain-language schema description.

Returns: A bound `InstanceHandle` for the new instance — read `inst.id` for its ID.

**`listInstances(options?)`** — List instances

Parameters:
- `options.ids` (`string[]`) — Filter to specific instance IDs.

Returns: Each item is an [`InstanceInfo`](#InstanceInfo).

**`getInstance(instanceId, options?)`** — Get an instance by ID

Parameters:
- `instanceId` (`string`, required) — The instance to fetch.

Returns: An [`InstanceInfo`](#InstanceInfo).

**`getInstanceSchema(instanceId, options?)`** — Get an instance's schema

Parameters:
- `instanceId` (`string`, required) — The instance to fetch the schema for.

Returns: An [`InstanceSchemaInfo`](#InstanceSchemaInfo).

**`updateInstanceSchema(instanceId, schemaText, schemaType, options?)`** — Update a schema; pass migrationPlan for non-additive changes

Parameters:
- `instanceId` (`string`, required) — The instance to update.
- `schemaText` (`string`, required) — The new schema source.
- `schemaType` (`SchemaType`, required) — Schema format (`SchemaType.YML` or `SchemaType.JSON`).
- `options.migrationPlan` (`MigrationPlan`) — Executor-ready plan for a non-additive change.
- `options.confirmDestructive` (`boolean`) — Required for ops that drop data. Defaults to `false`.

Returns: An [`InstanceInfo`](#InstanceInfo) — also carries `migration_id`, `prior_version`, `new_version`, `migration_warnings` when a migration ran.

Errors: Throws `XmemoryAPIError` with `.code` `non_additive_change_requires_plan` (no plan) or `destructive_confirmation_required` (drops data without `confirmDestructive`).

**`updateInstanceMetadata(instanceId, name, description, options?)`** — Update instance name and description

Parameters:
- `instanceId` (`string`, required) — The instance to update.
- `name` (`string`, required) — New instance name.
- `description` (`string`, required) — New instance description.
- `options.agentOwnerInstructions` (`string | null`) — Standing preference for how agents should use this instance. Omit to leave it as it is; pass `null` to clear it. Max 2000 characters.
- `options.expectedOwnerInstructionsEpoch` (`number`) — The `agent_owner_instructions_epoch` your edit was composed from; a save that raced someone else's edit is then refused rather than overwriting it.

Returns: The updated [`InstanceInfo`](#InstanceInfo). Omitting an option sends no such key at all, so a rename never disturbs the standing preference.

Errors: When `expectedOwnerInstructionsEpoch` is sent and the stored instructions changed after you read them, the save is refused rather than applied: `XmemoryAPIError` with `.status` `409` and `.code` `INVALID_INPUT`. Re-read the instance, reapply your change, and save again.

**`patchInstanceMetadata(instanceId, options?)`** — Change some metadata fields, leaving the rest alone

Parameters:
- `instanceId` (`string`, required) — The instance to update.
- `options.name` (`string`) — New instance name.
- `options.description` (`string | null`) — New instance description (pass `null` to clear).
- `options.agentSurfaces` (`readonly string[] | null`) — Agent surfaces this instance is expected to be used from — see `AgentSurface`. Advisory: it orders the connect steps and grants nothing.
- `options.agentDefaultBindingTier` (`string | null`) — Default prominence for a binding — see `BindingTier`. Advisory.
- `options.agentEngagementHints` (`readonly string[] | null`) — Short routing phrases, e.g. `"a convention is learned or corrected"`. At most 16, each at most 200 characters.
- `options.agentOwnerInstructions` (`string | null`) — Standing preference for how agents should use this instance. Max 2000 characters. Prefer `updateInstanceMetadata` for this field — it is the only call that accepts `expectedOwnerInstructionsEpoch`, so an edit sent here cannot be guarded against a concurrent one.

Returns: The updated [`InstanceInfo`](#InstanceInfo). Every option is independent: omit one and the stored value is untouched, pass `null` to clear it. This is the only call that accepts the three `agent*` hints; `updateInstanceMetadata` is the only one that accepts the epoch guard.

**`getSetupInstructions(instanceId, options?)`** — How to connect this instance on an agent surface, most likely first

Parameters:
- `instanceId` (`string`, required) — The instance to describe connect steps for.
- `options.format` (`SetupFormatValue`) — `SetupFormat.AGENT` (default) returns the steps to run now. `SetupFormat.PROJECT` additionally returns the files a team commits once, so nobody sets the instance up by hand.

Returns: `Promise<AgentSetupResult>`

Details: Answers "how do I also reach this memory somewhere else" — a desktop client, another
editor, a teammate's machine — rather than how to reach it from here. The same payload
the `get_setup_instructions` MCP tool serves and `xmemcli instance setup` prints.

Also on an instance handle as `instance.setupInstructions()`, without the id.

**Carries no credential.** The steps tell a reader to sign in themselves, out of band, so
nothing returned is a secret and an instance id stays an identifier rather than a key.

Read `result.format` rather than assuming. A server older than the `format` parameter
ignores it, answers 200, and names no format at all — so `undefined` is the signal that
the deployment predates the project rendering. This differs from the Python client, which
applies an `AGENT` default in its model; there is no runtime normalization point here,
and inventing one would report a format the server never claimed.

Advisory values are widened rather than closed: `step.kind`, `fragment.merge` and
`format` admit a value added to the server after this release, so an additive change does
not become a breaking one. A `step.kind` you do not recognise is not something to execute.

**`deleteInstance(instanceId, options?)`** — Delete an instance

Parameters:
- `instanceId` (`string`, required) — The instance to delete.

Returns: `string[]` — the deleted instance IDs.

**`dryRunMigration(instanceId, schemaText, schemaType, options?)`** — Preview a migration's DDL

Parameters:
- `instanceId` (`string`, required) — The instance to preview against.
- `schemaText` (`string`, required) — The proposed schema source.
- `schemaType` (`SchemaType`, required) — Schema format (`SchemaType.YML` or `SchemaType.JSON`).
- `options.migrationPlan` (`MigrationPlan`) — Executor-ready plan for a non-additive change.
- `options.confirmDestructive` (`boolean`) — Required for ops that drop data. Defaults to `false`.

Returns: `DryRunResult` — `{ status, instance_id, current_version, statements: string[] (planned DDL), warnings, plan_summary: PlanSummary, requires_metadata_sync }`.

**`listMigrations(instanceId, options?)`** — List applied migrations (newest first)

Parameters:
- `instanceId` (`string`, required) — The instance to list migrations for.
- `options.limit` (`number`) — Maximum number of records to return. Defaults to `50`.
- `options.beforeId` (`string`) — Page cursor: pass the previous page's `next_before_id`.
- `options.includeYaml` (`boolean`) — Include before/after YAML in each record.

Returns: `ListMigrationsResult` — `{ items: MigrationRecord[], next_before_id: string | null, has_more }`.

**`getMigration(instanceId, migrationId, options?)`** — Get a single migration record

Parameters:
- `instanceId` (`string`, required) — The instance the migration belongs to.
- `migrationId` (`string`, required) — The migration record to fetch.
- `options.includeYaml` (`boolean`) — Include before/after YAML in the record.

Returns: `MigrationRecord` — `{ id, applied_at, source, prior_version, new_version, ops, ops_summary, yaml_before, yaml_after }`.

### Instance methods

Data-plane methods on a bound instance handle (`xm.instance(id)`) — writing, reading, extracting, describing, and the suggestion engine.

**`write(text | mutations, options?)`** — Persist data — extract objects from text, or apply structured mutations

Parameters:
- `text` (`string`) — Free-form text to extract from. Pass either this or `mutations`.
- `mutations` (`WriteMutation[]`) — Ordered list of deterministic, LLM-free object create/update/delete and relation create/delete mutations — see [Structured writes](https://xmemory.ai/typescript/#structured-writes). An empty array throws.
- `options.extractionLogic` (`"fast" | "deep"`) — Speed/accuracy tradeoff. Text form only. Defaults to `"fast"`.
- `options.diffEngine` (`boolean`) — Override the server's diff-engine setting for this write. Text form only.
- `options.scope` (`WriteScope`) — Anchor a text write to concrete existing objects; the write is then confined to them. With the default `mode: "reject"` touching any other existing object fails the write, and with `mode: "drop"` those changes are skipped and reported under `changes.skipped_out_of_scope` instead. Text form only: the `mutations` overload takes `{ timeoutMs? }`, so the combination does not compile. It additionally requires read permission on the instance. See [Scoped writes](https://xmemory.ai/typescript/#scoped-writes).

Returns:
- `write_id` (`string`) — ID of the committed write.
- `changes` (`WriteChanges`) — What the write did, grouped into `created` / `updated` / `deleted` (each `unknown`), plus `skipped_out_of_scope` on a drop-mode scoped write.
- `trace_id` (`string | null`) — Correlate the call with API logs.

**`writeAsync(text | mutations, options?)`** — Enqueue a write; resolves with write_id immediately

Parameters:
- `text` (`string`) — Free-form text to extract from. Pass either this or `mutations`.
- `mutations` (`WriteMutation[]`) — Same structured-mutation input as `write`.
- `options.extractionLogic` (`"fast" | "deep"`) — Speed/accuracy tradeoff. Text form only. Defaults to `"fast"`.
- `options.diffEngine` (`boolean`) — Override the server's diff-engine setting for this write. Text form only.
- `options.scope` (`WriteScope`) — Anchor a text write to concrete existing objects; the write is then confined to them. With the default `mode: "reject"` touching any other existing object fails the write, and with `mode: "drop"` those changes are skipped and reported under `changes.skipped_out_of_scope` instead. Text form only: the `mutations` overload takes `{ timeoutMs? }`, so the combination does not compile. It additionally requires read permission on the instance. See [Scoped writes](https://xmemory.ai/typescript/#scoped-writes).

Returns:
- `write_id` (`string`) — Use this to poll for completion with `writeStatus`.

**`writeStatus(writeId, options?)`** — Poll the status of an async write

Parameters:
- `writeId` (`string`, required) — The write to poll, from `writeAsync`.

Returns:
- `write_id` (`string`) — Echoes the polled write ID.
- `write_status` (`WriteQueueStatus`) — Current queue status of the write.
- `error_detail` (`string | null`) — Failure detail when the write failed.
- `completed_at` (`string | null`) — When the write finished, when available.
- `changes` (`WriteChanges`) — What the completed write did, in the same shape `write` returns — including `skipped_out_of_scope` for a drop-mode scoped write. Absent until the write completes.

**`read(query, options?)`** — Query the instance in natural language

Parameters:
- `query` (`string`, required) — Natural-language question.
- `options.readMode` (`"single-answer" | "raw-tables" | "xresponse"`) — Response shape. Defaults to `"single-answer"`.
- `options.scope` (`ReadScope`) — Restrict the read to concrete objects, and optionally to the relations among them. See [Scoped reads](https://xmemory.ai/typescript/#scoped-reads).
- `options.includeRelatedTypes` (`RelatedTypesMode`) — `"types"` asks for `related_types` on the result. Sent as `include_related_types` only when set. Needs `instance.get_own` on the API key. See [Related types](https://xmemory.ai/typescript/#related-types).
- `options.relatedTypesDepth` (`number`) — With `"types"`, how many relation levels `related_types.objects` follows out from the touched types, 1 to 3. Sent as `related_types_depth` only when set. See [Related types](https://xmemory.ai/typescript/#related-types).
- `options.traceId` (`string`) — Correlate the call with API logs.

Returns:
- `reader_result` (`unknown`) — The answer, shaped by `readMode` (e.g. `{ answer }` for single-answer).
- `reader_results` (`TaggedReaderResult[]`) — One entry per sub-question when a composite question was split server-side — each `{ sub_query, reader_result, error }`, with every `reader_result` in the requested mode. Always an array: the wire response omits it for a single-intent question and the client normalizes that to `[]`, so an empty array means the question was not split. A sub-question that could not be answered sets its own `error` and leaves the others intact. A sub-question the schema provably cannot represent carries `null` rather than an empty result — that is an answer, meaning this memory cannot hold the concept, and it is worth trying a better-matching instance.
- `related_types` (`RelatedTypes | null`) — The schema around the answer, when the read asked for it with `includeRelatedTypes`; `null` otherwise. See [Related types](https://xmemory.ai/typescript/#related-types).
- `trace_id` (`string | null`) — Correlate the call with API logs.

**`extract(text, options?)`** — Extract objects without writing them

Parameters:
- `text` (`string`, required) — Free-form text to extract from.
- `options.extractionLogic` (`"fast" | "deep"`) — Speed/accuracy tradeoff. Defaults to `"fast"`.

Returns:
- `objects_extracted` (`unknown`) — The objects that would be stored (`objects` / `relations`).
- `trace_id` (`string | null`) — Correlate the call with API logs.

**`describe(options?)`** — Get agent-facing tool descriptions (cached 5 min)

Parameters:
- `options.includeHttp` (`boolean`) — Also show HTTP method and path for raw REST callers.

Returns: `DescribeResult` — properties `instanceId`, `instanceName`, `schemaSummary`, `tools`, plus `purpose` (what this memory is for), `ownerInstructions` (the standing preference set for it, verbatim) and `usageBrief` (generated from the schema; `null` until generated); methods `asText({ includeHttp? })`, `asAnthropicTools()`, `asOpenaiTools()`. `asText()` includes the first two, each labelled with where it came from rather than as the library's own words, and leaves `usageBrief` out since it restates `schemaSummary`. (Class with camelCase props, unlike the snake_case wire results.)

**`clearDescribeCache()`** — Force next describe() to fetch fresh data (synchronous)

Returns: `void`

**`getSchema(options?)`** — Get this instance's schema

Returns: An [`InstanceSchemaInfo`](#InstanceSchemaInfo).

**`setupInstructions(options?)`** — How to connect *this* instance somewhere else, most likely surface first

Parameters:
- `options.format` (`SetupFormatValue`) — `SetupFormat.AGENT` (default) returns the steps to run now; `SetupFormat.PROJECT` also returns the files a team commits once.

Returns: `Promise<AgentSetupResult>`

Details: The instance-scoped counterpart of `admin.getSetupInstructions()`, which takes an id. On
both because the MCP instance connection serves the same tool, so a caller holding an
instance handle need not reach through `admin` for it.

Computed from the instance's current metadata, so an owner who edits a surface hint
expects the next call to show it.

**`reviewSuggestions(options?)`** — Get the rolling schema-improvement proposal

Parameters:
- `options.sessionId` (`string`) — Session ID for tracing.

Returns: `ReviewSuggestionsResult` — `{ status, proposal: ConsolidatedProposal | null, retry_after_seconds }`. When `status` is `"evolution_in_progress"`, back off for `retry_after_seconds` — it's a status, not a thrown error.

**`decideSuggestions(proposalVersion, decisions, options?)`** — Record accept/reject/defer decisions in bulk

Parameters:
- `proposalVersion` (`string`, required) — Optimistic-concurrency token from `reviewSuggestions`.
- `decisions` (`DecisionInput[]`, required) — Per-item decisions — `{ item_fingerprint, decision, edits? }`.
- `options.sessionId` (`string`) — Session ID for tracing.

Returns: `DecideSuggestionsResult` — `{ decisions_recorded, warnings, next_proposal_version }`. Pass `next_proposal_version` to `applyPendingDecisions`.

Errors: Throws `XmemoryAPIError` with `.code` `stale_proposal_version` (token out of date) or `invalid_decision_input`.

**`applyPendingDecisions(proposalVersion, options?)`** — Apply accepted decisions as one migration

Parameters:
- `proposalVersion` (`string`, required) — The `next_proposal_version` from `decideSuggestions`.
- `options.sessionId` (`string`) — Session ID for tracing.

Returns: `ApplyPendingDecisionsResult` — `{ status ("ok" | "nothing_to_apply"), migration_id, prior_version, new_version, applied_items, summary, warnings, notes }`.

Errors: Throws `XmemoryAPIError` with `.code` `stale_proposal_version`, `dependency_closure_failed`, or `apply_failed`.

### Types

**Data types**

**`ClusterInfo`** — A cluster.
- `id` (`string`) — Cluster ID.
- `org_id` (`string`) — Owning organization ID.
- `name` (`string`) — Cluster name.
- `description` (`string | null`) — Cluster description.

**`InstanceInfo`** — An instance's metadata.
- `id` (`string`) — Instance ID.
- `cluster_id` (`string`) — The cluster this instance belongs to.
- `name` (`string`) — Instance name.
- `description` (`string | null`) — Instance description.
- `data_schema` (`object | null`) — The instance's data schema.
- `agent_surfaces` (`string[] | null`) — Agent surfaces this instance is expected to be used from. Advisory; `null` means no opinion. Typed as plain strings, so a value newer than your library version is returned rather than making the instance unreadable.
- `agent_default_binding_tier` (`string | null`) — Default prominence for a binding (`autoload` / `available`). Advisory.
- `agent_engagement_hints` (`string[] | null`) — Short routing phrases seeding when an agent should engage this instance.
- `agent_owner_instructions` (`string | null`) — The standing preference set for how agents should use this instance, rendered verbatim wherever it is shown.
- `agent_owner_instructions_epoch` (`number`) — Which edit of the instructions above this response describes. Pass it back as `expectedOwnerInstructionsEpoch` to make an edit conflict-safe.

**`InstanceSchemaInfo`** — An instance's schema.
- `data_schema` (`object`) — The instance's data schema.

**`ScopeObject`** — One concrete object a read or write scope may touch.
- `type` (`string`) — Object type — PascalCase class name or snake_case table name.
- `key` (`Record<string, string | number | boolean>`, required) — The object's user-defined primary key, one entry per primary-key field. Only objects of a type that declares a primary key can be scoped.

**`ReadScope`** — A read's scope.
- `objects` (`ScopeObject[]`) — The objects the read may see.
- `relationsScope` (`RelationsScope`) — `"no_relations"` (default) or `"all_relations"`, which also exposes the relations among the scoped objects.

**`RelatedTypes`** — What else the memory could answer about, for a read that asked with `includeRelatedTypes`. The shape is documented on the [REST API](https://xmemory.ai/api/#related-types).
- `touched` (`RelatedTypesTouched[]`) — Object types the read touched, each `{ object_type, fields_not_returned, related, omitted_related }` with `related` an array of `RelatedTypesLink` (`object_type`, `relation`, `touched_role`, `related_role`, `cardinality`). Empty when the read executed nothing.
- `objects` (`Record<string, RelatedTypesObjectType>`) — Every object type named in `touched` or reached within `depth` relation levels, once: `description`, `primary_key`, `fields`, and — on a read that asked for a depth above 1 — `distance` (listed edges from the nearest touched type), its own `related` edges below the last level, and `omitted_related`.
- `relations` (`Record<string, RelatedTypesRelation>`) — Every relation named by an edge, once: `description`.
- `depth` (`number`) — Relation levels asked for (`relatedTypesDepth`); absent on a one-level read. Never lowered by the server: what its budgets kept the walk from listing is counted on `omitted_objects`.
- `omitted_touched` (`number`) — Touched types the server dropped to stay within its payload budget.
- `omitted_objects` (`number`) — Object types within `depth` relation levels in the schema that the catalog does not list: the route to them ran through an edge the server's budget cut, or their level was dropped for its byte budget.
- `truncated` (`boolean`) — `true` when any type, edge or level was dropped for the budget.

**`WriteScope`** — A write's scope.
- `objects` (`ScopeObject[]`) — The objects the write is anchored to and confined to. There is no relation policy — relation changes follow the confinement rule rather than a mode of their own.
- `mode` (`WriteScopeMode`) — `"reject"` (default) fails the whole write when it would touch an object outside the scope. `"drop"` skips those changes and reports them under `changes.skipped_out_of_scope`, and creates only objects the scope names by primary key — such an object need not be stored yet. Sent only when set. Defaults to `"reject"`.

**`WriteChanges`** — What a write did.
- `created` (`unknown`) — Objects and relations the write created.
- `updated` (`unknown`) — Objects and relations it changed, with per-field old and new values.
- `deleted` (`unknown`) — Objects and relations it removed.
- `skipped_out_of_scope` (`SkippedOutOfScope[]`) — What a drop-mode scoped write left out; absent when it skipped nothing.

**`SkippedOutOfScope`** — One kind of change a drop-mode scoped write left out.
- `operation` (`string`) — `create`, `update`, `delete`, `merge`, `link` or `unlink`.
- `object_type_name` (`string`) — The object type, or the relation type for a link or unlink.
- `identity` (`string`) — The record's primary key, rendered as `name='Rex'` — empty unless the scope named that record.
- `fields` (`string[]`) — The fields those changes would have written; empty for anything but a write of fields.
- `count` (`number`) — How many records the entry stands for.

**`GenerateSchemaResult`** — A generated schema.
- `data_schema` (`object`) — The produced schema.

**`EnhanceSchemaResult`** — An evolved schema plus an executor-ready migration plan.
- `data_schema` (`object`) — The produced schema.
- `migration_plan` (`MigrationPlan | null`) — Reconciled plan; pass to `updateInstanceSchema` / `dryRunMigration`.
- `summary` (`string | null`) — Human-readable summary of the change.
- `warnings` (`object[]`) — Non-fatal reconciler observations.
- `repair_log` (`object[]`) — Auto-repairs applied to the LLM output.

**`DryRunResult`** — Planned migration DDL, with nothing applied.
- `status` (`"ok"`) — Always `"ok"` for a successful dry run.
- `instance_id` (`string`) — The instance previewed against.
- `current_version` (`number`) — Current schema version.
- `statements` (`string[]`) — The planned DDL statements.
- `warnings` (`string[]`) — Non-fatal warnings.
- `plan_summary` (`PlanSummary`) — Per-op-type breakdown.
- `requires_metadata_sync` (`boolean`) — Whether a metadata sync is needed after applying.

**`MigrationRecord`** — One applied-migration history row.
- `id` (`string`) — Migration record ID.
- `applied_at` (`string`) — When the migration was applied (ISO-8601).
- `source` (`string`) — `direct` or `suggestion_engine`.
- `decided_by` (`string | null`) — Who decided the migration, if recorded.
- `prior_version` (`number`) — Schema version before.
- `new_version` (`number`) — Schema version after.
- `ops` (`object[]`) — The raw ops applied (kept raw for forward-compat).
- `ops_summary` (`PlanSummary`) — Per-op-type breakdown.
- `notes` (`string | null`) — Free-text notes.
- `yaml_before` (`string | null`) — Schema YAML before (only when `includeYaml: true`).
- `yaml_after` (`string | null`) — Schema YAML after (only when `includeYaml: true`).

**`ListMigrationsResult`** — A page of migration records, newest first.
- `status` (`"ok"`) — Always `"ok"`.
- `instance_id` (`string`) — The instance listed.
- `items` (`MigrationRecord[]`) — The records on this page.
- `next_before_id` (`string | null`) — Pagination cursor; `null` at the tail.
- `has_more` (`boolean`) — Whether more pages follow.

**`ReviewSuggestionsResult`** — The rolling schema-improvement proposal (or an in-progress status).
- `status` (`"ok" | "evolution_in_progress"`) — `evolution_in_progress` means a migration is in flight.
- `instance_id` (`string`) — The instance reviewed.
- `proposal` (`ConsolidatedProposal | null`) — The proposal when `status` is `"ok"`.
- `retry_after_seconds` (`number | null`) — Back-off hint when `evolution_in_progress`.

**`DecideSuggestionsResult`** — Result of recording a batch of decisions.
- `status` (`"ok"`) — Always `"ok"`.
- `instance_id` (`string`) — The instance decided on.
- `decisions_recorded` (`RecordedDecision[]`) — One per recorded decision.
- `warnings` (`DependencyWarning[]`) — Advisory dependency warnings.
- `next_proposal_version` (`string`) — Token to pass to `applyPendingDecisions`.

**`ApplyPendingDecisionsResult`** — Result of applying accepted decisions as one migration.
- `status` (`"ok" | "nothing_to_apply"`) — `nothing_to_apply` when no accepted items remained.
- `instance_id` (`string`) — The instance applied to.
- `migration_id` (`string | null`) — The committed migration's ID (when one ran).
- `prior_version` (`number`) — Schema version before.
- `new_version` (`number`) — Schema version after.
- `applied_items` (`string[]`) — Item fingerprints that were applied.
- `summary` (`string`) — Human-readable summary.
- `warnings` (`string[]`) — Non-fatal warnings.
- `notes` (`string[]`) — Additional notes.

**`MigrationPlan`** — An ordered sequence of migration ops — the wire format emitted by `enhanceSchema` and consumed by `updateInstanceSchema` / `dryRunMigration`.
- `ops` (`MigrationOp[]`) — The ordered ops.

**`MigrationOp`** — One migration operation — a discriminated union keyed on `op_type`.
- `op_type` (`string`) — Discriminator: one of `add_object`, `remove_object`, `rename_object`, `change_object`, `add_field`, `remove_field`, `rename_field`, `change_field`, `add_relation`, `remove_relation`, `rename_relation`, `change_relation` — each variant carries operation-specific parameters.

**`PlanSummary`** — Per-op-type breakdown of a migration plan.
- `count_by_op_type` (`Record<string, number>`) — Op count keyed by `op_type`.
- `total` (`number`) — Total number of ops.

**`ConsolidatedProposal`** — The single rolling improvement proposal for an instance.
- `instance_id` (`string`) — The instance the proposal is for.
- `proposal_version` (`string`) — Optimistic-concurrency token for `decideSuggestions` / `applyPendingDecisions`.
- `schema_version` (`number`) — Schema version the proposal targets.
- `items` (`ProposalItem[]`) — The accept/reject/defer-able items.
- `generated_at` (`string`) — When the proposal was generated (ISO-8601).
- `notes` (`string[]`) — Proposal-level notes.

**`ProposalItem`** — One decide-able item inside a proposal.
- `item_fingerprint` (`string`) — Stable ID; pass in a `DecisionInput`.
- `op` (`object`) — The proposed op (raw; cast to `MigrationOp` when needed).
- `evidence_feedback_ids` (`string[]`) — Feedback IDs that motivated the item.
- `evidence_query_samples` (`string[]`) — Sample queries that motivated the item.
- `frequency` (`number`) — How often the supporting signal occurred.
- `depends_on` (`string[]`) — Fingerprints this item depends on.
- `current_decision` (`string | null`) — Existing decision, if any.
- `rationale` (`string`) — Why the item was proposed.
- `apply_blocked` (`boolean | undefined`) — `true` when the change cannot be applied as proposed (for example a field another item already adds as part of a new object); `rationale` says why. Accepting it fails the whole apply, so leave it out of a bulk accept. Absent when the server sends no flag. Requires `xmemory@3.15.0` or later.

**`DecisionInput`** — One decision in a `decideSuggestions` batch.
- `item_fingerprint` (`string`) — The `ProposalItem` to decide on.
- `decision` (`"accept" | "reject" | "defer"`) — The decision for this item.
- `edits` (`object | null`) — Optional op override (must keep the same `op_type`).

**`RecordedDecision`** — A persisted decision.
- `item_fingerprint` (`string`) — The decided item.
- `decision_id` (`string`) — ID of the recorded decision.

**`DependencyWarning`** — An advisory (non-blocking) dependency warning from `decideSuggestions`.
- `kind` (`string`) — Warning kind.
- `item_fingerprint` (`string`) — The item the warning concerns.
- `related_fingerprints` (`string[]`) — Related item fingerprints.
- `related_summaries` (`string[]`) — Human-readable summaries of the related items.
- `guidance` (`string`) — What to do about it.

**`ToolDescription`** — One agent-callable tool in a DescribeResult.
- `name` (`string`) — Tool name.
- `description` (`string`) — What the tool does.
- `when_to_use` (`string`) — Guidance on when to call it.
- `parameters` (`ToolParameterDescription[]`) — The tool's parameters.
- `http_method` (`string`) — Underlying HTTP method.
- `http_path` (`string`) — Underlying HTTP path.

**`ToolParameterDescription`** — One parameter of a ToolDescription.
- `name` (`string`) — Parameter name.
- `type` (`string`) — Parameter type.
- `description` (`string`) — What the parameter is.
- `required` (`boolean`) — Whether it must be supplied.
- `enum` (`string[] | undefined`) — Allowed values, if constrained.
- `default` (`string | undefined`) — Default value, if any.

**Enums**

```typescript
const SchemaType = { YML: 0, JSON: 1 } as const;
```

| Enum | Values |
| --- | --- |
| `SchemaType` | `SchemaType.YML`, `SchemaType.JSON` |
| `ExtractionLogic` | `"fast"`, `"deep"` |
| `ReadMode` | `"single-answer"`, `"raw-tables"`, `"xresponse"` |
| `RelationsScope` | `"no_relations"`, `"all_relations"` |
| `WriteScopeMode` | `"reject"`, `"drop"` |
| `RelatedTypesMode` | `"none"`, `"types"` |
| `RelationCardinality` | `"one_to_one"`, `"one_to_many"`, `"many_to_one"`, `"many_to_many"`, `"unconstrained"` |
| `WriteQueueStatus` | `"queued"`, `"processing"`, `"extracting"`, `"extracted"`, `"applying"`, `"completed"`, `"failed"`, `"not_found"` |

**Exceptions**

| Exception | Parent | Attributes |
| --- | --- | --- |
| `XmemoryAPIError` | `Error` | `.status`, `.code`, `.details`, `.retryAfter` (each `undefined` when absent) |
| `XmemoryHealthCheckError` | `XmemoryAPIError` | `.status` (HTTP status code or `undefined`) |
