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
npm install @mitosislabs/sdk
You need two things: an API key and an office id. The office is the memory.
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.
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;
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.
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_<feedId>) 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.
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.
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.
async function ingestInBatches(
cortex: ReturnType<typeof client.cortex>,
officeId: string,
feedKey: string,
rows: Array<Record<string, unknown>>,
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.
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.
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:
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:
const nearest = await cortex.recall(officeId, { query: 'refund policy', limit: 10 });
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.
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:
## 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:
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".
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.
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.
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.
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
A bulk push times out
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.
Rows ingest but the query finds nothing
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.
Re-running the script duplicated everything
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.
Every request 401s even though `mi login` worked
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.answer is not a function`
client.cortex is a factory. Call it first: client.cortex(officeId).answer(officeId, {...}).
Where to go next
Asking a memory
Reading scores, freshness, and source gaps rather than trusting rank one.
Writing to a memory
What is worth remembering, and what only makes retrieval worse.
Files and environment
The office drive that file_path rows point at.
Building an integration
Turn a one-off ingest script into a connectable source.