--platform ` | Snapshot an agent workspace |
| `mi backup list` / `mi backup restore ` | Manage snapshots |
| `mi hermes` | Run a local agent connected to your office |
| `mi init --office-manager ` | Point the CLI at production or the sandbox backend |
`mi cortex ingest` takes file paths, not URLs, and refuses a directory. For a
whole folder or an Obsidian vault, use `mi cortex sync-vault`, which recurses,
skips `.obsidian`/`.trash`/`.git`, and chunks notes too large to embed whole.
## Scripting
`mi cortex` commands emit JSON (`ask` renders prose unless you pass `--json`),
and exit codes are POSIX-style (`0` on success, non-zero on failure), so `mi`
composes cleanly inside shell pipelines and CI steps.
```bash
# Ingest, then query, then write the conclusion back.
mi cortex ingest ./handbook/*.md --office "$OFFICE" --feed handbook
mi cortex answer "open questions on billing" --office "$OFFICE" --limit 5 \
| jq -r '.results[] | "\(.score)\t\(.title)\t\(.universal_id)"'
mi cortex remember "Billing migration lands in Q4." \
--office "$OFFICE" --kind decision --confidence 0.8
```
The `--json` surfaces of `ask`, `answer` and `recall` also carry a `graph_url`
that deep-links to the cited nodes on the user's own graph:
```bash
mi cortex ask "what did we decide about pricing?" --office "$OFFICE" --json \
| jq -r '.graph_url'
```
For the same loop in TypeScript, with batching and provenance, see
[ingest and query](/developers/sdk/ingest-and-query).
Cortex operations against a cold or loaded backend can take a while; the client
waits up to 120 seconds by default rather than aborting a healthy request.
Override with `MI_TIMEOUT` (milliseconds).
## The same surface elsewhere
Everything the CLI does is also available over
[REST](/developers/api-reference) and [MCP](/developers/mcp/overview). The CLI is a convenience layer
over the same API, not a separate capability set.
---
# SDK overview
URL: https://mitosislabs.ai/developers/sdk/overview
@mitosislabs/sdk: the TypeScript client for offices, agents, memory and everything else.
```bash
npm install @mitosislabs/sdk
```
The package ships the `MitosisClient` class, the `mi` [CLI](/developers/cli/overview), and a
`mi-cortex-mcp` stdio bridge.
## Your first program
Ingest something, then ask about it. Run with
`MI_API_KEY=mi_... npx tsx first.ts`.
```ts
import { MitosisClient } from '@mitosislabs/sdk';
const client = new MitosisClient({
endpoint: 'https://m.mitosislabs.ai',
apiKey: process.env.MI_API_KEY,
});
const [office] = await client.offices.list();
const officeId = office.id;
const cortex = client.cortex(officeId);
// Create a feed (idempotent), then push a row into it.
const { feedKey } = await cortex.ensureFeed(officeId, 'handbook');
await cortex.pushRows(
officeId,
feedKey,
[{
external_id: 'handbook:pricing',
title: 'Pricing policy',
content: 'Solo moved to $19.99 per seat in July 2026. Team stayed at $49.',
}],
{ deferEmbed: true },
);
// Ask it back. Results carry scores, previews and citable universal_ids.
const answer = await cortex.answer(officeId, { query: 'what did pricing move to?', limit: 5 });
for (const r of answer.results) {
console.log(r.score.toFixed(3), r.title, '|', r.preview);
}
```
[Ingest and query](/developers/sdk/ingest-and-query) walks the same loop in
full: batching, local files, agent-scoped memory, provenance on write-back, the
CLI and plain-HTTP equivalents, and the failure modes worth knowing before you
hit them.
## Construct a client
`endpoint` is required. It points at the office-manager API, not at the website:
```ts
import { MitosisClient } from '@mitosislabs/sdk';
const client = new MitosisClient({
endpoint: 'https://m.mitosislabs.ai',
apiKey: process.env.MI_API_KEY,
});
```
Or reuse whatever `mi login` already stored in `~/.os1/config.json`:
```ts
const client = await MitosisClient.fromConfig();
```
`fromConfig()` prefers an API key over a stored JWT. A stale local dev JWT in
`~/.os1/keys/jwt.key` is ignored whenever a key is present. Without that rule
it would 401 every production request.
## The API modules
Every module hangs off the client and takes `officeId` as its first argument.
| Accessor | Covers |
| --- | --- |
| `client.offices` | [Create, configure, suspend, delete offices](/developers/sdk/offices-and-agents) |
| `client.employees` | [Hire and manage agents](/developers/sdk/offices-and-agents#agents) |
| `client.tasks` | [The task queue](/developers/sdk/tasks) |
| `client.files` | [Shared office storage](/developers/sdk/files-and-env) |
| `client.env` | [Per-office environment variables](/developers/sdk/files-and-env#environment) |
| `client.integrations` | [Connected services and credentials](/developers/sdk/integrations) |
| `client.credits` | [Balance, usage and quotas](/developers/sdk/credits) |
| `client.cortex(officeId)` | [Memory: ingest, ask, remember, graph](/developers/sdk/ingest-and-query) |
| `client.invites`, `client.join` | [External agent onboarding](/developers/guides/external-agents) |
| `client.backups`, `client.snapshots` | Workspace snapshots |
The accessor is `client.employees`, not `client.agents`. Older guides showed
`client.agents.hire(colonyId, …)`; the real call is
`client.employees.hire(officeId, …)`.
## Memory from the SDK
`client.cortex` is a function, not a property. It takes the office id and
returns the API for that memory:
```ts
const cortex = client.cortex(officeId);
// Read. `query` is the field name on answer(), not `question`.
const answer = await cortex.answer(officeId, { query: 'What did we ship in July?', limit: 8 });
const nearest = await cortex.recall(officeId, { query: 'billing friction', limit: 10 });
// Write. `agent` is required: every fact is attributed to somebody.
await cortex.remember(officeId, {
agent: 'atlas',
text: 'Solo moved to $19.99/seat in July 2026.',
kind: 'decision',
confidence: 0.9,
});
// Ingest.
const { feedKey } = await cortex.ensureFeed(officeId, 'handbook');
await cortex.pushRows(officeId, feedKey, rows, { deferEmbed: true });
// Inspect.
const status = await cortex.status(officeId); // per-feed raw/embedded counts
const graph = await cortex.graph(officeId, 200); // nodes + edges
const manifest = await cortex.manifest(officeId); // sources, counts, top entities
```
`manifest()` is an index, not content. It answers "what does this memory
hold" so you can decide whether a question is worth routing to the graph at
all. It reports connected sources, item counts and the entities the graph
knows best; it never returns the items themselves. Added in SDK 0.26.0.
Inside an agent loop, prefer `forAgent`. It attributes writes and carries
provenance from the last `ask()` into the next `remember()` for you:
```ts
const memory = client.cortex(officeId).forAgent(officeId, 'atlas');
const evidence = await memory.ask('what did pricing move to?');
console.log(evidence.context()); // citation-formatted block, ready for a prompt
await memory.remember('Solo is $19.99/seat as of July 2026.', { kind: 'decision' });
```
For bulk ingestion, pass `deferEmbed: true` to `cortex.pushRows(...)` and batch
at 10 to 25 rows per request. Inline embedding of a large batch runs past the
60 second origin timeout. Full worked example:
[ingest and query](/developers/sdk/ingest-and-query).
## Errors
Failures throw `OS1Error`, exported from the package root, carrying the HTTP
status and an optional error code.
```ts
import { OS1Error } from '@mitosislabs/sdk';
try {
await client.offices.get(officeId);
} catch (err) {
if (err instanceof OS1Error) {
// err.status: number
// err.code: string | undefined
// err.message
}
}
```
---
# Ingest and query
URL: https://mitosislabs.ai/developers/sdk/ingest-and-query
A complete, runnable loop: push your own content into a memory, then query it back with citations.
This is the whole loop in one page: put content into a memory, ask it a
question, get cited results, write the conclusion back. Every snippet below is
real code against `@mitosislabs/sdk`, not pseudocode, and the final section is
a single file you can copy, run, and get output from.
## Before you start
```bash
npm install @mitosislabs/sdk
```
You need two things: an API key and an office id. The office is the memory.
```bash
npx -y -p @mitosislabs/sdk@latest mi login # prints a URL to approve
npx -y -p @mitosislabs/sdk@latest mi offices list
```
`mi login` writes both to `~/.os1/config.json`, which is what
`MitosisClient.fromConfig()` reads. For a server or CI, skip the login and set
`MI_API_KEY=mi_...` from the dashboard user menu instead. See
[authentication](/developers/authentication).
```ts
import { MitosisClient } from '@mitosislabs/sdk';
// Explicit: endpoint is the office-manager API, not the website.
const client = new MitosisClient({
endpoint: 'https://m.mitosislabs.ai',
apiKey: process.env.MI_API_KEY,
});
// Or reuse whatever `mi login` stored:
// const client = await MitosisClient.fromConfig();
const [office] = await client.offices.list();
const officeId = office.id;
```
`client.cortex` is a function, not a property. Call it with the office id to
get that memory's API: `const cortex = client.cortex(officeId)`. Every method
on it then takes the office id again as its first argument. That is the real
signature, and it is the mistake most first programs make.
## Step 1: create a feed
A feed is where your rows land. `ensureFeed` creates the backing table and
registers the column mapping, and it is idempotent, so calling it on every run
is the intended usage rather than a wasted call.
```ts
const cortex = client.cortex(officeId);
const { feedKey } = await cortex.ensureFeed(officeId, 'handbook');
// feedKey === 'ext_handbook.integration_feed'
```
The feed id is yours to pick. It becomes the schema name (`ext_`) and
namespaces every row you push, so a second feed never collides with the first.
## Step 2: push rows
`pushRows` takes plain objects. `external_id` and `content` are the only
required fields, and `external_id` is what makes the whole thing idempotent:
re-pushing the same id updates that row in place instead of duplicating it.
```ts
const rows = [
{
external_id: 'handbook:pricing',
title: 'Pricing policy',
content: 'Solo moved to $19.99 per seat in July 2026. Team stayed at $49.',
modality: 'document',
metadata: { owner: 'finance', reviewed: '2026-07-14' },
},
{
external_id: 'handbook:support-sla',
title: 'Support SLA',
content: 'First response within one business day. Sev-1 within one hour.',
modality: 'document',
},
];
const res = await cortex.pushRows(officeId, feedKey, rows, { deferEmbed: true });
console.log(res.raw_persisted ?? res.ingested);
```
| Field | Required | What it does |
| --- | --- | --- |
| `external_id` | yes | Stable identity. Re-pushing the same id updates the row. |
| `content` | yes | The embeddable body. This is what retrieval searches. |
| `title` | no | Embedded alongside content, and used as the citation label. |
| `modality` | no | e.g. `document`, `note`, `file`. |
| `file_path` | no | Office-drive filename when the original was uploaded via `client.files`. |
| `metadata` | no | Arbitrary JSON, stored verbatim and returned with the row. |
Any extra top-level key you add is stored verbatim in `raw_data` and stays
resolvable by a feed's graph rules, which is how the Obsidian feed turns
`wikilinks` and `tags` into real edges.
Pass `deferEmbed: true` for anything bulk. Inline embedding runs inside the
request, which pushes a large batch past 30 to 60 seconds and trips both the
SDK timeout and the 60 second origin timeout in front of it. With defer, the
request returns in seconds and the background drainer vectorises the rows.
Rows are full-text searchable immediately either way.
### Batching a large ingest
Keep each request body small and let `external_id` make the run resumable. This
is the same loop the `mi cortex ingest` command uses internally.
```ts
async function ingestInBatches(
cortex: ReturnType,
officeId: string,
feedKey: string,
rows: Array>,
batchSize = 25,
) {
let ingested = 0;
for (let i = 0; i < rows.length; i += batchSize) {
const batch = rows.slice(i, i + batchSize);
const res = await cortex.pushRows(officeId, feedKey, batch, { deferEmbed: true });
ingested += res.raw_persisted ?? res.ingested ?? batch.length;
console.log(`${Math.min(i + batchSize, rows.length)}/${rows.length}`);
}
return ingested;
}
```
### Ingesting local files
Files are just rows. Read them, push them, and upload the binaries to the
office drive so the row can point at something openable.
```ts
import { readFileSync, statSync } from 'node:fs';
import { basename, resolve } from 'node:path';
import { createHash } from 'node:crypto';
const sha16 = (s: string) => createHash('sha256').update(s).digest('hex').slice(0, 16);
async function ingestFiles(paths: string[]) {
const rows = [];
for (const p of paths) {
const abs = resolve(p);
const name = basename(abs);
const buf = readFileSync(abs);
// Text heuristic: no NUL byte in the first 8KB.
const isText = !buf.subarray(0, 8192).includes(0);
if (!isText) {
await client.files.upload(officeId, name, buf);
}
rows.push({
external_id: `local:${sha16(abs)}:${name}`,
title: name,
content: isText
? buf.toString('utf8')
: `Binary file ${name} (${statSync(abs).size} bytes) stored on the office drive.`,
modality: isText ? 'document' : 'file',
file_path: name,
metadata: { source_path: abs },
});
}
return cortex.pushRows(officeId, feedKey, rows, { deferEmbed: true });
}
```
## Step 3: query it back
`answer()` is hybrid retrieval in one call: vector kNN, full-text, and a one-hop
graph expansion, fused with reciprocal rank fusion. There is no LLM at query
time. You get evidence, and your agent does the reasoning over it.
```ts
const answer = await cortex.answer(officeId, {
query: 'what did pricing move to?',
limit: 5,
});
for (const r of answer.results) {
console.log(r.score.toFixed(3), r.title, r.preview);
console.log(' id:', r.universal_id); // cite this
console.log(' from:', r.source_table);
console.log(' link:', r.source_url ?? '(none)');
}
console.log(answer.signals); // { vector_candidates, text_candidates, graph_added }
console.log(answer.freshness); // per-source last_success_at, so you know how stale this is
```
Every filter is optional and narrows the same call:
```ts
await cortex.answer(officeId, {
query: 'billing friction',
limit: 10,
source_table: 'integration_feed',
integration_id: 'handbook',
since: '2026-07-01T00:00:00Z', // RFC 3339, filters on fetched_at
expand_graph: true, // default; one-hop neighbours at a discounted score
include_raw: false, // true returns full raw_data per result
});
```
For a semantic-only search with no full-text or graph legs, use `recall`:
```ts
const nearest = await cortex.recall(officeId, { query: 'refund policy', limit: 10 });
```
Retrieval returns nearest matches, not a thresholded set. A question whose
answer was never ingested still comes back with the closest thing in the
graph. Read `score` and `freshness` before you treat a result as an answer.
See [nearest matches, not a thresholded set](/developers/memory/overview#nearest-matches-not-a-thresholded-set).
## Step 4: query as an agent, and write back
`forAgent` is the surface to use inside an agent loop. It attributes every write
to the agent, and it carries provenance from the last `ask()` into the next
`remember()` automatically, so conclusions get linked to the evidence that
produced them without you tracking ids by hand.
```ts
const memory = client.cortex(officeId).forAgent(officeId, 'atlas');
// ask -> Evidence
const evidence = await memory.ask('what did pricing move to?');
evidence.top; // best-scored result
evidence.isEmpty; // nothing matched
evidence.universalIds; // the provenance pool
evidence.isFresh(3600); // did every source sync within the last hour?
evidence.graphUrl(); // /graph deep link highlighting exactly these nodes
// A citation-formatted block, ready to drop into a prompt.
console.log(evidence.context());
// Or ask and render in one call:
const promptBlock = await memory.contextFor('what did pricing move to?', { maxItems: 5 });
// Write the conclusion back. Provenance defaults to the last ask().
await memory.remember('Solo is $19.99/seat as of July 2026.', {
kind: 'decision',
confidence: 0.9,
});
```
`evidence.context()` renders something like this, which is what you paste into
a model prompt:
```text
## Colony knowledge: "what did pricing move to?"
_2 item(s); oldest source sync 12m ago._
[1] (integration_feed, 12m ago, score 0.412) Pricing policy
Solo moved to $19.99 per seat in July 2026. Team stayed at $49.
id: ext_handbook:integration_feed:handbook:pricing
Sources:
[1] Pricing policy (ext_handbook:integration_feed:handbook:pricing)
```
The lower-level `remember` takes the agent name explicitly, if you would rather
not hold an `AgentMemory` instance:
```ts
await cortex.remember(officeId, {
agent: 'atlas',
text: 'Solo is $19.99/seat as of July 2026.',
kind: 'decision',
confidence: 0.9,
source_universal_ids: evidence.universalIds,
});
```
## Step 5: check what landed
Embedding is asynchronous when you defer it, so `status()` is how you tell
"still catching up" from "nothing arrived".
```ts
const status = await cortex.status(officeId);
console.log(status.per_feed); // per-feed raw/embedded counts and phase flags
const graph = await cortex.graph(officeId, 200);
console.log(graph.nodes.length, graph.edges.length);
// The original row behind any citation:
const source = await cortex.getRaw(officeId, evidence.top!.universal_id);
```
## The whole thing, in one file
Save as `memory.ts`, run with `MI_API_KEY=mi_... npx tsx memory.ts`.
```ts
import { MitosisClient, OS1Error } from '@mitosislabs/sdk';
async function main() {
const client = new MitosisClient({
endpoint: 'https://m.mitosislabs.ai',
apiKey: process.env.MI_API_KEY,
});
const [office] = await client.offices.list();
if (!office) throw new Error('No office on this account. Create one at mitosislabs.ai.');
const officeId = office.id;
const cortex = client.cortex(officeId);
// 1. Feed (idempotent).
const { feedKey } = await cortex.ensureFeed(officeId, 'handbook');
// 2. Ingest. Re-running updates in place; it never duplicates.
await cortex.pushRows(
officeId,
feedKey,
[
{
external_id: 'handbook:pricing',
title: 'Pricing policy',
content: 'Solo moved to $19.99 per seat in July 2026. Team stayed at $49.',
modality: 'document',
},
{
external_id: 'handbook:support-sla',
title: 'Support SLA',
content: 'First response within one business day. Sev-1 within one hour.',
modality: 'document',
},
],
{ deferEmbed: true },
);
// 3. Query. Full-text hits are available immediately; vector hits once the
// background drainer catches up.
const memory = cortex.forAgent(officeId, 'quickstart');
const evidence = await memory.ask('what did pricing move to?', { limit: 5 });
console.log(evidence.context());
// 4. Write the conclusion back, linked to the evidence above.
if (!evidence.isEmpty) {
const saved = await memory.remember('Solo is $19.99/seat as of July 2026.', {
kind: 'decision',
confidence: 0.9,
});
console.log('remembered as', saved.universal_id, 'with', saved.derived_from_edges, 'edges');
}
console.log('see it on the graph:', evidence.graphUrl());
}
main().catch((err) => {
if (err instanceof OS1Error) {
console.error(`Mitosis ${err.status}${err.code ? ` (${err.code})` : ''}: ${err.message}`);
process.exit(1);
}
throw err;
});
```
## The same loop from a shell
Every SDK call above has a CLI equivalent, so an agent that can only shell out
loses nothing.
```bash
OFFICE=$(npx -y -p @mitosislabs/sdk@latest mi offices list | jq -r '.[0].id')
# Ingest files, or a whole folder of Markdown.
mi cortex ingest ./handbook/pricing.md --office "$OFFICE" --feed handbook
mi cortex sync-vault ~/Notes --office "$OFFICE" --feed obsidian
# Query. `ask` renders the cited block; `answer` gives you the raw JSON.
mi cortex ask "what did pricing move to?" --office "$OFFICE"
mi cortex answer "what did pricing move to?" --office "$OFFICE" --limit 5 \
| jq -r '.results[] | "\(.score)\t\(.title)\t\(.universal_id)"'
# Write back, with provenance.
mi cortex remember "Solo is \$19.99/seat as of July 2026." \
--office "$OFFICE" --kind decision --confidence 0.9 \
--source ext_handbook:integration_feed:handbook:pricing
mi cortex status --office "$OFFICE"
```
## The same loop over plain HTTP
The SDK is a typed wrapper over these routes. `Authorization: Bearer mi_...` is
the same key.
```bash
BASE="https://m.mitosislabs.ai/api/v1/offices/$OFFICE/cortex"
# Ingest
curl -sX POST "$BASE/v1/ingest" \
-H "Authorization: Bearer $MI_API_KEY" \
-H 'content-type: application/json' \
-d '{
"feed_key": "ext_handbook.integration_feed",
"defer_embed": true,
"rows": [{
"external_id": "handbook:pricing",
"title": "Pricing policy",
"content": "Solo moved to $19.99 per seat in July 2026."
}]
}'
# Query
curl -sX POST "$BASE/v1/answer" \
-H "Authorization: Bearer $MI_API_KEY" \
-H 'content-type: application/json' \
-d '{"query": "what did pricing move to?", "limit": 5}' | jq
# Write back
curl -sX POST "$BASE/v1/remember" \
-H "Authorization: Bearer $MI_API_KEY" \
-H 'content-type: application/json' \
-d '{"agent": "atlas", "text": "Solo is $19.99/seat as of July 2026.", "kind": "decision"}'
```
## Things that bite
Pass `deferEmbed: true` and batch at 10 to 25 rows per request. Inline
embedding of a large batch runs past the 60 second origin timeout. The rows
are text-searchable as soon as they persist, so nothing is lost by deferring.
Check `cortex.status(officeId)`. If `per_feed` shows raw rows but no embedded
ones, the background drainer has not caught up yet, and vector hits will
appear shortly. Full-text hits should already be working, so a query using
words that literally appear in `content` is the fastest way to confirm the
rows are really there.
It did not, as long as `external_id` was stable. Ingestion is an upsert on
that id. A `Date.now()` or a random suffix in the id is what actually causes
duplicates.
A leftover local dev JWT in `~/.os1/keys/jwt.key` used to shadow the API key.
`fromConfig()` now prefers the key whenever one is present. If you built the
client by hand, pass `apiKey` and leave `jwt` unset.
`client.cortex` is a factory. Call it first: `client.cortex(officeId).answer(officeId, {...})`.
## Where to go next
Reading scores, freshness, and source gaps rather than trusting rank one.
What is worth remembering, and what only makes retrieval worse.
The office drive that `file_path` rows point at.
Turn a one-off ingest script into a connectable source.
---
# Chaining lookups
URL: https://mitosislabs.ai/developers/sdk/chaining-lookups
Ingest, read what the graph found, query your own databases on the result, and ingest again.
Every method on the SDK returns a promise, so a multi-stage pipeline is
ordinary `async`/`await`. The interesting part is not the syntax, it is that
each stage can depend on **what the previous stage discovered**: you ingest a
document, ask the graph what it mentioned, look those things up in a database
Cortex has never seen, and ingest only what matched.
This page builds one such pipeline end to end.
## The shape of it
A call transcript goes in. Extraction pulls out the companies and people it
mentions and links them to that row.
Read the document's neighbours in the graph and filter to organizations.
This is the conditional: what you look up next depends on this answer.
Your CRM, warehouse, whatever. Cortex has no access to it and does not
need any.
Not the whole CRM. Just the records for companies that actually came up.
Now query a third source for investors and employees of those same
companies, and ingest those too.
## Step 1: ingest, and know when the graph is ready
This is the one piece of sequencing that will bite you, so it comes first.
`pushRows` resolves when the rows are **stored**, which is not the same moment
the graph knows about them. Which of the two you get depends on one option:
| Call | Promise resolves in | Entities queryable |
| --- | --- | --- |
| `pushRows(...)` | ~1.6s for one row | already there |
| `pushRows(..., { deferEmbed: true })` | ~0.3s | ~11s later |
Both numbers are from one row on a live colony. `deferEmbed` is the right
choice for a bulk backfill precisely because it returns before the slow work
finishes. It is the wrong choice when your very next line reads the graph.
If you defer and then immediately read neighbours, you will get an empty list
and conclude the document mentioned nothing. Either ingest inline when the
next stage depends on the result, or poll until the entities land.
A small helper covers the deferred case. Poll rather than sleep a fixed
duration: enrichment time scales with the document.
```typescript
import type { CortexAPI, CortexGraphNode } from '@mitosislabs/sdk';
/** Wait until `universalId` has neighbours of `type`, or give up. */
async function waitForEntities(
cortex: CortexAPI,
officeId: string,
universalId: string,
{ type = 'entity:organization', timeoutMs = 60_000, everyMs = 2_000 } = {},
): Promise {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const { nodes } = await cortex.neighbors(officeId, universalId, { depth: 1 });
const hits = nodes.filter((n) => n.type === type);
if (hits.length > 0) return hits;
await new Promise((r) => setTimeout(r, everyMs));
}
// An empty result is a real answer: the document may mention no companies.
return [];
}
```
## Step 2: ask the graph what the document mentioned
`neighbors` returns the exact subgraph around one item. After an ingest, its
depth-1 neighbours are what extraction pulled out, joined by `evidence` edges.
```typescript
const { nodes } = await cortex.neighbors(officeId, transcriptId, { depth: 1 });
const companies = nodes
.filter((n) => n.type === 'entity:organization')
.map((n) => n.displayName!);
// -> ['northwind robotics', 'calder biosciences']
```
Use `neighbors`, not `graph`. `graph` returns a **server-clamped slice** of
the whole colony for overviews; filtering it for one document's connections
will silently miss edges that fell outside the slice.
Entity labels are normalized, which is what makes them usable as a lookup key
across sources that spell things differently. Match on them case-insensitively
rather than assuming your database uses the same casing.
## Step 3 to 5: the whole pipeline
Three data sources, each consulted only because of what the previous one
returned. `crm` and `capTable` stand in for your own databases; they are
ordinary async functions and Cortex never sees them.
```typescript
import { MitosisClient } from '@mitosislabs/sdk';
const mi = await MitosisClient.fromConfig();
const cortex = mi.cortex(officeId);
async function importCallAndExpand(transcript: string, callId: string) {
// 1. The transcript itself. Inline, because step 2 depends on the graph.
const { feedKey } = await cortex.ensureFeed(officeId, 'calls');
const transcriptId = `ext_calls:integration_feed:${callId}`;
await cortex.pushRows(officeId, feedKey, [
{ external_id: callId, title: `Call ${callId}`, content: transcript, modality: 'document' },
]);
// 2. What did it mention? Everything downstream is conditional on this.
const mentioned = await waitForEntities(cortex, officeId, transcriptId);
const names = mentioned.map((n) => n.displayName!.toLowerCase());
if (names.length === 0) return { companies: [], people: 0 };
// 3. Your CRM. One round trip per company, in parallel: these do not depend
// on each other, so awaiting them in a loop would just be slower.
const records = (await Promise.all(names.map((n) => crm.findCompany(n))))
.filter((r): r is CompanyRecord => r !== null);
// 4. Ingest ONLY the matches, not the CRM. Deferred: nothing below reads
// these back, so there is no reason to wait for embeddings.
const { feedKey: crmFeed } = await cortex.ensureFeed(officeId, 'crm');
await cortex.pushRows(
officeId,
crmFeed,
records.map((c) => ({
external_id: `company/${c.id}`,
title: c.name,
content: `${c.name}. ${c.description} Stage: ${c.stage}. HQ: ${c.hq}.`,
modality: 'record',
// Extra top-level keys stay on the row and are addressable by
// extraction rules, so mentioned_in becomes a real edge back to the call.
mentioned_in: transcriptId,
company_id: c.id,
})),
{ deferEmbed: true },
);
// 5. A third source, keyed on what step 3 actually matched.
const people = (
await Promise.all(records.map((c) => capTable.peopleFor(c.id)))
).flat();
const { feedKey: peopleFeed } = await cortex.ensureFeed(officeId, 'people');
await cortex.pushRows(
officeId,
peopleFeed,
people.map((p) => ({
external_id: `person/${p.id}`,
title: p.name,
content: `${p.name}, ${p.role} at ${p.companyName}.`,
modality: 'record',
company_id: p.companyId,
relation: p.kind, // 'investor' | 'employee'
})),
{ deferEmbed: true },
);
return { companies: records.map((c) => c.name), people: people.length };
}
```
Read the awaits as the dependency graph: steps 3 and 5 fan out with
`Promise.all` because their calls are independent of each other, while the
stages themselves stay sequential because each one needs the previous result.
### Do not fan out without a bound
`Promise.all` over a handful of companies is fine. `Promise.all` over ten
thousand opens ten thousand connections at once and takes down whichever
database is smallest. Cap the width:
```typescript
async function mapLimit(items: T[], limit: number, fn: (t: T) => Promise) {
const out: R[] = [];
for (let i = 0; i < items.length; i += limit) {
out.push(...(await Promise.all(items.slice(i, i + limit).map(fn))));
}
return out;
}
const records = await mapLimit(names, 8, (n) => crm.findCompany(n));
```
The same applies to `pushRows`: batch around 25 rows per call rather than
sending thousands in one request.
## Partial failure
One dead lookup should not lose the work that succeeded. `Promise.allSettled`
keeps the good results and lets you decide about the rest:
```typescript
const settled = await Promise.allSettled(names.map((n) => crm.findCompany(n)));
const records = settled
.filter((r): r is PromiseFulfilledResult => r.status === 'fulfilled')
.map((r) => r.value)
.filter((c): c is CompanyRecord => c !== null);
const failed = names.filter((_, i) => settled[i].status === 'rejected');
if (failed.length) console.warn('CRM lookup failed for', failed);
```
Ingest is idempotent per `external_id`, so re-running the pipeline after fixing
the outage updates the rows it already wrote instead of duplicating them. That
is what makes a retry safe.
## The callback form
If you would rather not hold the pipeline open, drive it from the change feed
instead. `pollLiveData` calls you back as rows land, and returns a function
that stops it:
```typescript
import { pollLiveData } from '@mitosislabs/sdk';
const stop = pollLiveData(cortex, officeId, 'calls', async ({ events }) => {
for (const e of events) {
if (e.layer !== 'raw' || e.sourceTable !== 'integration_feed') continue;
await expandOneCall(e.universalId); // steps 2 to 5, per row
}
});
// later
stop();
```
Same pipeline, different trigger. Reach for the callback form when documents
arrive continuously and for `async`/`await` when you are processing a known
batch, because a straight line is far easier to debug.
## Undoing a run
A pipeline that ingests derived records will eventually ingest wrong ones. Each
stage wrote under its own feed, so each can be removed on its own, and
[purge](/developers/memory/metadata#deleting) previews by default:
```typescript
// What would removing this run's CRM records take with it?
const plan = await cortex.purge(officeId, { source_schema: 'ext_crm' });
console.log(plan.matched, 'rows,', plan.entities_deleted, 'entities');
await cortex.purge(officeId, { source_schema: 'ext_crm', dry_run: false });
```
Verify a purge with `neighbors` or a direct read, not with `status`: the
status surface is cached and will happily report the old counts for a while
after the rows are gone.
---
# Offices and agents
URL: https://mitosislabs.ai/developers/sdk/offices-and-agents
Create an office, hire agents into it, and manage their lifecycle.
An **office** is one memory graph plus the agents that work against it. The API
calls it an office and gives it an `officeId`; the product calls it a memory.
## Offices
```ts
const office = await client.offices.create({ name: 'research' });
const all = await client.offices.list();
const one = await client.offices.get(officeId);
```
| Method | Signature |
| --- | --- |
| `create` | `create(req: CreateOfficeRequest)` |
| `list` | `list()` |
| `get` | `get(officeId)` |
| `getSettings` | `getSettings(officeId)` |
| `updateSettings` | `updateSettings(officeId, settings)` |
| `status` | `status(officeId)`, cluster status |
| `suspend` | `suspend(officeId)` |
| `delete` | `delete(officeId)` |
| `transfer` | `transfer(officeId, newOwnerId)` |
| `rotateSecret` | `rotateSecret(officeId)` |
| `setSecret` | `setSecret(officeId, secretName, value)` |
| `kubeconfig` | `kubeconfig(officeId)` |
`delete(officeId)` tears the office down. Snapshot anything you need first;
see `client.backups`.
## Agents
Agents are `employees` in the API.
```ts
const agent = await client.employees.hire(officeId, {
name: 'scout',
role: 'research assistant',
modelTier: 'standard',
skills: ['search', 'summarize'],
});
```
`HireRequest` accepts `name` (required), plus `role`, `modelTier`, `skills`,
`channels`, `resources`, `storage`, `selfConfigure`, `autoUpdate`, `chromium`,
`webTerminal` and `imageTag`.
| Method | Signature |
| --- | --- |
| `hire` | `hire(officeId, req)` |
| `list` | `list(officeId)` |
| `get` | `get(officeId, name)` |
| `update` | `update(officeId, name, req)` |
| `delete` | `delete(officeId, name)` |
| `logs` | `logs(officeId, name, { tail })` |
| `activity` | `activity(officeId, name, query)` |
| `presence` | `presence(officeId, name)` → `{ online, last_seen }` |
| `lastError` | `lastError(officeId, name)` |
| `action` | `action(officeId, name, action)` |
| `promote` | `promote(officeId, name, req)` |
| `setSkills` | `setSkills(officeId, name, req)` |
Agents are addressed by **name** within an office, not by an opaque id:
```ts
const logs = await client.employees.logs(officeId, 'scout', { tail: 200 });
const live = await client.employees.presence(officeId, 'scout');
```
## Debugging a quiet agent
`presence(officeId, name)` returns `online` and `last_seen`.
`lastError(officeId, name)` returns the most recent error and its timestamp.
`activity(officeId, name, query)` returns the activity feed; `logs` returns
raw output.
---
# Tasks
URL: https://mitosislabs.ai/developers/sdk/tasks
The office task queue: create, claim, complete, and attach artifacts.
Tasks are the work queue shared by the agents in an office. An agent claims work,
reports progress, and completes or fails it.
## Creating and listing
```ts
const task = await client.tasks.create(officeId, {
title: 'Summarize competitor pricing changes',
description: 'Cover the three named competitors and cite sources.',
kind: 'research',
priority: 2,
assigned_to: 'scout',
});
const open = await client.tasks.list(officeId, { status: 'open', limit: 50 });
const mine = await client.tasks.list(officeId, { assigned: 'scout' });
const stats = await client.tasks.stats(officeId);
```
`CreateTaskRequest` requires `title`; `description`, `kind`, `priority` and
`assigned_to` are optional. `list` filters on `status`, `kind`, `limit` and
`assigned`.
## The worker loop
```ts
const task = await client.tasks.claim(officeId, 'research');
try {
await client.tasks.log(officeId, task.id, 'fetching sources');
const result = await doTheWork(task);
await client.tasks.complete(officeId, task.id, result);
} catch (err) {
await client.tasks.fail(officeId, task.id, String(err));
}
```
| Method | Signature |
| --- | --- |
| `create` | `create(officeId, req)` |
| `list` | `list(officeId, { status, kind, limit, assigned })` |
| `get` | `get(officeId, taskId)` |
| `stats` | `stats(officeId)` |
| `update` | `update(officeId, taskId, patch)` |
| `claim` | `claim(officeId, kind?)` |
| `complete` | `complete(officeId, taskId, result)` |
| `fail` | `fail(officeId, taskId, error)` |
| `log` | `log(officeId, taskId, message)` |
| `comment` | `comment(officeId, taskId, message)` |
| `subtasks` | `subtasks(officeId, taskId)` |
| `addArtifact` | `addArtifact(officeId, taskId, { kind, path, label })` |
`taskId` accepts a string or a number.
## Artifacts
Attach outputs so the result is inspectable rather than just described:
```ts
await client.tasks.addArtifact(officeId, task.id, {
kind: 'report',
path: 'competitor-pricing-q3.md',
label: 'Q3 pricing summary',
});
```
Paths refer to the office's shared storage. See [files](/developers/sdk/files-and-env).
`log` is a running progress trail; `comment` is a message to humans reading the
task. Use `log` inside the worker loop and `comment` when a person needs to
know something.
---
# Files and environment
URL: https://mitosislabs.ai/developers/sdk/files-and-env
The office's shared drive, per-file permissions, and environment variables.
## Shared files
Every office has one shared drive. Agents read and write it, and per-file
permissions control which agent sees what.
```ts
await client.files.upload(officeId, 'report.md', Buffer.from(markdown));
const files = await client.files.list(officeId);
const res = await client.files.download(officeId, 'report.md');
const text = await res.text();
await client.files.delete(officeId, 'stale.csv');
```
| Method | Signature |
| --- | --- |
| `upload` | `upload(officeId, filename, data: Buffer \| Uint8Array)` |
| `list` | `list(officeId)` |
| `download` | `download(officeId, filename)` → `Response` |
| `delete` | `delete(officeId, filename)` |
| `changes` | `changes(officeId, since?)` |
| `getPermissions` | `getPermissions(officeId, filename)` |
| `setPermissions` | `setPermissions(officeId, filename, perms)` |
`download` returns a `Response`, so pick the body off it yourself with `.text()`,
`.arrayBuffer()`, or stream it.
### Watching for changes
`changes(officeId, since)` takes a millisecond timestamp and returns what moved
since then. Poll it rather than re-listing the whole drive:
```ts
let cursor = Date.now();
setInterval(async () => {
const { events, serverTime } = await client.files.changes(officeId, cursor);
cursor = serverTime ?? Date.now();
for (const c of events) handle(c);
}, 3000);
```
### Permissions
Permissions are per file, per agent. The default is that an agent sees the
drive, and you narrow from there:
```ts
await client.files.setPermissions(officeId, 'payroll.csv', {
agent_name: 'scout',
access: 'none',
});
```
## Environment variables
`client.env` holds per-office configuration injected into agent runtimes.
```ts
await client.env.set(officeId, 'REPORT_WEBHOOK', 'https://example.com/hook');
const vars = await client.env.list(officeId);
const values = await client.env.values(officeId);
const forOne = await client.env.getAgentEnv(officeId, 'scout');
await client.env.delete(officeId, 'REPORT_WEBHOOK');
```
| Method | Signature |
| --- | --- |
| `list` | `list(officeId)` → `EnvVar[]` |
| `values` | `values(officeId)` → `Record` |
| `set` | `set(officeId, key, value)` |
| `delete` | `delete(officeId, key)` |
| `getAgentEnv` | `getAgentEnv(officeId, name)` |
Environment variables are visible to the agents in the office. Credentials for
third-party services belong in [integrations](/developers/sdk/integrations), which scopes
them per agent, not in plain env vars.
---
# Integrations
URL: https://mitosislabs.ai/developers/sdk/integrations
Connect third-party services and scope their credentials per agent.
Integrations hold the credentials for third-party services and decide which
agents may use them. Prefer them over [environment variables](/developers/sdk/files-and-env#environment)
for anything secret, because they are scoped per agent and revocable.
## Listing what is connected
```ts
const connected = await client.integrations.list(officeId);
const models = await client.integrations.listProviderModels(officeId, 'anthropic');
```
## Credentials
```ts
await client.integrations.ensureSecret(officeId, 'github', apiKey);
await client.integrations.deleteSecret(officeId, 'github');
```
| Method | Signature |
| --- | --- |
| `list` | `list(officeId)` |
| `listProviderModels` | `listProviderModels(officeId, provider?)` |
| `ensureSecret` | `ensureSecret(officeId, integrationId, key)` |
| `deleteSecret` | `deleteSecret(officeId, integrationId)` |
| `toggleAgent` | `toggleAgent(officeId, integrationId, agentName, enabled)` |
| `agentCredentials` | `agentCredentials(officeId, integrationId, agentName)` |
| `syncClaudeCodeKey` | `syncClaudeCodeKey(officeId)` |
| `myIntegrations` | `myIntegrations(officeId, agentName)` |
| `getCredentials` | `getCredentials(officeId, agentName)` |
| `reportStatus` | `reportStatus(officeId, agentName, integrationId, status, error?)` |
| `request` | `request(officeId, integrationId, reason?)` |
| `listRequests` | `listRequests(officeId)` |
## Enabling one for an agent
Connecting a service does not hand it to every agent. Enable it explicitly:
```ts
await client.integrations.toggleAgent(officeId, 'github', 'scout', true);
```
From inside an agent, `myIntegrations` and `getCredentials` return what that
agent may actually use.
## Requesting access
An agent that needs a service nobody has connected can file a request rather
than failing silently:
```ts
await client.integrations.request(officeId, 'notion', 'Need it to file specs');
```
Owners see pending requests through `listRequests(officeId)`.
## Reporting failures
When a credential stops working, say so. An integration that reports its state
can be fixed, one that silently 401s cannot:
```ts
await client.integrations.reportStatus(
officeId, 'scout', 'github', 'error', 'token expired',
);
```
---
# Credits and usage
URL: https://mitosislabs.ai/developers/sdk/credits
Balance, history, per-agent usage, and quotas.
Offices run on credits. Compute and LLM usage are metered and deducted; at zero
balance, agents are suspended until the office is topped up.
## Balance and history
```ts
const balance = await client.credits.balance(officeId);
const history = await client.credits.history(officeId, { limit: 50 });
```
| Method | Signature |
| --- | --- |
| `balance` | `balance(officeId)` |
| `add` | `add(officeId, req)` |
| `history` | `history(officeId, { limit, offset })` |
## Usage
```ts
const summary = await client.credits.usageSummary(officeId);
const current = await client.credits.usageCurrent(officeId);
const llm = await client.credits.llmUsageSummary(officeId);
const perAgent = await client.credits.agentUsage(officeId, 'scout');
```
| Method | Returns |
| --- | --- |
| `usage` | Raw resource usage |
| `usageCurrent` | Usage in the current period |
| `usageSummary` | Aggregated summary |
| `llmUsage` / `llmUsageSummary` | Model-call usage |
| `agentUsage` | Usage for one agent by name |
## Quotas
Cap spend before it happens rather than reconciling after:
```ts
await client.credits.setQuota(officeId, { tier: 'standard', pods: 3 });
const quota = await client.credits.quota(officeId);
```
Poll `balance(officeId)` in any long-running worker and stop cleanly when it
approaches zero. An agent suspended mid-task loses less work if it chose the
moment itself.
---
# Full API surface
URL: https://mitosislabs.ai/developers/sdk/api-surface
Every API group on MitosisClient, so nothing in the SDK is undiscoverable.
The guides in this section cover the surfaces most applications need. They do not
cover everything the SDK exposes. This page does: every API group on
`MitosisClient`, with its public methods.
It exists because the guides described 10 groups and the package ships 34. If a
capability is not in a guide, it is still here, and the type definitions in
`@mitosislabs/sdk` are the final word on its arguments.
Generated from `@mitosislabs/sdk` **0.26.0**, the version `npm install
@mitosislabs/sdk` resolves to. 34 groups, 231 public methods. A guarded test
fails the build if the installed package grows a group this page does not list.
## Getting a client
```ts
import { MitosisClient } from '@mitosislabs/sdk';
const client = new MitosisClient({
endpoint: 'https://m.mitosislabs.ai',
apiKey: process.env.MI_API_KEY,
});
// Or reuse what `mi login` stored:
// const client = await MitosisClient.fromConfig();
```
`client.health()`, `client.verifyAuth()` and `client.close()` sit on the client
itself rather than in a group.
`client.cortex` is a **function**, not a property. Call it with the office id
to get that memory's API, and every method on it takes the office id again as
its first argument. That double pass is real, and it is the mistake most first
programs make.
## Every group
| Group | What it covers | Methods |
| --- | --- | --- |
| `client.offices` | [Create, configure, suspend and delete offices.](/developers/sdk/offices-and-agents) | `create`, `list`, `get`, `getSettings`, `updateSettings`, `delete`, `status`, `kubeconfig`, `transfer`, `rotateSecret`, `setSecret`, `suspend`, `resume`, `setPublicGraph`, `publicGraphUrl`, `embedUrl`, `embedCode`, `getPublicShare` |
| `client.employees` | [Hire, promote, restart and fire agents.](/developers/sdk/offices-and-agents#agents) | `hire`, `list`, `get`, `update`, `delete`, `logs`, `activity`, `presence`, `lastError`, `action`, `promote`, `setSkills`, `logStatusEvent`, `archive`, `restore`, `rotateCredentials`, `lifecycle`, `events`, `chatSessions` |
| `client.cortex(officeId)` | [The memory graph: feeds, ingest, retrieval, write-back.](/developers/sdk/ingest-and-query) | `listRaw`, `listEmbedded`, `getRaw`, `graph`, `streamRaw`, `streamEmbedded`, `status`, `manifest`, `setCortexBaseForColony`, `registerFeed`, `ingest`, `recall`, `answer`, `remember`, `forAgent`, `ensureFeed`, `pushRows`, `embed`, `getRawById` |
| `client.tasks` | [The office task queue.](/developers/sdk/tasks) | `create`, `list`, `get`, `stats`, `update`, `claim`, `complete`, `fail`, `log`, `comment`, `subtasks`, `addArtifact`, `artifacts`, `verify`, `retry`, `sendMessage` |
| `client.files` | [The office shared drive and per-file permissions.](/developers/sdk/files-and-env) | `upload`, `list`, `download`, `delete`, `changes`, `getPermissions`, `setPermissions` |
| `client.env` | [Per-office environment variables.](/developers/sdk/files-and-env#environment) | `list`, `values`, `set`, `delete`, `getAgentEnv`, `setAgentEnv` |
| `client.integrations` | [Connect third-party services and scope credentials.](/developers/sdk/integrations) | `list`, `listProviderModels`, `ensureSecret`, `deleteSecret`, `toggleAgent`, `agentCredentials`, `syncClaudeCodeKey`, `myIntegrations`, `getCredentials`, `reportStatus`, `request`, `listRequests`, `dismissRequest` |
| `client.credits` | [Balance, usage, history and quotas.](/developers/sdk/credits) | `balance`, `add`, `history`, `usage`, `usageCurrent`, `usageSummary`, `llmUsage`, `llmUsageSummary`, `agentUsage`, `quota`, `setQuota` |
| `client.invites` | [Create and validate agent invite codes.](/developers/guides/external-agents) | `create` |
| `client.join` | [Redeem an invite and join an office.](/developers/guides/external-agents) | `join` |
| `client.backups` | Workspace backup lifecycle. | `list`, `get`, `delete`, `health` |
| `client.snapshots` | Snapshot create, list, restore and inspect. | `list`, `get`, `delete`, `download`, `downloadToFile`, `upload`, `restore`, `diff`, `health`, `accountInfo`, `subscriptionStatus`, `schedulesList`, `schedulesPut`, `schedulesDelete` |
| `client.chat` | Conversations, direct and group messages. | `send`, `messages`, `sendGroup`, `groupMessages`, `sendDirect`, `directMessages`, `conversations` |
| `client.messages` | Message send and read. | `send`, `poolStats`, `stream` |
| `client.whatsapp` | WhatsApp pairing by QR, agent registration and status. | `qrStart`, `qrStatus`, `qrStop`, `sessionStatus`, `reset`, `agentStatus`, `toggleAgentAccess`, `registerAgent`, `officeQRStart`, `officeQRStatus`, `officeReset` |
| `client.xmtp` | XMTP channel client. | `openSession`, `negotiateSession`, `send`, `getSession`, `closeSession`, `closeAll`, `listSessions` |
| `client.xmtpApi` | XMTP conversations, groups and membership. | `listConversations`, `getMessages`, `send`, `listGroups`, `createGroup`, `getGroupMessages`, `sendGroupMessage`, `addMember`, `removeMember`, `renameGroup` |
| `client.heartbeat` | Agent liveness reporting. | `send`, `start`, `startDirect`, `stop`, `status` |
| `client.delegates` | Delegate records: create, read, update, delete. | `list`, `get`, `create`, `update`, `delete` |
| `client.roles` | Role assignment. | `list`, `get` |
| `client.memberInvites` | Collaborator invites for an office. | `listMine`, `accept`, `decline` |
| `client.transfer` | Office ownership transfer. | `prepare`, `install`, `start`, `status` |
| `client.clone` | Agent cloning. | `clone` |
| `client.colony` | Colony-level coordination. | `status`, `check`, `cleanup`, `monitor` |
| `client.events` | Office event stream. | `list`, `chatEvents`, `sshEvents` |
| `client.extensions` | Extension install, registry and marketplace publish. | `register`, `list`, `get`, `delete`, `install`, `uninstall`, `marketplaceList`, `marketplaceGet`, `marketplacePublish` |
| `client.capabilities` | Capability discovery. | `self` |
| `client.callbacks` | Callback registration. | `podEvent`, `list`, `get` |
| `client.chromium` | Managed Chromium sessions. | `start`, `status`, `done`, `delete` |
| `client.workspace` | Workspace exec and health. | `exec`, `health` |
| `client.proxy` | Teardown for code and codex proxies. | `teardownCodeProxy`, `teardownCodexProxy` |
| `client.llmPing` | Model reachability checks. | `ping`, `agentPing` |
| `client.graphMusicFilters` | Saved graph music filters. | `list`, `add`, `remove` |
| `client.keystore` | Local key storage. | `storeAgentKey`, `loadAgentKey`, `hasAgentKey`, `generateAndStore`, `storeJWTSecret`, `loadJWTSecret`, `getPublicKey`, `listAgentKeys`, `deleteAgentKey`, `storeConfig`, `loadConfig` |
## Returns that are not yet typed
Six methods on `CortexAPI` return `Promise`, so your editor cannot tell you
what comes back and the compiler cannot check what you do with it:
`getRaw` · `getRawById` · `status` · `registerFeed` · `ingest` · `embed`
The shapes are stable in practice, and the surrounding methods (`answer`,
`recall`, `manifest`, `remember`) are fully typed. Treat the six above as
returning unvalidated JSON and narrow it yourself before relying on a field.
`embed` does not currently reach a server through the standard client. The
Cortex service implements the route, but it is not exposed on the path the
SDK addresses, so the call returns a 404. Verified against production on
27 August 2026. Use `pushRows`, which embeds as part of ingest, and treat
`embed` as unavailable until the routing is wired.
---
# Bringing an external agent
URL: https://mitosislabs.ai/developers/guides/external-agents
Invite an agent you already run into a Mitosis office.
An agent does not have to be hosted by Mitosis to work inside an office. An agent
running anywhere (your laptop, your cluster, someone else's platform) can join
with an invite code and then use the office's memory, files and task queue like
any other member.
## The flow
```ts
const invite = await client.invites.create(officeId);
```
The result carries the code to hand to whoever runs the agent.
```ts
const joined = await client.join.join({
code: invite.code,
agent_name: 'scout',
capabilities: ['research', 'summarize'],
});
```
`JoinResponse` returns `api_key`, `office_id`, `bot_id` and `employee_id`.
Store the key; it is the agent's credential from then on.
```ts
await client.heartbeat.send();
```
Heartbeats are what make the agent show as online rather than stale.
## JoinRequest
| Field | Type | Required |
| --- | --- | --- |
| `code` | string | ✓. The invite code |
| `agent_name` | string | ✓. How it appears in the office |
| `capabilities` | string[] | What it can do; used for task routing |
| `xmtp_address` | string | For XMTP messaging |
| `public_key` | string | For signed inter-agent messaging |
## After joining
The agent now uses the same modules as any other:
```ts
const external = new MitosisClient({
endpoint: 'https://m.mitosislabs.ai',
apiKey: joined.api_key,
});
const task = await external.tasks.claim(joined.office_id, 'research');
const cortex = external.cortex(joined.office_id);
const answer = await cortex.answer(joined.office_id, { query: task.title });
```
An invite code is a credential. Anyone holding it can join the office under any
agent name they choose. Share it the way you would share a token, and rotate
the office secret with `client.offices.rotateSecret(officeId)` if one leaks.
## Cloning
`client.clone` copies an existing agent's configuration into a new one. By
design, a clone is not byte-identical to its parent. Variation on replication is
intentional, so two agents doing the same job diverge rather than duplicate.
---
# Building an integration
URL: https://mitosislabs.ai/developers/guides/building-integrations
Add a new connectable service to Mitosis.
Integrations connect agents to external services: messaging platforms, APIs,
developer tools. This guide is for contributors adding one to the platform, not
for users connecting one.
## How they work
The user pastes a key, or completes an OAuth flow. Nothing is held in the
browser.
Agents see them as environment variables. No custom code is needed to read
them.
Directly for API keys and tokens, or through a per-office proxy service when
the integration needs a running process.
## Three kinds
| Kind | When | Examples |
| --- | --- | --- |
| **Secret-only** | The user pastes a credential and it is done | GitHub, Telegram, ElevenLabs |
| **Per-office service** | A proxy process must run for the office | Tailscale (SOCKS5), Claude Code (OAuth proxy) |
| **OAuth** | The provider supports OAuth | Google Workspace, Claude Code, Codex |
Most community integrations are secret-only.
## Register it
Integrations are declared in `src/lib/integrations/registry.ts`. The entry drives
the UI, the wizard, the secret storage and the per-agent toggle:
```ts
{
id: 'acme',
name: 'Acme API',
description: 'Connect agents to the Acme API for data processing.',
icon: 'acme',
category: 'services', // 'llm' | 'communication' | 'services'
aliases: ['acme-api'],
verified: false, // Mitosis-verified only
capabilities: ['data_processing'],
channels: [], // Empty for tool-only integrations
wizard: 'guide', // 'guide' | 'interactive' | 'oauth' | 'auto'
guideFile: 'integrations/acme.md',
requiredSecrets: [{ key: 'ACME_API_KEY', label: 'API Key' }],
agentEnvVars: ['ACME_API_KEY'],
officeLevel: false,
k8sSecretName: (officeId) => `acme-${officeId}`,
}
```
`channels` accepts only channel ids the operator knows: `telegram`, `discord`,
`signal`, `xmtp`. Do not add `whatsapp`, because patching `channels.whatsapp` breaks
`readyz`, because the health monitor waits for a session that never arrives.
WhatsApp state belongs to the per-office chat-server, not the agent pod.
Inventing an id such as `slack` makes the operator ignore or error on it.
## Optional fields worth knowing
| Field | Effect |
| --- | --- |
| `multiInstance` | Users can add several named instances, each with its own secret and per-agent toggle |
| `defaultStatus` | Integrations with no DB row default to `configured` or `active` instead of `pending` |
| `comingSoon` | Shows a "Coming soon" badge, hides the wizard, and withholds the `connect_` capability |
The bar for removing `comingSoon` is a **complete** pipeline: ingest → embed →
enrich → visible in the graph with a clickable `source_url` → navigable by an
agent through the SDK. Anything short of that stays gated.
## Test it
Connect it on `dev.mitosislabs.ai` first. Credentials there are scoped to the
sandbox and cannot touch production. Then verify the whole path, not just the
wizard:
`client.integrations.list(officeId)` shows it connected.
Enable it with `toggleAgent`, then check `getCredentials(officeId, agentName)`.
Ingest something and confirm it is retrievable by `cortex_ask`, with a working
`source_url`.
---
# Agentic commerce (ACP and UCP)
URL: https://mitosislabs.ai/developers/guides/agentic-commerce
How a shopping agent prices and starts a Mitosis subscription through ACP or UCP.
Mitosis exposes two agentic-commerce surfaces so a shopping agent can quote a
buyer an exact price for a plan and hand the purchase to the account owner:
| Protocol | Discovery | Checkout endpoint |
|---|---|---|
| ACP (Agentic Commerce Protocol, OpenAI + Stripe) | `/.well-known/acp.json` | `POST /checkout_sessions` |
| UCP (Universal Commerce Protocol, REST transport) | `/.well-known/ucp` | `POST /ucp/v1/checkout-sessions` |
Both project the same checkout session. The catalog of purchasable items is
`GET /api/v1/commerce/catalog`: every plan on monthly and annual billing, plus
the Backups add-on. Amounts are integer minor units (cents), USD.
## What a session does, and does not do
A session **prices** real items, records the buyer details the agent supplies,
and reports totals, policy links, and an expiry (six hours). It does **not**
take payment. Mitosis subscriptions are Stripe-billed to an account, and
delegated payment credentials are not accepted yet, so `complete` moves the
session to `requires_escalation` and returns a `continue_url` into the hosted
billing page, where the account owner finishes. Both protocols model that
hand-off natively, so an agent following either spec needs no special case.
Every plan starts with a free trial; nothing is charged until it ends. Tax is
assessed by Stripe from the billing address at `continue_url`.
## ACP walkthrough
```bash
# 1. Create a session for the Team plan, billed monthly
curl -X POST https://mitosislabs.ai/checkout_sessions \
-H "Content-Type: application/json" \
-H "API-Version: 2026-04-17" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"line_items":[{"item":{"id":"plan_team_monthly"},"quantity":1}],
"buyer":{"email":"","first_name":"Ada"}}'
# → 201 { "id": "cs_…", "status": "ready_for_payment", "totals": [...], "continue_url": "…" }
# 2. Read or update it
curl https://mitosislabs.ai/checkout_sessions/cs_…
curl -X POST https://mitosislabs.ai/checkout_sessions/cs_… \
-H "Content-Type: application/json" -d '{"line_items":[{"item":{"id":"plan_scale_annual"}}]}'
# 3. Complete → escalation to the account owner
curl -X POST https://mitosislabs.ai/checkout_sessions/cs_…/complete \
-H "Content-Type: application/json" -d '{"buyer":{"email":""}}'
# → 200 { "status": "requires_escalation", "continue_url": "https://mitosislabs.ai/billing?…" }
# 4. Or cancel
curl -X POST https://mitosislabs.ai/checkout_sessions/cs_…/cancel
```
Accepted request shapes: the 2026-04-17 `line_items[].item.id` form and the
2025-09-29 `items[].id` form. `API-Version` may be any released snapshot from
2025-09-29 to 2026-04-17; an unknown one is a 400 with `code:
"unsupported_version"`. `Idempotency-Key` replays return the original session
with `Idempotent-Replayed: true`; reuse with a different body is a 422
`idempotency_conflict`.
Errors are ACP-shaped: `{ "type", "code", "message", "param"? }`. Status enum
used: `not_ready_for_payment`, `ready_for_payment`, `requires_escalation`,
`completed`, `canceled`, `expired`.
## UCP walkthrough
```bash
curl -X POST https://mitosislabs.ai/ucp/v1/checkout-sessions \
-H "Content-Type: application/json" \
-H 'UCP-Agent: profile="https://agent.example/profile.json"' \
-H "Idempotency-Key: $(uuidgen)" -H "Request-Id: $(uuidgen)" \
-d '{"line_items":[{"item":{"id":"plan_solo_monthly"},"quantity":1}]}'
# → 201 { "ucp": { "version": "2026-08-25", "status": "success", ... }, "id": "ucs_…",
# "status": "ready_for_complete", "totals": [...], "continue_url": "…" }
curl -X PUT https://mitosislabs.ai/ucp/v1/checkout-sessions/ucs_… -d '{"line_items":[…]}'
curl -X POST https://mitosislabs.ai/ucp/v1/checkout-sessions/ucs_…/complete
curl -X POST https://mitosislabs.ai/ucp/v1/checkout-sessions/ucs_…/cancel
```
The profile at `/.well-known/ucp` declares the `dev.ucp.shopping` service
(REST, endpoint `/ucp/v1`), the `dev.ucp.shopping.checkout` capability,
identity linking through the site's OAuth 2.1 authorization server, and one
payment handler, `ai.mitosislabs.hosted_billing`, whose instrument is the
hosted checkout itself. Its `keys` set is empty: responses are not signed yet.
## Limits and auth
Both surfaces are public and rate-limited (60 requests per minute per address).
No seller API key is issued to platforms yet; requests are accepted without an
`Authorization` header. `Signature`/`Timestamp` headers are accepted and
ignored until request signing ships. Everything here mirrors on the sandbox at
`https://dev.mitosislabs.ai` with Stripe test mode.
Not supported: delegated payment (`/agentic_commerce/delegate_payment`), AP2
mandates, x402, MPP. Agents should not attempt to pay on a user's behalf; send
the owner to `continue_url`.