# Mitosis developer documentation, in full Source: https://mitosislabs.ai/developers Every page of the developer documentation, concatenated. Index: https://mitosislabs.ai/developers/llms.txt --- # Mitosis for developers URL: https://mitosislabs.ai/developers The four developer surfaces: MCP server, REST API, CLI, and TypeScript SDK. **Cortex** is a knowledge graph that belongs to one team. It takes what a team has already written (mail, calendar, documents, contacts, chat history, notes), does the expensive semantic work once while ingesting it, and then serves it to agents as scored, typed, cited evidence. Two properties are worth knowing before you write any code: - The work happens **at ingest**. Embedding and extraction are paid once per item, no matter how many agents read it later. - There is **no LLM at query time**. A query runs vector kNN, full-text and a one-hop graph expansion, fused with reciprocal rank fusion. You get evidence; your agent does the reasoning. [What Cortex is](/developers/concepts/what-is-cortex) is the five-minute version of the whole model. ## Sixty seconds Put something into a cortex, then ask it back. Same loop in three surfaces: ```ts TypeScript SDK 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 cortex = client.cortex(office.id); // Ingest const { feedKey } = await cortex.ensureFeed(office.id, 'handbook'); await cortex.pushRows(office.id, feedKey, [{ external_id: 'handbook:pricing', title: 'Pricing policy', content: 'Solo moved to $19.99 per seat in July 2026.', }], { deferEmbed: true }); // Query const answer = await cortex.answer(office.id, { query: 'what did pricing move to?' }); console.log(answer.results[0].preview, answer.results[0].universal_id); ``` ```bash CLI npx -y -p @mitosislabs/sdk@latest mi login OFFICE=$(mi offices list | jq -r '.[0].id') mi cortex ingest ./handbook/pricing.md --office "$OFFICE" --feed handbook mi cortex ask "what did pricing move to?" --office "$OFFICE" ``` ```bash MCP claude mcp add --transport http mitosis https://mitosislabs.ai/api/mcp # then, from the agent: # cortex_ask({ question: "what did pricing move to?" }) # cortex_remember({ text: "Solo is $19.99/seat as of July 2026." }) ``` The full worked loop, with batching, local files, provenance on write-back and the plain-HTTP equivalents, is in [ingest and query](/developers/sdk/ingest-and-query). ## The four ways in The graph is one thing. These are four ways to reach it, on the same data and the same auth. Pick whichever fits where your code runs. Streamable HTTP at `/api/mcp`. The route most agents take. Claude, ChatGPT, Cursor and Grok connect to it directly, with OAuth 2.1 and open client registration. Nine public operations under `/api/v1`. Read endpoints need no credential. RFC 9457 `problem+json` errors, rate-limit headers, `Idempotency-Key` support. `mi` ships inside the SDK package. The fastest way to query a memory from a terminal or a shell script. `@mitosislabs/sdk`. Full control over offices, agents, tasks, files, integrations and credits. ## What lives in a cortex A cortex (an *office*, in the API, and a *memory* in the docs) is one graph belonging to one person or team. Connecting a source (Gmail, Calendar and Drive through Google Workspace, plus WhatsApp, GitHub, Notion and Obsidian) ingests its contents, embeds them, and links them into the graph. Agents then read the graph through any of the four surfaces above, and write back to it with [`cortex_remember`](/developers/memory/writing). Retrieval returns **nearest matches, not a thresholded set**. A question whose answer lives in a source the user never connected comes back with the closest thing in the graph rather than with nothing, so responses carry explicit [source-gap signals](/developers/memory/asking#source-gaps) that tell you when the answer is missing rather than merely poor. Each office is its own namespace, database and pods. Data does not flow between cortexes; see [cortexes and access](/developers/concepts/cortexes-and-access). ## Concepts worth reading first Identity, relationships, provenance and ranking: the four things a folder of notes cannot carry. How a flat row becomes a node, and why canonical normalization is the whole trick. The derivation ladder, re-enrichment on rule change, and the consolidation cycle. Why paying for inference once at ingest beats paying for it on every query. ## Start here The [quickstart](/developers/quickstart) gets an MCP client answering questions from a real graph in about five minutes. Public endpoints need none. Everything touching a memory does. See [authentication](/developers/authentication). [`dev.mitosislabs.ai`](https://dev.mitosislabs.ai/api/v1) mirrors every production surface with disposable data and no billing side effects. ## Machine-readable surfaces Agents that discover services on their own can start from any of these: | Document | What it carries | | --- | --- | | [`/openapi.json`](https://mitosislabs.ai/openapi.json) | OpenAPI 3.1 spec for the public API | | [`/.well-known/mcp/server-card.json`](https://mitosislabs.ai/.well-known/mcp/server-card.json) | MCP server card (SEP-1649), mirrors `tools/list` | | [`/auth.md`](https://mitosislabs.ai/auth.md) | How an agent registers and authenticates | | [`/llms.txt`](https://mitosislabs.ai/llms.txt) | Service summary for language models | | [`/.well-known/agent-skills/index.json`](https://mitosislabs.ai/.well-known/agent-skills/index.json) | Installable agent skills | | [`/.well-known/agent-card.json`](https://mitosislabs.ai/.well-known/agent-card.json) | A2A agent card | --- # Quickstart URL: https://mitosislabs.ai/developers/quickstart Connect an agent to a memory graph and get an answer out of it. Two paths. The MCP one is what most agents use; the HTTP one proves the API is reachable without any client at all. ## Talk to the public API first Nothing here needs a credential, so it is a clean way to confirm you can reach Mitosis: ```bash curl -s https://mitosislabs.ai/api/v1 | jq ``` The index self-identifies its environment, which matters once you start pointing the same code at `dev.mitosislabs.ai`: ```json { "environment": "production", "endpoints": { "...": "..." } } ``` ## Connect an MCP client The product MCP server is Streamable HTTP at `https://mitosislabs.ai/api/mcp`. It supports OAuth 2.1 with dynamic client registration, so a client that speaks MCP auth needs no pre-provisioned key. The user approves access in a browser. ```bash Claude Code claude mcp add --transport http mitosis https://mitosislabs.ai/api/mcp ``` ```json Cursor / mcp.json { "mcpServers": { "mitosis": { "url": "https://mitosislabs.ai/api/mcp" } } } ``` ```bash CLI npx -y -p @mitosislabs/sdk@latest mi login ``` The first tool call that touches a memory triggers the OAuth flow. Public tools (`get_pricing`, `get_platform_status`, `search_docs`, `list_skills`) answer before you sign in at all. ## Ask the graph a question Once connected, the memory tools appear. The one to reach for first is `cortex_ask`. It returns ranked evidence from the graph, with a citation on every item: ```text cortex_ask({ question: "What did we decide about pricing last quarter?" }) ``` `cortex_ask` ranks by relevance, not recency. The newest matching thing is not necessarily first. To bound a search by time, pass `since` / `until` (RFC 3339) to `cortex_ask`; `cortex_recall` takes only `query` and `limit`. Nothing composes prose for you: `results[]` comes back with a `score`, a `preview` and a `universal_id` per item, and `cited_graph_url` deep-links to those exact nodes in the user's own graph. See [reading the response](/developers/memory/asking#reading-the-response). ## Then write something back Memory is not read-only. An agent that learns a durable fact should store it: ```text cortex_remember({ text: "Prefers async design review over live meetings." }) ``` That fact is embedded and linked into the same graph, so a later `cortex_ask` from any client can retrieve it. ## Where to go next Every MCP tool, its arguments, and the scope it needs. API keys, device-code login, and the OAuth 2.1 flow. How to tell "no answer" from "the source was never connected". When you need offices, agents, tasks and files rather than just memory. --- # Authentication URL: https://mitosislabs.ai/developers/authentication Which surfaces need a credential, and the three ways to get one. Mitosis has a deliberately small auth surface: public read endpoints take no credential at all, and everything that touches a person's memory takes a bearer token or an OAuth grant. ## What needs a credential | Surface | Credential | | --- | --- | | `/api/v1` read endpoints | None | | MCP public tools (`get_pricing`, `get_platform_status`, `search_docs`, `list_skills`) | None | | MCP memory tools (`cortex_*`) | OAuth 2.1 grant, scope `memory:read` or `memory:write` | | SDK / CLI office operations | API key or device-code session | | Backups API | Bearer token, scopes `backups:read` / `backups:write` / `backups:admin` | ## OAuth 2.1 for MCP clients The MCP server supports **dynamic client registration**, so an MCP client does not need a pre-registered `client_id`. Point it at the server and it discovers everything it needs: ```bash curl -s https://mitosislabs.ai/.well-known/oauth-authorization-server | jq curl -s https://mitosislabs.ai/.well-known/oauth-protected-resource | jq ``` The first is RFC 8414 metadata (and carries an `agent_auth` block describing agent registration); the second is RFC 9728 protected-resource metadata. The user approves access in a browser, and the client receives a token scoped to their memory. Memory tools declare the scope they need. Reads (`cortex_ask`, `cortex_recall`, `cortex_manifest`, `cortex_status`, `cortex_connectable_sources`, `cortex_connect_link`) need `memory:read`. Writes (`cortex_remember`, `cortex_ingest`, `cortex_ingest_conversation`) need `memory:write`. ## Device-code login (CLI) For humans at a terminal, the CLI runs a device-code flow: it prints a URL, you approve in the browser, and the session is stored locally. ```bash npm install -g @mitosislabs/sdk mi login mi whoami ``` ## API keys (scripts and servers) Create a key at the top of the [signed-in docs](https://mitosislabs.ai/dashboard/developers), or with **Copy API Key** in the dashboard user menu (keys are prefixed `mi_`). It is shown once. Pass it as an environment variable. The SDK and CLI both read `MI_API_KEY`: ```bash export MI_API_KEY="mi_..." mi offices list ``` In code, `endpoint` is required alongside the key: ```ts import { MitosisClient } from '@mitosislabs/sdk'; const client = new MitosisClient({ endpoint: 'https://m.mitosislabs.ai', apiKey: process.env.MI_API_KEY, }); ``` Or let the SDK read whatever `mi login` already stored in `~/.os1/config.json`: ```ts const client = await MitosisClient.fromConfig(); ``` An API key takes precedence over a stored JWT in `fromConfig()`. A leftover local dev JWT in `~/.os1/keys/jwt.key` is ignored whenever a key is present. ## Bearer tokens over HTTP `header` is the only bearer method the protected-resource metadata advertises, so tokens in query strings are not accepted: ```http GET https://mitosislabs.ai/api/snapshots Authorization: Bearer ``` The authoritative scope list is `scopes_supported` in the authorization-server metadata. Treat this page as a summary and that document as the source of truth. ## Machine-readable version [`/auth.md`](https://mitosislabs.ai/auth.md) carries the same instructions in the [workos/auth.md](https://github.com/workos/auth.md) convention, for agents that discover and register on their own. --- # What Cortex is URL: https://mitosislabs.ai/developers/concepts/what-is-cortex One graph per team, built once at ingest, read by any agent. Cortex is a knowledge graph that belongs to one team. You connect sources to it, it does the expensive work once while it ingests them, and after that any agent with access to that team can read it. In the API the graph is an *office*, identified by an `officeId`. "Memory", "cortex" and "office" name the same object, and you will meet all three words in SDK signatures and in the dashboard. One office is one graph.
## The work happens at ingest A row arrives through a feed. Cortex embeds it, extracts typed entities and edges from it, and writes those into the graph with a record of how each one was derived. That is the expensive part, and it is paid once per item, no matter how many agents read the item later or how often. ```ts const cortex = client.cortex(officeId); const { feedKey } = await cortex.ensureFeed(officeId, 'handbook'); await cortex.pushRows(officeId, feedKey, rows, { deferEmbed: true }); ``` `ensureFeed` is idempotent, and `external_id` on each row is the idempotency key, so re-pushing a row updates it in place. The full runnable loop, with batching, local files, the CLI and the plain-HTTP equivalents, is in [ingest and query](/developers/sdk/ingest-and-query). ## What a query returns `answer()` is hybrid retrieval in one call: vector kNN, full-text, and a one-hop graph expansion, fused with reciprocal rank fusion. ```ts const answer = await cortex.answer(officeId, { query: 'what did pricing move to?', limit: 5, }); ``` **There is no LLM at query time.** You get evidence, and your agent does the reasoning over it. Each result carries a `score`, a `universal_id`, its `source_table` and a `source_url`, plus `signals` describing how the result was found and per-source `freshness` telling you how stale it is. Retrieval returns nearest matches, not a thresholded set, which is the one thing to understand before you build on it. See [nearest matches, not a thresholded set](/developers/memory/overview#nearest-matches-not-a-thresholded-set). ## Provenance is the spine Every item carries a **universal id**: ```text universal_id = integration_id ":" source_table ":" sha256(natural_key) ``` That id is the same across the raw layer, the embedded layer and the graph layer, so a citation resolves back to the exact row it came from. Answers carry their citations as universal ids plus a `cited_graph_url` that opens the graph with those nodes highlighted. ## Reading it from an agent `forAgent` is the surface to use inside an agent loop. It attributes writes to the agent, and carries provenance from the last `ask()` into the next `remember()`. ```ts const memory = client.cortex(officeId).forAgent(officeId, 'atlas'); const evidence = await memory.ask('what did pricing move to?'); console.log(evidence.context()); await memory.remember('Solo is $19.99/seat as of July 2026.', { kind: 'decision' }); ``` `ask()`, `context()`, `contextFor()` and `remember()` are the whole agent surface. ## What has been measured Verging Labs evaluated Cortex between 23 and 25 July 2026, building and probing it entirely through the public APIs. Across 272 scored questions it recorded 0 fabricated answers and 0 flat-wrong answers, and 97 percent correct "I do not know" responses on facts that were never stored. ## Where to go next What a folder of notes or a vector dump cannot do, and what a graph does instead. The derivation ladder, re-enrichment on rule change, and the consolidation cycle. Many cortexes, who can read one, and where the isolation boundary sits. The complete runnable loop against `@mitosislabs/sdk`. --- # Why not files URL: https://mitosislabs.ai/developers/concepts/why-not-files Where notes and vector dumps stop working, and what a graph does instead. Most teams keep what they know in a folder of notes, or in a vector store of chunked documents. Both work. Both fail on the same four things, and they fail for the same reason: the text is stored, but nothing about the text is stored. ## The four things text does not carry | | A file or a flat vector dump | A cortex | | --- | --- | --- | | **Identity** | "Dana Ito", the mailbox "dana.ito" and "D. Ito" are three unrelated strings. | Canonical normalization collapses them into one node, so a fact about any spelling is a fact about the person. | | **Relationships** | Proximity only. Two facts are related if they happen to sit near each other in a document, or near each other in embedding space. | Typed edges. A row is `from` a person, `owned_by` an organization, `attended_by` a set of people, because a rule said so. | | **Provenance** | A chunk, sometimes with a filename. | A `universal_id` that resolves to the exact source row, in every layer, plus a graph link that highlights it. | | **Ranking** | One similarity score, which says how close the text is, never how trustworthy the claim is. | Every membership and edge is tagged with how it was derived, from a declared field down to a probabilistic extraction, so you can rank by kind of evidence. | Add more documents and none of those four improve. Identity gets worse: more spellings, more duplicates, more near-identical chunks competing for the same slots in a result set. Search over a pile of text returns more text. It does not return an answer. There is no size at which this flips over. The failure is structural, not a threshold, and we do not publish one. A two-file vault has the same missing identity and the same missing provenance as a two-thousand-file vault; it just has fewer chances to show you. ## What a graph gives you instead Extraction rules normalize a value before it becomes a node: emails, phone numbers and URLs each have their own normalization. Two spellings of the same person land on one node. See [typed entities](/developers/concepts/typed-entities). Rules also connect rows that share a common value at the same path, which is the mechanism that turns flat rows into a graph rather than a list. Every edge records how it was produced. A declared field outranks an exact lexical match, which outranks a geometric similarity, which outranks an LLM extraction. See [the derivation ladder](/developers/concepts/enrichment#every-fact-records-how-it-was-derived). Every result carries universal ids and a graph link. A claim you cannot trace is a claim you can drop. ## Retrieval, not search The practical difference shows up in what your model receives. A vector dump hands it the nearest chunks of text and leaves the reading to the model. A cortex hands it a small set of scored, typed, cited results, so the model's context carries answers rather than a haystack to search. That is also why the same setup helps a small or local model rather than only a large one. See [context and inference cost](/developers/concepts/cost). ## What files are still good for Nothing here says stop keeping files. Files are the source. A vault, a Drive folder and an export are all normal inputs, and a row can point at a file on the office drive through `file_path` while its text lives in the graph. The argument is about what you query, not about where the original lives. Push your own files into a cortex and query them back with citations. What a user can connect, and how to hand them a link that does it. --- # How Cortex re-fits data URL: https://mitosislabs.ai/developers/concepts/enrichment The derivation ladder, re-enrichment on rule change, and the consolidation cycle. Data does not arrive finished. Rules get written after the rows they should have applied to. Entities that mattered in March stop being mentioned in August. This page is about the two mechanisms that keep an already-ingested graph fitted to what you know now. ## The spine A source row is persisted, embedded, enriched into entities and edges, written into the graph, and served by retrieval. Each stage reads the stage before it, and the enrich stage is the one that can be run again later. ## Enrichers are pure functions An enricher takes a row and returns an `EnrichmentResult`. It writes nothing. A single **Writer** owns every database write and every conflict rule, which is what makes the following properties hold rather than mostly hold. All three are enforced by tests. | Property | What it means for you | | --- | --- | | **Idempotent** | Running enrichment on the same row again produces the same graph. Re-runs are safe, and they are the normal case, not an exception. | | **Atomic per row** | A row's enrichment commits as a unit. If the commit happened, everything for that row landed. There is no half-enriched row to detect. | | **Cite-able** | `edge_sources` and `node_sources` accumulate evidence across re-runs, so an edge asserted by three separate rows records all three. | One enricher failing is logged and counted. It never aborts the row, and it never takes the other enrichers down with it. ## Every fact records how it was derived Every membership and every edge is tagged with the mechanism that produced it. This is the part that lets an agent rank one claim above another, and it is the whole point of the design: a declared field and a guess are both in the graph, but they are never indistinguishable. The seven rungs, in descending order of trust: | Rung | Kind of evidence | | --- | --- | | `structural` | A declared field. The row said so. Gold. | | `lexical_canonical` | An exact match after normalization. | | `lexical_fuzzy` | An approximate string match. | | `embedding_similarity` | Geometric closeness in embedding space. | | `cluster_centroid` | Membership derived from a cluster rather than a pair. | | `temporal_adjacency` | Things that happened close together in time. | | `llm_extraction` | A model read the text and asserted it. Probabilistic. | Structural facts are gold. Similarity is geometric. LLM extraction is probabilistic. Treat them differently, because the graph already does. When an answer surprises you, the derivation kind is usually the explanation. A confident-looking edge sitting on `embedding_similarity` is telling you the two things are near each other, not that one is a fact about the other. ## Re-enrichment on rule change A feed's extraction rules can change. When they do, the feed's `rulesVersion` is bumped, and a backlog drainer re-runs rows that were **already embedded** against the new rules. The practical consequence: old data gets the benefit of a rule written later. You do not have to have known, at ingest time, everything you would eventually want to extract. Nothing is re-fetched from the source and nothing is re-embedded; the enrich stage runs again over rows that are already there. ## The consolidation cycle Separately, a maintenance pass runs over the graph. Think of it as sleep-time work rather than request-time work. It does three things: `salience = evidence x recency x confidence`. An entity that keeps being reaffirmed by new rows stays high; one that stopped being mentioned decays. The same quality gate that runs at extraction time is applied again, so entities that should never have been promoted get caught on a later pass. An entity is invalidated only when it is **both** below a salience floor **and** unreaffirmed past a TTL. Both conditions are required. This is deliberately conservative: a quiet but well-evidenced entity survives, and so does a weak but recently reaffirmed one. Entities are **invalidated, not deleted**. The graph is append-only: every enrich write is an upsert, and nothing is destroyed. Invalidation changes what retrieval will surface, not what the graph holds. ## Where to go next What extraction rules declare, and how a row becomes a node. The ingest-once, read-many shape this all serves. --- # Typed entities URL: https://mitosislabs.ai/developers/concepts/typed-entities Typed extraction and canonical normalization, and why they make a small model behave. A row is flat. It has a sender, a title, a body, some fields. A graph needs *things*: this person, that organization, this channel. Extraction rules are what turn the first into the second. ## What a rule declares An extraction rule points at a field in a row and says what that field is. | Part | What it says | Values | | --- | --- | --- | | `type` | What kind of thing the field names. | `person`, `organization`, `location`, `channel` | | `role` | What the row is *to* that thing. | `from`, `owned_by`, `attended_by` | | `weight` | How confident the rule is, in `[0,1]`. | a number | | `canonical` | How to normalize the value before it becomes a node. | `email`, `phone`, `url`, `lower`, `raw` | A mail row with a `from` address produces a `person` node, with the row related to it in the `from` role, at whatever weight the rule carries. ## Canonical normalization is the whole trick `canonical` is the small part that does the heavy lifting. Two rows can name the same person with different text, and without normalization they become two nodes that never meet. Normalize first, and the mailbox `Dana.Ito` and the mailbox `dana.ito` at the same domain land on one node, so a fact learned from either row is a fact about the same person. This is the difference between a graph and a pile of strings that happen to be similar. See [why not files](/developers/concepts/why-not-files). ## Shared values become edges The second kind of rule works between rows rather than inside one. It connects rows that **share a common value at the same path**: the same thread id, the same document reference, the same channel. That is the mechanism that turns a table of independent rows into a connected graph. Retrieval then uses those edges. `expand_graph` is on by default in [`answer()`](/developers/sdk/ingest-and-query#step-3-query-it-back), and it pulls in one-hop neighbours at a discounted score. ## Which rung a rule lands on A rule reading a declared field produces `structural` evidence, the top rung. A rule matching two normalized values produces `lexical_canonical`. Neither of them involves a model. That matters, because the alternative, asking an LLM to read the text and assert the relationship, sits at the bottom rung as `llm_extraction`, and the graph marks it as such. The full ladder and what each rung means is in [how Cortex re-fits data](/developers/concepts/enrichment#every-fact-records-how-it-was-derived). ## What you can declare today Entity and edge rules ship with the built-in integrations. You cannot author your own today. The public SDK's feed registration accepts only these fields: `feed_key`, `schema`, `table`, `primaryKeyCols`, `embeddableCols`, `cursorCol`, `modality`, `driveRefCol`. Be precise about what that means for a feed of your own. Your rows are embedded, searchable by vector and full text, returned with citations, and they become nodes in the graph. What they do **not** get is entities and edges: a feed registered without rules produces neither, so there is nothing for one-hop expansion to add on top of those rows. A built-in integration's rules apply to that integration's feed, not to yours. `cursorCol` is the field worth your attention here: it drives incremental sync, so each run picks up where the last one stopped rather than re-reading everything, which is what keeps a real database in step. For pushing rows without standing up a feed table of your own, see [ingest and query](/developers/sdk/ingest-and-query#step-2-push-rows). Any extra top-level key on a row is stored verbatim in `raw_data` and stays resolvable. ## Why this makes a small model behave Two reasons, and they are both structural. There is no LLM at query time. Embedding and extraction were paid once at ingest, so the model you run at query time is not the model doing the semantic work. Retrieval returns a small set of scored, typed, cited results rather than a pile of documents. A small model asked to pick between four cited facts behaves far better than the same model asked to read forty pages and find one. [Context and inference cost](/developers/concepts/cost) works through what that means for what you pay and where the model runs. --- # Context and inference cost URL: https://mitosislabs.ai/developers/concepts/cost Why paying for inference once at ingest beats paying for it on every query. Every retrieval system decides where the semantic work happens. Cortex puts it at ingest. Most per-query RAG stacks put it at query time. The consequences of that one choice run through cost, latency, and which models you can actually use. ## Where the work happens | | Per-query stack | Cortex | | --- | --- | --- | | **Embedding** | At ingest. | At ingest. | | **Understanding what an item means** | On every query, by re-reading the retrieved chunks with a model. | Once, at ingest, into typed entities and edges. | | **Cost of the second agent reading the same item** | The same as the first. | Nothing. The item was already understood. | | **Cost of the same agent asking again tomorrow** | The same as today. | Nothing. | | **What the query returns** | Text, for a model to read. | Scored, typed, cited evidence. | Enrichment is paid once per item. It does not scale with how many times the item is read, or with how many agents read it. A per-query stack re-derives meaning on every call, for every agent, forever. That is an architectural argument, and it is the honest form of the claim. We do not publish a dollar figure, because the number depends entirely on your model, your traffic and your corpus. ## There is no LLM at query time `answer()` runs vector kNN, full-text search, and a one-hop graph expansion, fused with reciprocal rank fusion. No model is called, which is why retrieval is measured in milliseconds rather than seconds: the published median answer latency is 477ms. What comes back is evidence, not prose: ```ts const answer = await cortex.answer(officeId, { query, limit: 5 }); for (const r of answer.results) { console.log(r.score, r.title, r.universal_id, r.source_url); } ``` Your agent does the reasoning. That means you choose the model, per call, with full knowledge of how much evidence it has to chew on. ## What your model actually receives The second half of the cost story is context. A pile of retrieved documents consumes tokens in proportion to how much text was retrieved, and most of those tokens are haystack. A small set of scored, typed, cited results consumes tokens in proportion to how many facts you asked for. ```ts const memory = client.cortex(officeId).forAgent(officeId, 'atlas'); const promptBlock = await memory.contextFor('what did pricing move to?', { maxItems: 5 }); ``` `contextFor` renders a citation-formatted block sized by `maxItems`, so the prompt you build has a shape you control rather than one your retrieval happened to produce. Retrieval returns nearest matches, not a thresholded set, so a small `limit` is a budget rather than a quality filter. 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). ## Local embeddings Ingest-time work does not have to leave your infrastructure. An office can be configured to use a local Ollama embedding pool running `mxbai-embed-large`. To be precise about what that moves: it is the **embedding model** that runs locally. It is the component that turns text into vectors during ingest, not a generation model and not something in the query path, because the query path does not call a model at all. ## Where to go next The extraction that gets paid for once, and what it produces. Re-running the ingest-time work when your rules change. --- # Cortexes and access URL: https://mitosislabs.ai/developers/concepts/cortexes-and-access Many cortexes, who can read one, and how sharing actually works. You can have as many cortexes as you have offices. Each one is a separate graph with a separate set of people who can read it. This page is about where that line sits, because a memory product is only as useful as its boundaries are believable. ## One office, one graph, one boundary Each office runs its own namespace, its own database and its own pods. There is no cross-tenant query path. A cortex is an isolation boundary, not a filtered view of a shared store. Every memory call takes the office id explicitly, which is the same boundary showing up in the API: ```ts const cortex = client.cortex(officeId); await cortex.answer(officeId, { query: 'what did pricing move to?' }); ``` ## Adding people People reach a cortex through the office. They are added by membership and by invites, and access is granted at the office level: a member of the office can read that office's memory. There is no per-source or per-node sharing inside a cortex. If two groups of people should not see the same material, that is two cortexes, not one cortex with two audiences. ## Sharing a cortex publicly A cortex can additionally be made publicly readable through a share link. Two things to know about it: Nothing is public until someone turns it on for that office. A share link is a view. It does not grant writes, and it does not make the holder a member of the office. ## Agents Agents live in an office and read its cortex. Their credentials are scoped per integration, so an agent that needs Drive does not thereby get the mailbox. See [integrations](/developers/sdk/integrations) for enabling one for a specific agent, and [authentication](/developers/authentication) for how a credential is issued in the first place. ## There is no cross-cortex query There is no cortex-to-cortex linking, no federation, and no cross-cortex query. Data does not flow between cortexes. If you need results from two cortexes, your code queries each one and combines the results itself. There is no server-side join, and a `universal_id` from one cortex means nothing in another. The practical shape this pushes you towards: one cortex per team whose knowledge genuinely is shared, rather than one giant cortex per company that everybody squints at. ## Where to go next Create an office, hire agents into it, and manage their lifecycle. What a user can connect to a cortex, and how to hand them a link that does it. --- # How memory works URL: https://mitosislabs.ai/developers/memory/overview Sources, the graph, and what retrieval actually returns. A **memory** is one graph belonging to one person or team. In the API it is an *office*, identified by an `officeId`. The two words describe the same object, and you will see both in SDK signatures. ## The shape of it The user authorizes Google Workspace (Gmail, Calendar, Drive), WhatsApp, GitHub, Notion or Obsidian, or uploads an export. Authorization always happens in the dashboard. An agent can produce the link, but never the grant. Items are pulled, chunked, embedded, and written into the graph with their provenance intact. [`cortex_status`](/developers/mcp/tools#cortex-status) reports the backlog while this runs. Retrieval fuses vector, full-text and graph expansion. Facts an agent learns go back in through [`cortex_remember`](/developers/memory/writing), linked to whatever they were derived from. ## Nearest matches, not a thresholded set This is the single most important thing to understand before you build on retrieval. A query always returns the closest things in the graph. It does **not** return nothing when the answer is absent, it returns near-misses. So "the model got a weak answer" and "the source that would answer this was never connected" look identical if you only read the results. Mitosis makes the difference explicit. A response can carry one of three blocks: The memory holds data, but not the kind that could answer this question. The block names the sources that would, says what each one answers, and carries a `cta` with a connect link. Results came back, but they may not be an answer. Treat the response as provisional rather than authoritative. No sources exist yet, so an empty result reflects absent data rather than a failed search. A result carrying **none** of these blocks means the answer is genuinely not in the memory, not merely unretrieved. That is the case where "I don't know" is the correct thing for your agent to say. ## Provenance Every result carries its citations as **universal ids**, on `results[].universal_id`, plus `cited_graph_url`, a deep link that opens the user's own graph with exactly those nodes highlighted. When you write a fact back with `cortex_remember`, pass the universal ids it came from as `source_universal_ids` so the new node inherits provenance edges instead of floating free. ## Which retrieval call to use | Call | Use it when | | --- | --- | | [`cortex_ask`](/developers/memory/asking) | Almost always. Fuses vector, full-text and graph expansion, and returns ranked evidence with citations. | | [`cortex_recall`](/developers/memory/asking#recall) | You specifically want nearest-neighbour matches on meaning, returned as source excerpts. | | [`cortex_manifest`](/developers/mcp/tools#cortex-manifest) | You need counts and source names. An index, never content. | `cortex_manifest` is an index. It reports what sources exist and how many items they hold; it never returns the contents. Do not reach for it to answer a question. --- # Asking a memory URL: https://mitosislabs.ai/developers/memory/asking cortex_ask, cortex_recall, and how to read what comes back. ## cortex_ask The default retrieval call. It fuses vector search, full-text search and graph expansion, then returns the fused result set with provenance attached. No model runs server-side: what comes back is evidence to reason over, not prose. ```json { "question": "What did we decide about pricing last quarter?", "limit": 10, "since": "2026-04-01T00:00:00Z" } ``` | Argument | Type | Notes | | --- | --- | --- | | `question` | string | **Required.** Natural-language question or search query. | | `limit` | number | Max results. Defaults to 10. | | `source_table` | string | Restrict to one source table, e.g. `gmail_messages`. | | `since` | string | RFC 3339 lower bound on item time. | | `until` | string | RFC 3339 upper bound on item time. | Ranking is by **relevance, not recency**. "What's the latest on X?" returns the most relevant items about X, which may not be the newest. When recency is what you actually want, bound it with `since` / `until`. ### Follow-up questions are new questions A result describes only the question it was fetched for. There is no conversational state on the server. A follow-up needs its own `cortex_ask` call with its own question, not a re-read of the previous result. ## cortex_recall Semantic-only vector search, returning source excerpts rather than a composed answer. Reach for it when you want nearest-neighbour matches on meaning and intend to do your own synthesis. ```json { "query": "onboarding friction", "limit": 20 } ``` It takes `query` and `limit`, nothing else. There are no time bounds on `cortex_recall`; use `cortex_ask` when you need them. ## Reading the response `cortex_ask` returns **evidence, not prose**. There is no model on the server side composing an answer: you get a ranked result set with provenance, and your agent does the reasoning over it. An abridged response looks like this: ```json { "query": "What did we decide about pricing last quarter?", "results": [ { "universal_id": "gmail_messages:18f2c9a...", "score": 0.412, "title": "Re: tier pricing", "preview": "Agreed: Solo goes to $19.99/seat from July.", "source_table": "gmail_messages", "source_url": "https://mail.google.com/mail/u/0/#inbox/18f2c9a...", "age_seconds": 3421, "signals": { "vector_rank": 1, "text_rank": 3 } } ], "freshness": [ { "source_id": "google-workspace", "last_success_at": "2026-08-26T18:02:00Z", "status": "ok" } ], "signals": { "vector_candidates": 40, "text_candidates": 22, "graph_added": 6 }, "took_ms": 380, "graph_url": "https://mitosislabs.ai/graph?office=4d38b034-...", "cited_graph_url": "https://mitosislabs.ai/graph?office=4d38b034-...&cite=gmail_messages%3A18f2c9a...", "memory": { "office_id": "4d38b034-...", "office_name": "Mitosis Labs", "graph_url": "https://mitosislabs.ai/graph?office=4d38b034-..." } } ``` Four fields carry most of the operational meaning: The evidence. Each item has a `universal_id` (this is the citation: pass it to `cortex_remember` as `source_universal_ids`), a fused `score`, a `preview`, the `source_table` it came from, and a `source_url` when the original is linkable. There is **no top-level `citations` array**: the ids live here. Per-source sync state. A confident-looking answer drawn from a source that last synced a week ago is a different claim from the same answer drawn from one that synced an hour ago, and only this field tells them apart. Deep link into the user's own graph with the cited nodes highlighted. Good to surface directly to a human who asks "where did that come from?" Absent when the answer cited nothing, because an empty `&cite=` highlights nothing. `graph_url` is the same graph without the citations pre-selected, and is always present. Which memory answered. Note that a connector's *display name* is text the user typed and identifies nothing. This field is the real one. ## The same call from the SDK and the CLI The MCP tools above are one surface over the retrieval engine. From TypeScript the same call is `cortex.answer()`, whose request field is `query` rather than `question`, and whose results carry per-item scores and signals instead of a composed prose answer. ```ts SDK const cortex = client.cortex(officeId); const answer = await cortex.answer(officeId, { query: 'What did we decide about pricing last quarter?', limit: 10, since: '2026-04-01T00:00:00Z', }); for (const r of answer.results) { console.log(r.score.toFixed(3), r.title, r.universal_id); } ``` ```ts SDK, agent-scoped // Attributes writes, and carries provenance from ask() into remember(). const memory = client.cortex(officeId).forAgent(officeId, 'atlas'); const evidence = await memory.ask('What did we decide about pricing last quarter?'); evidence.top; // best-scored result evidence.isEmpty; // nothing matched at all evidence.isFresh(3600); // did every source sync within the last hour? evidence.universalIds; // the ids remember() will link against evidence.graphUrl(); // /graph deep link highlighting exactly these nodes console.log(evidence.context()); // citation-formatted block for a prompt ``` ```bash CLI mi cortex ask "What did we decide about pricing last quarter?" --office "$OFFICE" mi cortex answer "..." --office "$OFFICE" --limit 10 \ | jq -r '.results[] | "\(.score)\t\(.title)\t\(.universal_id)"' ``` | MCP field | SDK equivalent | | --- | --- | | `results[].universal_id` | `answer.results[].universal_id`, or `evidence.universalIds` | | `cited_graph_url` | `evidence.graphUrl()`, or `graph_url` in `mi cortex ask --json` | | per-source sync state | `answer.freshness[]`, or `evidence.isFresh()` | Full worked example, including ingest: [ingest and query](/developers/sdk/ingest-and-query). ## Handling a gap instead of guessing The mistake to avoid is treating weak results as an answer. Check for the gap blocks described in [how memory works](/developers/memory/overview#nearest-matches-not-a-thresholded-set) before you compose a reply: ```ts const res = await askMemory(question); if (res.memory_state) { // Nothing connected yet. Offer res.memory_state.cta.url, don't apologise // for a search that never had anything to search. } else if (res.source_gap) { // The graph has data, just not this kind. Name the missing source and // offer res.source_gap.cta.url. } else if (res.possible_source_gap) { // Answer, but hedge: these may be near-misses. } else { // Trustworthy: either a real answer, or a real absence. } ``` The `cta.url` in each block opens the user's own dashboard page for connecting sources. Handing it over is safe: the link only *shows* them that page, and connecting is an authorization they perform there. --- # Writing to a memory URL: https://mitosislabs.ai/developers/memory/writing cortex_remember, cortex_ingest, and cortex_ingest_conversation. Memory is not read-only. An agent that reaches a conclusion worth keeping should put it back, so the next session, in any client, can retrieve it. Both write tools need the `memory:write` scope. ## cortex_remember Stores one durable fact. The text should be a single self-contained statement, readable years later without the surrounding conversation. ```json { "text": "Pricing moved to $19.99/seat for Solo in July 2026.", "kind": "decision", "confidence": 0.9, "source_universal_ids": ["gmail_messages:18f2c...", "drive_files:1kZ..."] } ``` | Argument | Type | Notes | | --- | --- | --- | | `text` | string | **Required.** The fact itself, as one self-contained statement. | | `kind` | string | e.g. `decision`, `observation`, `task-outcome`. | | `confidence` | number | `0..1`. Weights the provenance edges. | | `source_universal_ids` | string[] | Universal ids from a previous `cortex_ask` that this fact came from. | Passing `source_universal_ids` is what turns a free-floating note into a linked node. Take the ids straight from `results[].universal_id` on the `cortex_ask` that led you to the conclusion. ### What is worth remembering Write facts that outlive the session: decisions and their reasons, stable preferences, outcomes, corrections to something previously believed. Skip the transient: what the user asked five minutes ago, anything already in a connected source (it is in the graph already), and anything you inferred with low confidence and did not verify. A memory full of restated context retrieves worse than a sparse one. ### The same write from the SDK and the CLI ```ts SDK, agent-scoped const memory = client.cortex(officeId).forAgent(officeId, 'atlas'); // Provenance defaults to the results of this instance's most recent ask(), // so the conclusion is linked to the evidence that produced it. await memory.ask('what did pricing move to?'); await memory.remember('Pricing moved to $19.99/seat for Solo in July 2026.', { kind: 'decision', confidence: 0.9, }); // Override it explicitly when you need to: await memory.remember('...', { sources: ['gmail_messages:18f2c...'] }); await memory.remember('...', { sources: [] }); // store with no provenance ``` ```ts SDK, explicit await client.cortex(officeId).remember(officeId, { agent: 'atlas', // required text: 'Pricing moved to $19.99/seat for Solo in July 2026.', kind: 'decision', confidence: 0.9, source_universal_ids: ['gmail_messages:18f2c...', 'drive_files:1kZ...'], }); ``` ```bash CLI mi cortex remember "Pricing moved to \$19.99/seat for Solo in July 2026." \ --office "$OFFICE" --agent atlas --kind decision --confidence 0.9 \ --source gmail_messages:18f2c... --source drive_files:1kZ... ``` The write is idempotent per agent and text: re-remembering the same fact updates it in place rather than adding a second node. The response tells you what landed: ```json { "status": "ok", "universal_id": "agent_memory:atlas:9f21...", "embedded": true, "linked_sources": 2, "derived_from_edges": 2, "retrievable_at": "2026-08-26T18:04:11Z" } ``` ## cortex_ingest Pushes a document into the memory so its text is searchable — a CSV, a note, an export the user handed the agent. This is content, not a derived conclusion. ```json { "filename": "investors.csv", "content": "name,email\nAda,ada@example.com\n" } ``` The HTTP MCP server takes the text (`filename` + `content`, or `files[]`). It cannot read a local path. The stdio server (`mi-cortex-mcp`) also accepts `path` / `paths` and reads those files from disk. A new source waits until the user picks **Standard** or describes a goal. The response carries `choice`; when `choice.choice_required` is true, record the answer with `cortex_choose_enrichment`. ## cortex_ingest_conversation Ingests a whole conversation rather than a single fact, so the exchange itself becomes retrievable. Use it at the end of a substantive session; use `cortex_remember` for the one-line conclusions that came out of it. ## Ingesting content, not conclusions `cortex_remember` is for facts an agent derived. To push whole documents into a memory, so their text becomes retrievable in its own right, use `cortex_ingest` (MCP) or the SDK ingest path: ```ts const cortex = client.cortex(officeId); const { feedKey } = await cortex.ensureFeed(officeId, 'handbook'); await cortex.pushRows(officeId, feedKey, [{ external_id: 'handbook:pricing', // stable id: re-pushing updates in place title: 'Pricing policy', content: 'Solo moved to $19.99 per seat in July 2026. Team stayed at $49.', }], { deferEmbed: true }); ``` See [ingest and query](/developers/sdk/ingest-and-query) for batching, local files, and the CLI and plain-HTTP equivalents. ## Absent write scope If a client holds only `memory:read`, the write tools are not offered. That is the expected state for a read-only integration. Treat it as a signal to surface what you would have stored, rather than as an error to retry. --- # Connecting sources URL: https://mitosislabs.ai/developers/memory/sources Discover what a user can connect, and hand them a link that does it. An agent can never connect a source itself. Connecting is an authorization the user performs in their own dashboard. What an agent can do is *discover* what is connectable and *produce the link* that takes them there. That distinction matters when you are writing agent copy: the link only shows the user a page. It grants nothing on its own, so offering it is safe and needs no hedging. ## Discover what is available `cortex_connectable_sources` takes no arguments and lists the sources this user has **not** yet connected, with what each one would answer: ```json {} ``` A source type missing from that list can still reach the memory as an uploaded export. "Not connectable" is not the same as "unsupported". ## Produce a connect link `cortex_connect_link` returns the link for one source: | Argument | Type | Notes | | --- | --- | --- | | `source_id` | string | **Required.** The id exactly as listed, e.g. `google-workspace`, `github`, `notion`. | | `link_text` | string | Link text naming what connecting *answers*, in the user's own words. | `link_text` is worth care. "Connect Google Workspace" describes a mechanism; "find the thread where we agreed the launch date" describes what the user gets. The second is the one that gets clicked, so take the phrasing from what they actually asked. ```json { "source_id": "google-workspace", "link_text": "search the mail thread where you agreed the launch date" } ``` ## When to offer one Do not volunteer connect links against every weak result. Offer one when the response explicitly tells you the source is missing: a `source_gap` or `memory_state` block, each of which already carries a ready-made `cta.headline` and `cta.url`. Those are the moments where connecting genuinely unblocks the user's question rather than interrupting it. ## Ingestion is not instant After a user connects something, ingestion runs in the background. [`cortex_status`](/developers/mcp/tools#cortex-status) reports the backlog. A memory that answers poorly one minute after a large mailbox is connected is not broken. It is still filling. --- # Metadata and filters URL: https://mitosislabs.ai/developers/memory/metadata The fields you attach when writing to a memory, and the filters that read them back. Every item in a memory carries the same small set of fields, whatever produced it. Those fields are what retrieval filters on, what ranking sorts by, and what decides where an item is allowed to travel. This page is the reference for all of them. ## What you set when pushing content `pushRows` takes an array of rows. Four fields are structural, the rest is yours. ```ts await cortex.pushRows(officeId, feedKey, [ { external_id: 'handbook/onboarding.md', title: 'Engineering onboarding', content: 'Day one: get an office invite, then run mi login...', modality: 'document', file_path: 'onboarding.md', metadata: { team: 'eng', reviewed: '2026-08-01' }, // Anything else is kept verbatim on the row. tags: ['handbook', 'onboarding'], }, ]); ``` | Field | Type | Notes | | --- | --- | --- | | `external_id` | string | **Required.** Stable identity. Pushing the same id again updates that item in place rather than adding a second copy. | | `content` | string | **Required.** The text that gets embedded and full-text indexed. | | `title` | string | Embedded alongside `content`, and used as the citation label. | | `modality` | string | What kind of thing this is, for example `document`, `note`, or `file`. | | `file_path` | string | The office drive filename, when the original was uploaded separately. | | `metadata` | object | Free-form. Stored with the row and returned on reads. | Any other top-level key you send is stored verbatim on the row rather than discarded. That matters because extraction rules read those keys by path: a `tags` or `wikilinks` array becomes graph structure if a rule points at it. See [Typed entities](/developers/concepts/typed-entities) for how a rule is declared. `metadata` is a bag that comes back on reads. Top-level keys are what extraction rules can address. If you want a value to become an entity or an edge, put it at the top level, not inside `metadata`. ## Sensitivity Every item carries a sensitivity, and it is the field that decides where the item is allowed to go. There are exactly three values, enforced as a database constraint rather than a convention: | Value | Meaning | | --- | --- | | `local-only` | The default. Stays inside the cortex. | | `redact` | May leave, with sensitive spans removed. | | `cloud-ok` | May be sent to an external model or service. | Items land as `local-only` unless something sets otherwise, so a memory you have just filled is entirely `local-only`. You can restrict a query to one band: ```ts await cortex.answer(officeId, { query: 'pricing', sensitivity: 'cloud-ok' }); ``` An unrecognized sensitivity is not rejected. The filter simply matches nothing, so a typo like `cloud_ok` or `public` returns zero results that look exactly like an empty memory. If a filtered query comes back empty, check the spelling against the three values above before concluding the data is missing. ## Event time and ingest time Two timestamps mean different things, and confusing them is the usual cause of a wrong answer to a question about recency. When the cortex ingested the row. A file written last year but synced this morning has a `fetched_at` of this morning. When the thing actually happened at its source: the message sent, the event started, the file last modified. This is the field to reason about when the question is about time. Retrieval reads the question for temporal intent and reports what it decided in `signals.temporal_intent`: | Value | Triggered by | Effect | | --- | --- | --- | | `none` | An ordinary question | Ranked by relevance only. | | `recency` | "the latest", "most recent" | Adds a time-ordered leg. Results gain a `time_rank`. | | `window` | "in the last three months" | Applies a hard time bound. | Because a window is a hard bound, a narrow or empty result set on a time-phrased question is often the bound doing its job rather than missing data. `signals.temporal_intent` tells you which happened. Content pushed with `pushRows` has no `event_time`. The feed that `ensureFeed` creates does not declare a source of event time, so pushed rows carry only `fetched_at` and cannot take part in recency ranking. If the timing of your own content matters, keep a date in `metadata` and filter on it yourself, and prefer relevance-phrased questions over "latest" ones. ## Filtering a query Every filter below is optional and they combine. All of them apply to `cortex.answer`. | Filter | Type | Notes | | --- | --- | --- | | `source_table` | string | One source table, for example `calendar_events`. | | `integration_id` | string | One integration, for example `google-workspace`. | | `sensitivity` | string | One of the three values above. | | `since` | string | RFC 3339 lower bound on item time. | | `until` | string | RFC 3339 upper bound on item time. | | `limit` | number | Maximum results. | | `expand_graph` | boolean | Pull one hop of graph neighbours around the top candidates. Defaults to on. | | `include_raw` | boolean | Return the full original row per result. Heavier payload. | ```ts const evidence = await cortex.answer(officeId, { query: 'renewal terms', source_table: 'gmail_messages', since: '2026-06-01T00:00:00Z', limit: 10, }); ``` Narrowing to a single source is also the cheapest thing you can do to a slow query, because it shrinks every leg of the search at once. ## Metadata on a fact you write `remember` stores a conclusion rather than a source document, so it takes its own metadata: ```ts await cortex.remember(officeId, { agent: 'atlas', text: 'Solo tier moved to 19.99 per seat in July 2026.', kind: 'decision', confidence: 0.9, source_universal_ids: evidence.universalIds, metadata: { ticket: 'CLA-1234' }, }); ``` | Field | Type | Notes | | --- | --- | --- | | `agent` | string | **Required.** Who is remembering. Every fact is attributed. | | `text` | string | **Required.** One self-contained fact. | | `kind` | string | A label such as `decision` or `observation`. | | `confidence` | number | 0 to 1. Weights the derivation edges back to the sources. | | `source_universal_ids` | string[] | What this was concluded from. Pass the ids from the answer you reasoned over. | | `sensitivity` | string | Defaults to `local-only`, like everything else. | | `metadata` | object | Free-form. | Writes are idempotent per agent and text, so re-running the same `remember` updates the existing fact instead of duplicating it. ## Deleting `purge` removes a filtered subset of a memory: the raw rows, their embeddings, and everything derived from them. It takes the same filter vocabulary as a query, and they stack the same way. ```ts // Preview first. This is the default: an absent dry_run means "do not delete". const plan = await cortex.purge(officeId, { source_table: 'gmail_messages' }); console.log(plan.matched, 'items,', plan.entities_deleted, 'entities'); // Then commit, in batched rounds. await cortex.purgeAll(officeId, { source_table: 'gmail_messages', dry_run: false, limit: 1000, }); ``` A dry run is not an estimate. The server performs the real cascade and then rolls it back, so the counts are measured. On one colony a selector matching **21 rows** reported 136 graph nodes, 120 edges and 98 entities alongside them: derived structure is most of what a delete actually touches, and guessing at it is how people delete more than they meant to. Four properties are worth knowing before you write the call: `dry_run` defaults to true. You have to pass `dry_run: false` to remove anything, so a forgotten field can never be the destructive choice. A call with no filters returns 400 rather than matching the whole colony. A dropped variable cannot silently become "delete everything". One call purges at most `limit` items, capped at 5000, so a wide selector cannot hold a long lock on the colony database. Loop while `remaining > 0`, or let `purgeAll` do it. Unlike a query, a bad `sensitivity` or `time_field` here is a 400. On a read an unrecognized value is a confusing empty result; on a delete a silent no-op is worse, because you would believe it worked. To remove a whole source rather than a subset, use `deregisterFeed`, which takes a `feed_key` or an `integration_id` and also drops the feed registration itself. That is what the dashboard's disconnect-a-source flow calls. From a shell, `mi cortex purge` previews by default and needs `--yes` to delete: ```bash mi cortex purge --office --table gmail_messages --until 2026-01-01T00:00:00Z mi cortex purge --office --table gmail_messages --until 2026-01-01T00:00:00Z --yes --all ``` ## Current limits The list and graph reads take a `limit` and have no cursor, so they are not a way to walk an entire memory. Use them for sampling and use [retrieval](/developers/memory/asking) for finding things. --- # MCP server URL: https://mitosislabs.ai/developers/mcp/overview Streamable HTTP at /api/mcp, the route most agents take into a memory. Mitosis runs a Model Context Protocol server at **`https://mitosislabs.ai/api/mcp`** over Streamable HTTP. It is the primary integration surface: Claude, ChatGPT, Cursor, Grok and any MCP-speaking client connect to it directly. ## Servers | Server | URL | Auth | | --- | --- | --- | | Product | `https://mitosislabs.ai/api/mcp` | OAuth 2.1 for memory tools; public tools open | | Docs | `https://mitosislabs.ai/api/mcp/docs` | None | | Backups | `https://mitosislabs.ai/api/mcp/backups` | Bearer token | | Sandbox | `https://dev.mitosislabs.ai/api/mcp` | Same, scoped to the sandbox | ## Auth The server supports OAuth 2.1 with **dynamic client registration**, so a client needs no pre-registered `client_id`. Point it at the URL and the user approves access in a browser. Full detail in [authentication](/developers/authentication). Public tools answer before any sign-in, which makes them a good connectivity check: if `get_platform_status` returns and `cortex_ask` prompts for auth, the transport is fine and you simply have not authorized yet. ## Discovery The server card mirrors `tools/list` exactly, so you can inspect the tool surface without connecting: ```bash curl -s https://mitosislabs.ai/.well-known/mcp/server-card.json | jq '.tools[].name' ``` It is generated from the same modules the live server serves, so it cannot drift from what the server actually offers. Each tool carries its annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`) and, for the memory tools, the scope it requires. ## Resources Beyond tools, the server exposes the machine-readable documents as MCP resources: `llms-txt`, `llms-full-txt`, `pricing-md`, `auth-md`, and `openapi`, plus a `read_doc` resource for individual documentation pages. Clients cache tool lists. If you change what a connector exposes and a client keeps calling the old surface, disconnect and re-add the connector to force a refresh. This is a common source of "my new tool isn't showing up". ## Next Every tool, argument, and scope. Claude, ChatGPT, Cursor, Grok, and the CLI. --- # MCP tool reference URL: https://mitosislabs.ai/developers/mcp/tools Every tool the Mitosis MCP server exposes, with arguments and required scope. The live list is always `tools/list`, mirrored byte-for-byte at [`/.well-known/mcp/server-card.json`](https://mitosislabs.ai/.well-known/mcp/server-card.json). This page is the human-readable version of the same surface. ## Memory tools All of these touch a user's private memory and require an OAuth grant. ### cortex_ask Searches the memory, fusing vector, full-text and graph retrieval, and answers with provenance. **Scope:** `memory:read`. | Argument | Type | Required | | --- | --- | --- | | `question` | string | ✓ | | `limit` | number | Defaults to 10 | | `source_table` | string | Restrict to one source table | | `since` / `until` | string | RFC 3339 bounds on item time | It returns evidence, not prose. Every response carries `results[]` (each with a `universal_id`, a `score` and a `preview`), `freshness[]`, `cited_graph_url`, and `memory` naming which office answered: ```json { "results": [ { "universal_id": "gmail_messages:18f2c9a...", "score": 0.412, "title": "Re: tier pricing", "preview": "Agreed: Solo goes to $19.99/seat from July.", "source_table": "gmail_messages" } ], "freshness": [{ "source_id": "google-workspace", "last_success_at": "2026-08-26T18:02:00Z" }], "cited_graph_url": "https://mitosislabs.ai/graph?office=4d38b034-...&cite=gmail_messages%3A18f2c9a...", "memory": { "office_id": "4d38b034-...", "office_name": "Mitosis Labs" } } ``` It may also carry `memory_state`, `source_gap` or `possible_source_gap`, which say *why* an answer is thin. Read them before composing a reply: see [reading the response](/developers/memory/asking#reading-the-response) and [handling a gap](/developers/memory/asking#source-gaps). ### cortex_recall Semantic-only vector search returning source excerpts. Prefer `cortex_ask` unless you specifically want nearest-neighbour matches on meaning. **Scope:** `memory:read`. | Argument | Type | Required | | --- | --- | --- | | `query` | string | ✓ | | `limit` | number | Defaults to 10 | ### cortex_manifest An index of the memory: counts and source names, **never content**. Takes no arguments. **Scope:** `memory:read`. ### cortex_status Ingestion and feed status, including backlog. Use it to explain why a freshly-connected source is not answering yet. **Scope:** `memory:read`. ### cortex_connectable_sources Lists sources the user has not connected, and what each would answer. No arguments. **Scope:** `memory:read`. ### cortex_connect_link Returns the connect link for one source. **Scope:** `memory:read`. | Argument | Type | Required | | --- | --- | --- | | `source_id` | string | ✓. For example `google-workspace`, `github`, `notion` | | `link_text` | string | Link text in the user's own words | ### cortex_remember Saves one durable fact into the graph. **Scope:** `memory:write`. | Argument | Type | Required | | --- | --- | --- | | `text` | string | ✓ | | `kind` | string | e.g. `decision`, `observation`, `task-outcome` | | `confidence` | number | `0..1` | | `source_universal_ids` | string[] | Ids from a prior `cortex_ask` | ### cortex_ingest Pushes a document into the memory so its text is searchable. **Scope:** `memory:write`. This is how an agent adds a file the user handed it (a CSV, a note, an export). Pass the text; this HTTP server cannot read a local filesystem path. The stdio server (`mi-cortex-mcp`) also accepts `path` / `paths` and reads those files from disk. | Argument | Type | Required | | --- | --- | --- | | `filename` | string | With `content`. The document's name | | `content` | string | With `filename`. The document text | | `files` | `{ name: string, content: string }[]` | Several documents in one call | | `feed` | string | Feed id (`ext_`). Letters, digits, underscore. Defaults to `local_files` | | `title` | string | Override title when ingesting a single file | ```json { "filename": "investors.csv", "content": "name,email\nAda,ada@example.com\n" } ``` A new source waits for Standard or a described goal. The response carries `choice`; when `choice.choice_required` is true, record the user's answer with `cortex_choose_enrichment`. ### cortex_ingest_conversation Ingests a whole conversation so the exchange itself becomes retrievable. **Scope:** `memory:write`. | Argument | Type | Required | | --- | --- | --- | | `turns` | `{ role: "user" \| "assistant", text: string }[]` | ✓. In order, verbatim | | `session_id` | string | Stable id for the conversation. Re-ingesting the same one upserts rather than duplicating | | `title` | string | Short human title | ```json { "turns": [ { "role": "user", "text": "What did we decide about pricing?" }, { "role": "assistant", "text": "Solo moved to $19.99/seat in July 2026." } ], "session_id": "chat_01H9X...", "title": "Pricing decision" } ``` Exclude tool output, hidden reasoning, connect links, exchanges where you could not answer, and anything resembling a credential. Split a very long conversation across calls, reusing the same `session_id`. ### cortex_choose_enrichment Records what the user wants pulled out of a source: **Standard**, or one or more saved goals. A new source waits until this is answered. **Scope:** `memory:write`. | Argument | Type | Required | | --- | --- | --- | | `feed_key` | string | ✓ | | `choice` | string | `standard` or `describe_goal`. Omit to read the current state | | `goal` | string | New goal, in the user's words. Needed with `describe_goal` unless `goal_ids` is set | | `goal_ids` | string[] | Saved goals from `cortex_list_goals` to run on this source | | `idempotency_key` | string | Same key never records twice | ### cortex_list_goals Lists saved goals this office already asked Mitosis to pull out. Last-used first. Pass chosen ids as `goal_ids` to `cortex_choose_enrichment`. No arguments. **Scope:** `memory:read`. ### cortex_create_goal Designs a goal from the user's own words and returns a read-back of what Mitosis understood, with a real example from their data. When the goal leaves something Mitosis cannot decide, the reply carries `answer_required: true` and one round of `questions`, each with the options it can act on. Designing a goal extracts nothing. Designing, saving, and running are three separate operations. **Scope:** `memory:write`. | Argument | Type | Required | | --- | --- | --- | | `goal` | string | ✓ What the user wants out of their data, in their words | | `feed_keys` | string[] | Sources to draw the read-back example from. A goal belongs to the memory, not to one source | ### cortex_answer_goal Records the answer to the questions from `cortex_create_goal`, keyed by question id. `{"*":"skip"}` is the answer for a user who says to go ahead with whatever Mitosis thinks best. This is the last round. The reply carries `answer_required: false` and no further questions: Mitosis asks about a goal once. **Scope:** `memory:write`. | Argument | Type | Required | | --- | --- | --- | | `draft_id` | string | ✓ From `cortex_create_goal` | | `answers` | object | ✓ Question id to the chosen option, or the user's own words | ### cortex_accept_goal Saves a settled goal into the memory's goal library with the recipe it was designed with, and returns its goal id. Still extracts nothing: `cortex_choose_enrichment` with `choice=describe_goal` and that id in `goal_ids` is what runs it on a source. **Scope:** `memory:write`. | Argument | Type | Required | | --- | --- | --- | | `draft_id` | string | ✓ From `cortex_create_goal` or `cortex_answer_goal` | ## Search and fetch Present for clients that expect the `search` / `fetch` pair. ChatGPT in particular refuses a server whose `search` and `fetch` it cannot see. | Tool | Required argument | What it does | | --- | --- | --- | | `search` | `query` | Searches the user's memory | | `fetch` | `id` | Retrieves one item in full by its universal id | ## Public tools No credential. Safe to call before any sign-in, and useful as a connectivity check. | Tool | Required argument | What it returns | | --- | --- | --- | | `get_pricing` | none | Current plans, prices, credit allowances, how usage is charged, add-ons | | `get_platform_status` | none | Operational status of the website, API and MCP server | | `search_docs` | `query` | Ranked documentation and product-page results with URLs | | `list_skills` | none | Published agent skills, with links | ## Resources | Resource | Contents | | --- | --- | | `llms-txt` | Service summary for language models | | `llms-full-txt` | The complete documentation corpus | | `pricing-md` | Machine-readable pricing | | `auth-md` | Agent registration and authentication instructions | | `openapi` | Full spec for the public REST API | | `read_doc` | One reference document in full, by `doc` id | Annotations on every tool (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`) are carried on the server card. If your harness gates tool use on annotations, read them from there rather than hard-coding assumptions. --- # Connecting an MCP client URL: https://mitosislabs.ai/developers/mcp/clients Claude, ChatGPT, Cursor, Grok, and anything else that speaks MCP. Every client below connects to the same URL, `https://mitosislabs.ai/api/mcp`, over Streamable HTTP, and authorizes through the same OAuth flow. Only the registration syntax differs. ## Claude ```bash Claude Code claude mcp add --transport http mitosis https://mitosislabs.ai/api/mcp ``` ```text claude.ai Settings → Connectors → Add custom connector → paste the server URL. ``` ## Cursor and other config-file clients ```json { "mcpServers": { "mitosis": { "url": "https://mitosislabs.ai/api/mcp" } } } ``` ## ChatGPT Add it as a custom connector with the same URL. ChatGPT requires a server to expose `search` and `fetch`, which Mitosis does. See [tool reference](/developers/mcp/tools#search-and-fetch). ChatGPT ignores MCP server `instructions` entirely. Everything a model needs to use these tools correctly is therefore repeated in each tool's own description, which is why they read long. ## Local stdio bridge If a client can only speak stdio, the SDK ships a bridge binary: ```bash npx -y -p @mitosislabs/sdk@latest mi-cortex-mcp ``` It resolves a credential from `MI_API_KEY`, then `MITOSIS_API_KEY`. ## After connecting Confirm the surface is live before debugging anything deeper: `get_platform_status` needs no auth. If it answers, transport is fine. `cortex_manifest` returns counts and source names. If it prompts for auth, complete the browser flow. `cortex_ask` against something you know is in the graph. ## Tool lists are cached Clients cache `tools/list`. When a connector's tools change, a client may keep calling the old surface until the connector is removed and re-added. If a tool you know exists is invisible, re-add before investigating the server. --- # REST API reference URL: https://mitosislabs.ai/developers/api-reference Every operation in the public API at /api/v1, generated from the OpenAPI spec. The public API is small on purpose: nine read-oriented operations under `/api/v1`, none of which need a credential. Errors are RFC 9457 `application/problem+json`, responses carry rate-limit headers, and writes accept an `Idempotency-Key`. The reference below is generated from [`/openapi.json`](https://mitosislabs.ai/openapi.json), the same document the site serves, so the two cannot disagree. Point a code generator or a function- calling agent at the spec directly rather than at this page. Both hosts are listed in the spec's `servers` array. `dev.mitosislabs.ai` mirrors production with disposable data and no billing side effects, and `GET /api/v1` reports which one you reached in its `environment` field. --- # Mitosis CLI URL: https://mitosislabs.ai/developers/cli/overview mi: query and manage a memory from a terminal. The `mi` CLI ships inside the [`@mitosislabs/sdk`](https://www.npmjs.com/package/@mitosislabs/sdk) npm package. It is the fastest way for humans and agents to talk to Mitosis from a shell. ## Install ```bash npx npx -y -p @mitosislabs/sdk@latest mi --help ``` ```bash global npm install -g @mitosislabs/sdk mi --help ``` ## Authenticate ```bash mi login # device-code flow, prints a URL to approve in the browser mi whoami # verify the session ``` For headless use, set `MI_API_KEY=mi_...` with a key from the dashboard user menu; no `mi login` needed. See [authentication](/developers/authentication). ## Commands Every `mi cortex` command takes `--office `. Get yours from `mi offices list`, which prints JSON: ```bash OFFICE=$(mi offices list | jq -r '.[0].id') ``` ### Memory | Command | What it does | | --- | --- | | `mi cortex ask "" -o ` | Hybrid retrieval, rendered as a cited, prompt-ready block. Add `--json` for the raw response. | | `mi cortex answer "" -o ` | Same retrieval as raw JSON: RRF scores, per-leg signals, freshness. | | `mi cortex recall "" -o ` | Semantic-only vector search. Hits carry source and excerpt. | | `mi cortex remember "" -o ` | Write a durable fact back, with `--kind`, `--confidence`, `--source `. | | `mi cortex ingest -o ` | Ingest local files. Text becomes searchable content; binaries go to the office drive with a metadata row. | | `mi cortex sync-vault -o ` | Sync a folder of Markdown. Recurses, chunks large notes, resumable, and `--schedule hourly` keeps it current. | | `mi cortex get -o ` | The original source row behind any citation. | | `mi cortex status -o ` | Totals, embed and graph coverage, per-feed freshness. | | `mi cortex manifest -o ` | What the memory contains: connected sources, item counts, and the top people and topics it knows. `--json` for the raw manifest. | ### Everything else | Command | What it does | | --- | --- | | `mi login` / `mi whoami` | Device-code sign-in, and verify the session | | `mi offices list` | List offices you belong to | | `mi backup create --agent --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`.