Store and retrieve long-term memory for AI agents with TypeScript or JavaScript.
npm install getmnemoRequires Node.js 18 or later. The package has no runtime dependencies and supports both ESM and CommonJS.
Create a client, add a memory, and search the same container:
import { Mnemo } from 'getmnemo'
const mnemo = new Mnemo({
apiKey: process.env.MNEMO_API_KEY!,
})
await mnemo.add({
containerTag: 'user:jane',
content: 'Jane prefers Japanese short-grain rice for onigiri.',
memoryType: 'preference',
})
const { results } = await mnemo.search({
containerTag: 'user:jane',
q: 'What kind of rice does Jane prefer?',
})
console.log(results)Keep the API key on your server. Do not expose a full-access key in browser code.
A container keeps one user, customer, project, or agent separate from the others.
Use a readable containerTag:
await mnemo.add({
containerTag: 'customer:acme',
content: 'Acme renewed through December.',
})Or use the equivalent structured scope:
await mnemo.add({
scope: { type: 'customer', id: 'acme' },
content: 'Acme renewed through December.',
})Set a default when most calls use the same container:
const mnemo = new Mnemo({
apiKey: process.env.MNEMO_API_KEY!,
defaultContainerTag: 'user:jane',
})
await mnemo.add({ content: 'Jane likes onigiri.' })
await mnemo.search({ q: 'What food does Jane like?' })The SDK stops the request if a required container is missing.
const response = await mnemo.add({
containerTag: 'user:jane',
content: 'Jane avoids shellfish.',
memoryType: 'preference',
metadata: { source: 'onboarding' },
})
console.log(response.items[0].id)
console.log(response.receipt.status) // "searchable"addMany() accepts up to 100 memories:
const response = await mnemo.addMany({
scope: { type: 'customer', id: 'acme' },
source: { provider: 'hubspot', importId: 'run_42' },
items: [
{
content: 'Acme renewed through December.',
idempotencyKey: 'hubspot:deal:123:v9',
metadata: { objectType: 'deal' },
},
{
content: 'The account owner is Priya.',
idempotencyKey: 'hubspot:company:456:owner:v3',
},
],
})
console.log(response.stats)
console.log(response.receipt.items)For a governed import whose source system is authoritative, keep the submitted text and metadata unchanged while skipping derived LLM enrichment:
await mnemo.addMany({
scope: { type: 'customer', id: 'acme' },
enrichmentMode: 'skip',
items: approvedRecords,
})The API key determines the workspace. workspaceId remains accepted only as a
deprecated compatibility option and is no longer sent as a caller-selected
header. For audit or rebuild workflows, use createWorkspaceExport(), then
poll getWorkspaceExport() until its download URL is available.
Use a stable idempotencyKey when an import may be retried. Repeating the same
write will not create another copy.
Every successful add() and addMany() response includes a receipt:
{
writeId: 'e5bf3f0f-16e2-4a5d-9c7f-98e437cf86c4',
status: 'searchable',
searchableAt: '2026-07-28T09:14:22.442Z',
items: [
{
inputIndex: 0,
memoryId: '8a8a4f8c-cf91-43e4-9a0e-7c2bb2c4d3f2',
status: 'created',
},
],
}Receipt items follow input order. A deduplicated item reused an equivalent
memory already present in the same container. Both outcomes are searchable
when the API returns.
The SDK validates the complete receipt before resolving add() or addMany().
Missing, duplicated, or contradictory item outcomes fail the call instead of
being reported as successful. Timed-out or disconnected memory batches are
retried only when every item carries a stable idempotencyKey.
const { results } = await mnemo.search({
containerTag: 'user:jane',
q: 'What changed after the dentist appointment?',
limit: 10,
includeSources: true,
})
for (const result of results) {
console.log(result.score, result.content, result.sources?.provenance)
}Use the following options only when your application needs more control:
| Option | Values | Purpose |
|---|---|---|
searchMode |
hybrid, memories, documents |
Select the content to search. |
filters |
Record<string, unknown> |
Restrict results by metadata. |
includeSources |
boolean |
Return provenance with each result. |
strategies |
temporal, graph, rerank, agentic |
Add a retrieval strategy to the normal search. |
excludeIds |
string[] |
Leave out results already in the agent's context. |
For example:
const result = await mnemo.search({
containerTag: 'user:jane',
q: 'What changed after the dentist appointment?',
strategies: ['temporal'],
excludeIds: ['mem_123'],
})
console.log(result.strategiesRan)temporal usually adds little latency. graph may add moderate latency.
rerank and agentic may add model cost and take longer.
Use documents for conversations, notes, transcripts, and other raw text. Mnemo processes documents asynchronously.
const accepted = await mnemo.documents.create({
containerTag: 'customer:acme',
content: meetingTranscript,
contentType: 'conversation',
customId: 'meeting-2026-07-27',
})
const job = await mnemo.jobs.wait(accepted.jobId)
console.log(job.status)createBatch() accepts up to 50 documents. Every result is reported separately,
so one invalid document does not hide the others.
const batch = await mnemo.documents.createBatch({
documents: [
{
scope: { type: 'customer', id: 'acme' },
content: meetingTranscript,
contentType: 'conversation',
customId: 'meeting-2026-07-27',
},
{
scope: { type: 'customer', id: 'acme' },
content: accountNotes,
contentType: 'note',
customId: 'account-notes-2026-07-27',
},
],
})
for (const item of batch.results) {
if (item.status === 'accepted') {
await mnemo.jobs.wait(item.jobId)
} else {
console.error(`Document ${item.index} failed: ${item.error}`)
}
}Queue a public YouTube video as a timestamped transcript:
const estimate = await mnemo.youtube.estimate({
url: 'https://www.youtube.com/watch?v=VIDEO_ID',
mode: 'transcript',
})
if (estimate.quota.willFit) {
const video = await mnemo.youtube.create({
scope: { type: 'course', id: 'onboarding' },
url: estimate.canonicalUrl,
mode: 'transcript',
})
await mnemo.jobs.wait(video.jobId)
}Use transcript_and_visuals when diagrams, interface changes, actions, or
on-screen text matter. Mnemo stores selected timestamped observations with
YouTube evidence links; it does not copy or retain the video itself.
Addressing a memory by id needs a container, exactly like list. Pass
containerTag or scope per call, or set defaultContainerTag on the client
and omit it everywhere:
const memory = await mnemo.get('mem_123', { containerTag: 'user:jane' })
await mnemo.update(
memory.id,
{
content: 'Jane now prefers brown rice.',
metadata: { changedBy: 'user' },
},
{ containerTag: 'user:jane' },
)
const page = await mnemo.list({
containerTag: 'user:jane',
limit: 20,
})
console.log(page.items, page.nextCursor)A structured scope works too, and the client's defaultContainerTag fills in
whenever a call passes no container of its own:
await mnemo.get('mem_123', { scope: { type: 'customer', id: 'acme' } })
const scoped = new Mnemo({
apiKey: process.env.GETMNEMO_API_KEY!,
defaultContainerTag: 'user:jane',
})
await scoped.get('mem_123') // containerTag=user:jane is sent for youDeletion is recoverable when recovery is enabled for the workspace:
const deleted = await mnemo.delete('mem_123', { containerTag: 'user:jane' })
console.log(deleted.receipt?.restorableUntil)
await mnemo.restore('mem_123')Skip the recovery window only when permanent deletion is intentional:
await mnemo.delete('mem_123', { permanent: true, containerTag: 'user:jane' })Protected memories require explicitly privileged API-key scopes before they can be changed or deleted:
const result = await mnemo.add({
containerTag: 'user:jane',
content: 'Never disclose the account recovery phrase.',
mutationPolicy: 'privileged',
})
await mnemo.protect(result.items[0].id)
await mnemo.unprotect(result.items[0].id)Use a server-side key with memories:protect to create or change protection.
Updating, deleting, and restoring protected memories also requires the matching
memories:protected:update or memories:protected:delete scope.
profile() returns prompt-ready facts, recent context, preferences, and hard
constraints for one container:
const context = await mnemo.profile({
containerTag: 'user:jane',
staticLimit: 12,
dynamicLimit: 10,
})Search is off by default. To include search results, provide both
includeSearch and q:
const context = await mnemo.profile({
containerTag: 'user:jane',
includeSearch: true,
q: 'What should I know before replying?',
})Version 0.6.0 adds the personal-memory surface: people, reminders, a
timeline, a daily brief, meetings, inbound capture and memory merge. Each
resource needs its own API-key scope (for example people:read,
reminders:write, brief:read); existing keys do not gain them
automatically.
One memory container per person, tagged person:<slug>:
const jane = await mnemo.people.create({
displayName: 'Jane Doe',
relationship: 'client',
email: 'jane@example.com',
importantDates: [{ label: 'Birthday', date: '1990-04-12', recurring: true }],
})
const page = await mnemo.people.list({ q: 'jane' })
const person = await mnemo.people.get(jane.slug)
await mnemo.people.update(jane.slug, { company: 'Acme', notes: null })
// Reader answer over the person's memories, plus open reminders.
const summary = await mnemo.people.summary(jane.slug, {
q: 'What matters to Jane right now?',
})
// Archive (keeps the container); optionally soft-delete its memories.
await mnemo.people.archive(jane.slug, { deleteMemories: true })Per-person memories are ordinary memories in that container:
mnemo.list({ containerTag: jane.tag }) or
mnemo.add({ containerTag: jane.tag, content: '...' }).
A reminder is a memory with memoryType: "reminder" and a dueAt. File it
under a person, any container, or the client's defaultContainerTag:
const reminder = await mnemo.reminders.create({
content: 'Send Jane the revised deck',
dueAt: '2026-09-10T09:00:00Z',
personSlug: 'jane-doe',
idempotencyKey: 'deck:v2',
})
// Open reminders, workspace-wide, ordered by due time.
const open = await mnemo.reminders.list({ status: 'open', days: 30 })
// Overdue / due today / upcoming in the caller's timezone.
const buckets = await mnemo.reminders.upcoming({
days: 7,
timezone: 'Asia/Karachi',
})
await mnemo.reminders.update(reminder.id, { dueAt: '2026-09-11T09:00:00Z' })
await mnemo.reminders.complete(reminder.id)
await mnemo.reminders.reopen(reminder.id, { dueAt: '2026-09-12T09:00:00Z' })reminders.list and reminders.upcoming are workspace-wide unless you pass
containerTag or containerType; the client's defaultContainerTag is not
applied to them.
A merged, newest-first stream of memories, reminders and documents for one
container (event rows are opt-in):
const timeline = await mnemo.timeline.get({
containerTag: 'person:jane-doe',
from: '2026-09-01T00:00:00Z',
types: ['memory', 'reminder', 'document'],
limit: 50,
})
for (const item of timeline.items) {
console.log(item.occurredAt, item.type, item.title, item.createdBy?.label)
}Reminders due, important dates, recent captures, open follow-ups and today's meetings for one container:
const brief = await mnemo.brief.today({
containerTag: 'user:me',
timezone: 'Asia/Karachi',
sections: ['core', 'followUps'],
})
console.log(brief.reminders?.dueToday, brief.followUps?.answer)
// A specific local day.
const yesterday = await mnemo.brief.get({
containerTag: 'user:me',
date: '2026-09-01',
})Upcoming calendar meetings (synced through a connected Google Calendar) with attendees matched to people, and a pre-meeting brief:
const { items, connections } = await mnemo.meetings.upcoming({ days: 3 })
const meeting = await mnemo.meetings.get(items[0].documentId)
const prep = await mnemo.meetings.brief(meeting.documentId, {
q: 'What did we agree last time?',
})
console.log(prep.brief?.answer, prep.people, prep.previousMeetings)Link a phone number so its texts and voice notes become memories. The
verification code is returned only on create and regenerateCode; the
phone must text it once to activate the channel:
const channel = await mnemo.inbound.channels.create({
phone: '+14155550100',
containerTag: 'user:me', // defaults to phone:<E.164>
})
console.log(channel.verificationCode, channel.inboundNumber, channel.webhookUrl)
await mnemo.inbound.channels.list()
await mnemo.inbound.channels.update(channel.id, { status: 'disabled' })
await mnemo.inbound.channels.regenerateCode(channel.id)
await mnemo.inbound.channels.delete(channel.id)Fold duplicates into one survivor. With into, that memory keeps its id and
the others are soft-deleted (restorable); without it a new memory is created
from content. The key needs both memories:write and memories:delete:
const merged = await mnemo.memories.merge({
containerTag: 'user:jane',
ids: ['mem_1', 'mem_2'],
into: 'mem_1',
})
console.log(merged.memory.id, merged.deletedIds, merged.replayed)Every Memory now reports who wrote it in createdBy (api_key, mcp,
user, connector, inbound or system, with the key or client name as
label) and its dueAt. list() can filter on both:
const recent = await mnemo.list({
containerTag: 'user:jane',
since: '2026-09-01T00:00:00Z',
createdByKind: 'inbound',
memoryType: 'note',
})memoryType is optional. Supported values include:
memorypreferencefactobservationeventnoteremindergoal
The API stores an unknown value as memory instead of rejecting the write.
The SDK throws:
MnemoHTTPErrorfor an unsuccessful API response.MnemoTimeoutErrorwhen a request reaches its timeout.MnemoErroras the base class for SDK errors.
import { MnemoHTTPError } from 'getmnemo'
try {
await mnemo.search({
containerTag: 'user:jane',
q: 'rice preference',
})
} catch (error) {
if (error instanceof MnemoHTTPError) {
console.error(error.status, error.body)
} else {
throw error
}
}The client retries transient network errors, 429 responses, and 5xx
responses. Set maxRetries: 0 to disable retries.
| Option | Required | Default | Description |
|---|---|---|---|
apiKey |
Yes | - | Mnemo API key. The key is bound to one workspace. |
workspaceId |
No | - | Deprecated compatibility option; not sent to the API. |
defaultContainerTag |
No | - | Container used when a call does not provide one. |
baseUrl |
No | https://api.mnemohq.com |
API base URL. |
timeoutMs |
No | 30000 |
Request timeout in milliseconds. |
maxRetries |
No | 3 |
Retries for transient failures. |
fetch |
No | globalThis.fetch |
Custom fetch implementation for tests or proxies. |
API keys are full-access by default. You can create a key with only the
read, write, delete, or billing scopes it needs. The personal-memory
resources use explicit scopes (people:*, reminders:*, brief:read,
meetings:read, timeline:read, inbound:*) that existing keys do not gain
automatically. Use a read-only scoped
key or a server proxy when a key may reach client code.
| Method | What it does |
|---|---|
add(input) |
Add one memory. |
addMany(input) |
Add up to 100 memories. |
createWorkspaceExport() |
Start a tenant-bound workspace export. |
listWorkspaceExports() |
List export jobs for the current API-key workspace. |
getWorkspaceExport(exportId) |
Read one export job and its download URL. |
search(input) |
Search memories, documents, or both. |
get(memoryId, options?) |
Get one memory in an explicit or default container. |
list(input) |
List memories in one explicit or default scope, with since / until / createdByKind / memoryType filters. |
update(memoryId, input, options?) |
Update one memory in an explicit or default container. |
delete(memoryId, options?) |
Delete one memory in an explicit or default container. |
restore(memoryId) |
Restore a recoverable deletion. |
protect(memoryId) |
Require privileged mutation scopes for a memory. |
unprotect(memoryId) |
Return a memory to the standard mutation policy. |
setProtection(memoryId, policy) |
Set the mutation policy explicitly. |
profile(input) |
Build context for a prompt. |
documents.create(input) |
Start one document ingestion job. |
documents.createBatch(input) |
Start up to 50 document ingestion jobs. |
documents.get(documentId) |
Get one document. |
documents.list(input) |
List documents in one explicit or default scope. |
documents.update(documentId, input) |
Update and optionally reprocess a document. |
documents.delete(documentId) |
Delete a document. |
jobs.get(jobId, options?) |
Get one ingestion job. |
jobs.list() |
List ingestion jobs. |
jobs.wait(jobId, options?) |
Wait for an ingestion job to finish. |
youtube.estimate(input) |
Estimate a YouTube ingestion against quota. |
youtube.create(input) |
Start a YouTube ingestion. |
youtube.get(ingestionId) |
Get one YouTube ingestion. |
people.create(input) |
Create a person (person:<slug> container). |
people.list(input?) |
List people, newest first. |
people.get(slug) |
Get one person with memory and reminder counts. |
people.update(slug, input) |
Update contact fields; null clears a field. |
people.archive(slug, options?) |
Archive a person, optionally soft-deleting their memories. |
people.summary(slug, options?) |
Reader summary plus open reminders and recent memories. |
reminders.create(input) |
Create a reminder under a person or container. |
reminders.list(input?) |
List reminders by status and due window. |
reminders.upcoming(input?) |
Overdue / today / upcoming buckets and important dates. |
reminders.update(memoryId, input) |
Reschedule or edit a reminder. |
reminders.complete(memoryId) |
Complete a reminder. |
reminders.reopen(memoryId, input) |
Reopen a completed reminder with a new due time. |
timeline.get(input?) |
Merged memory / document / event stream for one container. |
brief.today(input?) |
Daily brief for today. |
brief.get(input?) |
Daily brief for a specific local day. |
meetings.upcoming(input?) |
Upcoming calendar meetings and connections. |
meetings.get(documentId) |
Get one meeting. |
meetings.brief(documentId, options?) |
Pre-meeting brief with matched people. |
inbound.channels.create(input) |
Link a phone number for WhatsApp / SMS capture. |
inbound.channels.list(input?) |
List capture channels. |
inbound.channels.get(id) |
Get one capture channel. |
inbound.channels.update(id, input) |
Change a channel's container or disable it. |
inbound.channels.delete(id) |
Remove a capture channel. |
inbound.channels.regenerateCode(id) |
Issue a new verification code. |
memories.merge(input) |
Merge 2..20 memories into one survivor. |
All request and response types are exported from getmnemo.
- Read the Mnemo documentation.
- Report SDK problems in GitHub Issues.
MIT