Python
The xmemory-ai package gives your Python code persistent, structured memory. Write free-form text, have it automatically extracted into typed objects, and query it back in natural language.
For MCP-based integration (no SDK needed), see the MCP guide.
API key: To use xmemory APIs or integrations, you need an API key. Get one from the xmemory console. Copy and securely store the key. Never share your API key publicly.
The full method, data-type, enum, and exception listing is in the Reference at the bottom of this page.
Installation
Section titled “Installation”pip install xmemory-aiRequires Python 3.10+ and pydantic>=2.0.
Quick start
Section titled “Quick start”This is the full flow — from schema to stored knowledge to answers — in a single script:
import yamlfrom xmemory import XmemoryClient, SchemaType
# Connect (reads XMEM_API_KEY from env if api_key is not passed)client = XmemoryClient(api_key="your-api-key")
# List available clustersclusters = client.admin.list_clusters()cluster_id = clusters[0].id
# Describe what you want to rememberschema = client.admin.generate_schema( cluster_id, "Track contacts with name, email, company, and notes.",)
# Create a memory instance from that schemainst = client.admin.create_instance( cluster_id=cluster_id, name="contacts", schema_text=yaml.dump(schema.data_schema), schema_type=SchemaType.YML,)# inst is an InstanceAPI handle — all subsequent calls go through it.
# Write some informationinst.write("Alice Johnson works at Acme Corp. Her email is alice@acme.com.")inst.write("Bob Lee is a designer at Globex. He joined last Monday.")
# Read it backresult = inst.read("What is Alice's email?")print(result.reader_result) # {"answer": "alice@acme.com"}
result = inst.read("Who joined recently?")print(result.reader_result) # {"answer": "Bob Lee joined last Monday."}
client.close()Once you have an instance ID, skip the schema step on subsequent runs:
client = XmemoryClient(api_key="your-api-key")inst = client.instance("your-saved-instance-id")Configuration
Section titled “Configuration”| Parameter | Env var | Default | Description |
|---|---|---|---|
api_key |
XMEM_API_KEY |
None |
API key for authentication |
url |
XMEM_API_URL |
https://api.xmemory.ai |
API base URL |
timeout |
— | 60 |
Default request timeout in seconds |
http_client |
— | None |
External httpx.Client (you manage its lifecycle) |
All parameters are keyword-only. The API key and URL fall back to their environment variables when not passed.
Context manager
Section titled “Context manager”with XmemoryClient(api_key="your-api-key") as client: inst = client.instance("your-instance-id") result = inst.read("What is Alice's email?")Async client
Section titled “Async client”AsyncXmemoryClient exposes the same methods with the same signatures as the sync
client — three things differ, and that’s the whole story:
awaitevery call —await inst.read(...),await client.admin.create_instance(...).aclose()instead ofclose()— or just use the context manager.async withinstead ofwith.
from xmemory import AsyncXmemoryClient
async def main(): async with AsyncXmemoryClient(api_key="your-api-key") as client: inst = client.instance("your-instance-id") result = await inst.read("What is Alice's email?") print(result.reader_result) # exiting `async with` calls aclose() for you (or: await client.aclose())
import asyncioasyncio.run(main())Writing
Section titled “Writing”Send free-form text — xmemory extracts structured objects according to your schema and merges them into the knowledge graph.
inst = client.instance("your-instance-id")
resp = inst.write("Carol is a senior engineer at Initech. Her email is carol@initech.com.")print(resp.changes) # what the write created / updated / deletedprint(resp.trace_id) # request trace id when availableprint(resp.console_url) # this operation's trace in the consoleWriteResult, ReadResult, AsyncWriteResult, WriteStatusResult, and ExtractResult all carry console_url, a direct link to that operation’s trace in the console. All of them except WriteStatusResult also carry trace_id, which is useful when you want to correlate SDK calls with API logs.
console_url points at the call rather than at an individual record, and it is None when the deployment has no console configured, so check it before rendering a link. Requires xmemory-ai>=0.15.0.
Extraction logic
Section titled “Extraction logic”Control the speed/accuracy tradeoff — pass extraction_logic=ExtractionLogic.DEEP to override the FAST default:
| Value | When to use |
|---|---|
FAST |
High-volume, low-stakes writes (default) |
DEEP |
Important or complex information |
Async writes
Section titled “Async writes”For latency-sensitive code, enqueue a write and return immediately:
resp = inst.write_async("Dave manages the London office.")print(resp.write_id) # use this to poll for completionThen check the status:
status = inst.write_status(resp.write_id)print(status.write_status)# → WriteQueueStatus.QUEUED | PROCESSING | EXTRACTING | EXTRACTED | APPLYING | COMPLETED | FAILED | NOT_FOUNDDo not call read immediately after write_async — the data may not be committed yet. Poll with write_status until COMPLETED, or use write (synchronous) when you need to read right after.
Structured writes
Section titled “Structured writes”When you already know exactly what to store, skip extraction: pass structured_mutations instead of text (exactly one of the two — the client raises ValueError otherwise). These are deterministic, LLM-free object creates, updates, and deletes or relation creates and deletes, applied in list order — later mutations may reference objects created earlier in the same batch.
from xmemory import ObjectCreate, ObjectMutation, ObjectUpdate, RelationMutation
resp = inst.write(structured_mutations=[ ObjectMutation( object_type="person", create=ObjectCreate(key={"email": "carol@initech.com"}, values={"name": "Carol"}), ), RelationMutation( relation_type="works_at", create={"endpoints": [ {"object_name": "person", "key": {"email": "carol@initech.com"}}, {"object_name": "company", "key": {"name": "Initech"}}, ]}, ),])print(resp.changes) # exactly what was created / updated / deletedAn ObjectMutation carries exactly one of create / update / delete. key identifies an object by its user-defined primary-key fields, or (for update/delete) by {"xuid": ...}. A None value in object values clears that field. A RelationMutation creates or deletes a relation through its endpoints; this caller-addressing format does not mean object primary keys are required for relations extracted from text. XMD v1 relations do not have scalar fields:
inst.write(structured_mutations=[ ObjectMutation( object_type="person", update=ObjectUpdate(key={"email": "carol@initech.com"}, values={"role": None}), ),])Plain dicts in the API wire form work anywhere a model does — handy for payloads produced by an LLM:
inst.write(structured_mutations=[ {"object_mutation": {"object_type": "person", "delete": {"key": {"email": "carol@initech.com"}}}},])write_async accepts structured_mutations identically — poll with write_status as usual. See the API reference for the full wire format, relation addressing (endpoints, allow_bulk_delete), and in-batch ordering rules.
Scoped writes
Section titled “Scoped writes”A write normally reconciles whatever the extractor produced against every record in the instance — the right default for “here is something new”, the wrong one for “here is more about this record”. Pass a scope to anchor a text write to records you already know about:
from xmemory import ScopeObject, WriteScope
resp = inst.write( "After her promotion she is a surgeon, and her desk phone is +1-555-0100.", scope=WriteScope( objects=[ScopeObject(type="Person", key={"name": "Alice Johnson"})], ),)print(resp.changes) # Alice updated in place, not duplicatedThe scoped objects’ current values are shown to the extractor, so the new information is folded into them instead of producing a near-duplicate; the write is then confined to the scope, free to modify or delete the scoped objects and to create new objects and relations anchored to them, but failing with an XmemoryAPIError if it would touch anything else. The confinement is checked against the resulting plan rather than asked of the extractor, so it holds regardless of what the extraction produced. See Scoped writes for the wire format and the full list of rejections.
WriteScope takes the same ScopeObjects as ReadScope, identified the same way. Scope everything the write should be able to change — a record left out may still be linked to, but not edited:
resp = inst.write( "The Atlas migration finished on Tuesday and Bob Lee signed it off.", scope=WriteScope(objects=[ ScopeObject(type="Project", key={"name": "Atlas"}), ScopeObject(type="Person", key={"name": "Bob Lee"}), ]),)Unlike ReadScope there is no relations_scope: the relations among the scoped objects always accompany the extraction hint, and relation changes are governed by the confinement rule instead of a mode.
write_async accepts the same scope; a scope violation is reported by write_status as a failed write.
Before you reach for it
Section titled “Before you reach for it”- Scope applies to text writes only. Combining it with
structured_mutationsraisesValueErrorin the client, since those bypass extraction and leave a scope nothing to anchor to. - Only objects of a type with a user-defined primary key can be scoped: a scope names records by that key, so a type declared
primary_key: []has nothing to name its records by. - The server accepts a scope with fast extraction only, caps how many objects one scope may name, and can have scoped writes disabled entirely. Those are deployment rules, so they surface as an
XmemoryAPIErrorrather than being second-guessed client-side. - A scoped write additionally requires read permission on the instance, on top of write permission: the scoped objects’ current field values ride the extraction prompt, which makes a scope-carrying write a read of those rows.
Reading
Section titled “Reading”Ask questions in natural language. xmemory translates them into SQL against the knowledge graph and returns a formatted answer.
resp = inst.read("Who works at Acme Corp?")print(resp.reader_result)print(resp.trace_id)Read modes
Section titled “Read modes”from xmemory import ReadMode
# Plain-text answer (default)resp = inst.read("What is Alice's email?", read_mode=ReadMode.SINGLE_ANSWER)# → {"answer": "alice@acme.com"}
# Structured objects and relationsresp = inst.read("Show all contacts", read_mode=ReadMode.XRESPONSE)# → {"objects": [...], "relations": [...]}
# Raw SQL result setsresp = inst.read("List all contacts", read_mode=ReadMode.RAW_TABLES)# → {"columns": [...], "rows": [...]}, or None when nothing matchedComposite questions
Section titled “Composite questions”A question holding several independent parts is answered in one call. The server splits it into sub-questions, answers each on its own, and runs them against one shared snapshot. Nothing has to be enabled or passed.
resp = inst.read("Who owns billing, and when did the last incident close?")
resp.reader_result # the combined answerfor part in resp.reader_results: print(part.sub_query, part.reader_result, part.error)Each part’s reader_result is in the read_mode you asked for, so a RAW_TABLES composite question gives one table per part.
reader_results is always a list — the wire response omits it for a single-intent question and the client normalizes that to [], so an empty list means the question was not split. A part that could not be answered sets its own error and leaves the others intact, so check error per entry rather than assuming the whole call failed.
Related types
Section titled “Related types”Ask with include_related_types=RelatedTypesMode.TYPES and related_types on the result says what else the memory could answer about: the object types the read touched, the fields it did not return, and the object types a declared relation links them to, with a catalog of every named type and relation. The shape is documented on the REST API.
from xmemory import RelatedTypesMode
resp = inst.read("Which courses require an English test?", include_related_types=RelatedTypesMode.TYPES)for touched in resp.related_types.touched: print(touched.object_type, touched.fields_not_returned, [link.object_type for link in touched.related])related_types is None unless the read asked for it. The API key needs instance.get_own, the permission the schema endpoints need; without it the read raises XmemoryAPIError with .status 403 and a message naming the permission. Available from xmemory-ai 0.18.0.
Add related_types_depth=2 (up to 3) to follow the relations further: every entry under related_types.objects then carries its distance from the touched types and, below the last level, its own related edges; related_types.depth echoes the depth asked for and omitted_objects counts what the server’s budgets kept out of the catalog (shape). On a one-level read these fields are absent and read as their defaults (distance is None). Available from xmemory-ai 0.20.0.
deep = inst.read("Which courses require an English test?", include_related_types=RelatedTypesMode.TYPES, related_types_depth=2)print({name: entry.distance for name, entry in deep.related_types.objects.items()})Scoped reads
Section titled “Scoped reads”Restrict a query to a specific set of objects. Useful for multi-tenant systems, privacy controls, and compliance requirements.
from xmemory import ReadScope, ScopeObject
scope = ReadScope( objects=[ ScopeObject(type="Person", key={"name": "Alice Johnson"}) ], relations_scope="no_relations",)resp = inst.read("What is their email?", scope=scope)print(resp.reader_result)Each ScopeObject names one object by its type — the PascalCase class name or the snake_case table name — plus its user-defined primary key, with one entry per primary-key field. Only objects of a type that declares a primary key can be scoped.
Multiple objects with relations
Section titled “Multiple objects with relations”scope = ReadScope( objects=[ ScopeObject(type="Person", key={"email": "alice@example.com"}), ScopeObject(type="Person", key={"email": "bob@example.com"}), ], relations_scope="all_relations",)resp = inst.read("Which projects do these people work on together?", scope=scope)Scope options
Section titled “Scope options”relations_scope takes one of two string values:
"no_relations"(default): Only the specified objects."all_relations": Also expose the relations among the specified objects.
Objects that are not there
Section titled “Objects that are not there”A ScopeObject naming an object the instance does not hold does not raise. It drops out of
the scope, and the read answers from whatever is left — an empty result when nothing matched, or
the objects that do exist when only some did. So you can scope to an object before you know
whether it has been written yet, which is the opposite of a scoped write, where every named
object must exist.
The trade-off: an aggregate is over the objects that were found, not the ones you listed. If it matters that every entry resolved, read the objects back rather than trusting a count over them. See Scoped reads for the full rules, including the malformed scopes that do still raise.
Connecting an instance elsewhere
Section titled “Connecting an instance elsewhere”admin.get_setup_instructions(instance_id) and inst.setup_instructions() both return an
AgentSetupResult: how to reach the same memory from another agent surface, ordered
most-likely-first. On either handle, because the MCP instance connection serves the same tool.
setup = inst.setup_instructions()for surface in setup.surfaces: print(surface.label) for step in surface.steps: print(" ", step.description, step.command or "")The default SetupFormat.AGENT answers what do I run right now, here. SetupFormat.PROJECT
also returns the files a team commits once, so nobody sets the instance up by hand:
from xmemory import SetupFormat
setup = inst.setup_instructions(format=SetupFormat.PROJECT)if setup.format == SetupFormat.PROJECT and setup.project: for fragment in setup.project.fragments: print(fragment.path, fragment.merge) # a merge, never a file to overwriteCheck setup.format rather than assuming. A server older than that parameter ignores it and
still answers 200, so asking for PROJECT is not the same as receiving it.
Nothing returned carries a credential: the steps tell a reader to sign in themselves, out of band, so an instance id stays an identifier rather than a key.
Advisory values — step.kind, fragment.merge, format — arrive as enum members when this
release knows them and as plain strings when it does not, so a value added to the server later
cannot make the whole result unparseable. A step.kind you do not recognise is not something to
execute.
Error handling
Section titled “Error handling”from xmemory import XmemoryAPIError
try: resp = inst.read("...", scope=scope)except XmemoryAPIError as e: if e.status == 400: print(f"Invalid scope: {e.message}") else: raiseExtracting (without writing)
Section titled “Extracting (without writing)”Preview what xmemory would extract from a piece of text, without storing anything:
resp = inst.extract("Dave manages the London office.")print(resp.objects_extracted)print(resp.trace_id)Accepts the same extraction_logic parameter as write.
Describing (agent tool discovery)
Section titled “Describing (agent tool discovery)”The describe() method returns the agent-facing tool descriptions for an instance, with its schema in schema_summary. Use it to tell an LLM what tools are available and how to call them.
desc = inst.describe()
# Plain text — inject into a system promptprint(desc.as_text())
# Anthropic tool-use formattools = desc.as_anthropic_tools()
# OpenAI function-calling formattools = desc.as_openai_tools()Results are cached locally for 5 minutes. To force a refresh (e.g. after updating the schema):
inst.clear_describe_cache()desc = inst.describe()as_text() shows tools as method signatures by default. Pass include_http=True to also show HTTP method and path for raw REST callers.
describe() also carries what the instance is for:
desc.purpose # what the memory is for (the instance description)desc.owner_instructions # the standing preference set for it, verbatimdesc.usage_brief # generated from the schema; None until generatedas_text() includes the first two — the purpose under the instance line, the
standing preference above the schema summary. usage_brief is left out of it,
since it restates the schema summary already there; read the attribute if you
want it.
Both are free text set by anyone holding edit permission on the instance, so
as_text() labels each with where it came from rather than presenting it as the
library’s own words. Those labels state provenance; they are not a security
boundary. If you inject this into a system prompt you are still handling text
you do not control.
Cluster and instance management
Section titled “Cluster and instance management”These methods live on client.admin and cover the control plane: discovering clusters, generating schemas, creating instances, and evolving them safely.
Clusters
Section titled “Clusters”clusters = client.admin.list_clusters()for c in clusters: print(f"{c.id}: {c.name}")Schema generation
Section titled “Schema generation”Describe what you want to track in plain language when generation is more convenient than writing XMD directly. Agents may use either path:
schema = client.admin.generate_schema( cluster_id, "Track user preferences, open tasks with priorities, and conversation history.",)print(schema.data_schema)Object names use CamelCase (UserPreferences, OpenTask). The generation endpoint handles naming conventions automatically. The returned document is XMD, whose descriptions fine-tune extraction and permit grounded derivation rather than merely defining domain terminology. To evolve an existing schema (rename / remove / type change) rather than create one, retrieve the current XMD and use enhance_schema — it returns a structured migration plan; see Schema evolution.
Instances
Section titled “Instances”Create an instance from a generated schema:
import yamlfrom xmemory import SchemaType
inst = client.admin.create_instance( cluster_id=cluster_id, name="my-memory", schema_text=yaml.dump(schema.data_schema), schema_type=SchemaType.YML, description="Optional description",)# inst is a bound InstanceAPI — use inst.write(), inst.read(), etc.For a purely additive change (new objects/fields/relations), update the schema directly — existing data is preserved:
new_schema = client.admin.generate_schema( cluster_id, "Add an assignee field to tasks.", current_yml_schema=current_schema,)client.admin.update_instance_schema( instance_id, yaml.dump(new_schema.data_schema), SchemaType.YML)List and delete instances:
instances = client.admin.list_instances()info = client.admin.get_instance(instance_id)
client.admin.delete_instance(instance_id)Agent-facing instance metadata
Section titled “Agent-facing instance metadata”An instance can carry metadata that shapes how agents connect to it and what they
do with it. Use patch_instance_metadata to set the advisory hints: every
argument is independent — omitting one leaves the stored value untouched,
and passing None clears it.
from xmemory import AgentSurface, BindingTier
client.admin.patch_instance_metadata( instance_id, # Advisory hints — they seed what a connect flow proposes, and grant nothing. agent_surfaces=[AgentSurface.CLAUDE_CODE, AgentSurface.CODEX], agent_default_binding_tier=BindingTier.AUTOLOAD, agent_engagement_hints=["a convention is learned or corrected"],)Concurrent edits to these three are last-writer-wins by design: they only seed
what a connect flow proposes, so the loser of a race re-applies a suggestion.
agent_owner_instructions is not like that.
Read it back from InstanceInfo. The hints come back as plain strings, so a
value your server knows and your library version does not is returned rather
than rejected:
info = client.admin.get_instance(instance_id)info.agent_owner_instructionsinfo.agent_surfaces # e.g. ["claude_code", "codex"]info.agent_default_binding_tier # e.g. "autoload"Set agent_owner_instructions through update_instance_metadata, not
patch_instance_metadata. It is rendered to agents verbatim and a second
writer edits it from the same screen, so a silently lost edit is a rule that
stops being enforced — and update_instance_metadata is the only call that
carries expected_owner_instructions_epoch. Pass the epoch you read the value
at and the losing save is refused instead of applied:
info = client.admin.get_instance(instance_id)client.admin.update_instance_metadata( instance_id, info.name, info.description, agent_owner_instructions=(info.agent_owner_instructions or "") + "\nAlso: never paraphrase a rule.", expected_owner_instructions_epoch=info.agent_owner_instructions_epoch,)Schema evolution
Section titled “Schema evolution”Non-additive changes (rename / remove / type change) preserve data via a structured migration plan. Use enhance_schema to get the plan, dry_run_migration to preview the DDL, then update_instance_schema to apply it. confirm_destructive=True is required for ops that drop data.
current = client.admin.get_instance_schema(instance_id).data_schema
# 1. Enhance — new schema + an executor-ready migration plan.enhanced = client.admin.enhance_schema( cluster_id, "Rename Person.mail to Person.email.", yaml.dump(current),)print(enhanced.summary)for op in enhanced.migration_plan.ops: print(op)
new_yaml = yaml.dump(enhanced.data_schema)
# 2. Dry-run — preview the DDL, apply nothing.preview = client.admin.dry_run_migration( instance_id, new_yaml, SchemaType.YML, migration_plan=enhanced.migration_plan,)print(preview.statements)
# 3. Update — apply. A rename is non-destructive, so confirm_destructive stays False.info = client.admin.update_instance_schema( instance_id, new_yaml, SchemaType.YML, migration_plan=enhanced.migration_plan, confirm_destructive=False,)print(info.migration_id, info.prior_version, "->", info.new_version)Review applied migrations:
page = client.admin.list_migrations(instance_id, limit=20)for r in page.items: print(r.id, r.source, r.prior_version, "->", r.new_version)
detail = client.admin.get_migration(instance_id, page.items[0].id, include_yaml=True)print(detail.yaml_before, detail.yaml_after)Migration ops are exported as typed models (MigrationPlan, AddField,
RenameField, RemoveObject, …). ProposalItem.op and MigrationRecord.ops
stay as raw dicts for forward compatibility — use parse_migration_op(...) to
validate them.
Suggestion engine (review → decide → apply)
Section titled “Suggestion engine (review → decide → apply)”xmemory watches read traffic and, on demand, surfaces a single rolling proposal of schema improvements. The flow is three calls — never wrap them; the checkpoints are the point.
from xmemory import DecisionInput
inst = client.instance(instance_id)
# 1. Review — proposal + optimistic-concurrency token.review = inst.review_suggestions()if review.status == "evolution_in_progress": print(f"Migration in flight; retry in {review.retry_after_seconds}s")else: proposal = review.proposal for item in proposal.items: print(item.item_fingerprint, item.rationale, item.op)
# 2. Decide — accept / reject / defer per item, in bulk. decided = inst.decide_suggestions( proposal.proposal_version, [DecisionInput(item_fingerprint=i.item_fingerprint, decision="accept") for i in proposal.items], )
# 3. Apply — commit accepted decisions as one migration. applied = inst.apply_pending_decisions(decided.next_proposal_version) print(applied.status, applied.summary)Error handling
Section titled “Error handling”All errors raise XmemoryAPIError. The exception carries an optional .status (HTTP status code), .code (structured error code, when the server returned one), .details, and .retry_after (int seconds parsed from the Retry-After header, >= 0, or None).
from xmemory import XmemoryAPIError, XmemoryHealthCheckError
# Check connectivitytry: client.check_health()except XmemoryHealthCheckError as e: print(f"API unreachable: {e}")
# Handle operation errorstry: inst.write("...")except XmemoryAPIError as e: print(f"Error (HTTP {e.status}): {e}")XmemoryHealthCheckError is a subclass of XmemoryAPIError, so catching XmemoryAPIError covers both.
The schema-evolution methods set .code to a structured error type you can match on — e.g. stale_proposal_version, destructive_confirmation_required, non_additive_change_requires_plan:
try: inst.apply_pending_decisions(token)except XmemoryAPIError as e: if e.code == "stale_proposal_version": review = inst.review_suggestions() # re-review and retryThe same .code branching separates a spent allowance from a genuine rate limit — match on .code, not on the status:
try: inst.write("...")except XmemoryAPIError as e: if e.code == "QUOTA_EXCEEDED": # Plan/usage allowance exhausted; e.details["kind"] is # "daily_quota_exceeded" or "monthly_quota_exceeded". # Not retryable now — surface "limit reached / upgrade your plan". # You may retry after e.details["retry_after_seconds"] (if not None). ... elif e.code == "RATE_LIMITED": # Genuine rate limit — back off and retry, honoring Retry-After. ... else: raiseFor both RATE_LIMITED (429) and a resettable QUOTA_EXCEEDED (402), use e.retry_after — the seconds parsed from the Retry-After header (an int >= 0, or None when the header was absent or in HTTP-date form) — as your backoff delay.
Reference
Section titled “Reference”The complete method, data-type, enum, and exception listing. Methods are grouped by where they live; expand a row for its parameters, returns, and details.
Client
Section titled “Client”Constructors and client-level handles.
XmemoryClientCreate a sync client
XmemoryClient(url=None, *, timeout=60, api_key=None, http_client=None)Parameters
urlstrXMEM_API_URL. The only positional arg. Defaults to None.timeoutfloat60.api_keystrXMEM_API_KEY. Defaults to None.http_clienthttpx.ClientNone.AsyncXmemoryClientCreate an async client (same signature; methods are awaitable)
AsyncXmemoryClient(url=None, *, timeout=60, api_key=None, http_client=None)Parameters
urlstrXMEM_API_URL. The only positional arg. Defaults to None.timeoutfloat60.api_keystrXMEM_API_KEY. Defaults to None.http_clienthttpx.AsyncClientNone.adminControl-plane accessor — clusters, schema generation, instances, schema evolution
client.admin→AdminAPIReturns
client.admin (AsyncAdminAPI on the async client) — the receiver for the admin / control-plane methods (clusters, schema generation, instances, schema evolution).
instanceGet a handle for data operations on an instance
client.instance(instance_id)→InstanceAPIParameters
instance_idstrrequiredReturns
A bound InstanceAPI handle (AsyncInstanceAPI on the async client) — call write / read / extract / … on it.
check_healthRaise XmemoryHealthCheckError if the API is unreachable
client.check_health()Returns
None on success; raises XmemoryHealthCheckError otherwise.
closeClose the underlying HTTP client (no-op if you supplied your own)
client.close()Details
Async client: use await client.aclose() instead — or prefer the context manager (with / async with).
acloseAsync counterpart of close() (AsyncXmemoryClient)
await client.aclose()Admin methods
Section titled “Admin methods”Control-plane methods on client.admin — clusters, schema generation, instances, and schema evolution.
list_clustersList clusters
Parameters
idslist[str]timeoutfloatget_clusterGet a cluster by ID
Parameters
cluster_idstrrequiredtimeoutfloatgenerate_schemaGenerate a schema from a plain-language description
generate_schema(cluster_id, schema_description, *, current_yml_schema=None, timeout=None)→GenerateSchemaResultParameters
cluster_idstrrequiredschema_descriptionstrrequiredcurrent_yml_schemastrtimeoutfloatenhance_schemaEvolve a schema → new schema + migration plan
enhance_schema(cluster_id, schema_description, current_yml_schema, *, timeout=None)→EnhanceSchemaResultParameters
cluster_idstrrequiredschema_descriptionstrrequiredcurrent_yml_schemastrrequiredtimeoutfloatReturns
Pass the returned migration_plan to dry_run_migration / update_instance_schema.
create_instanceCreate a new instance; returns a bound handle
create_instance(cluster_id, name, schema_text, schema_type, *, description=None, schema_description=None, timeout=None)→InstanceAPIParameters
cluster_idstrrequirednamestrrequiredschema_textstrrequiredYML or JSON).descriptionstrschema_descriptionstrtimeoutfloatReturns
A bound InstanceAPI handle for the new instance — call inst.id to get its ID.
list_instancesList instances
Parameters
idslist[str]timeoutfloatget_instanceGet an instance by ID
Parameters
instance_idstrrequiredtimeoutfloatget_instance_schemaGet an instance's schema
Parameters
instance_idstrrequiredtimeoutfloatupdate_instance_schemaUpdate a schema; pass migration_plan for non-additive changes
update_instance_schema(instance_id, schema_text, schema_type, *, migration_plan=None, confirm_destructive=False, timeout=None)→InstanceInfoParameters
instance_idstrrequiredschema_textstrrequiredYML or JSON).migration_planMigrationPlan | dictenhance_schema).confirm_destructiveboolFalse.timeoutfloatReturns
The migration_id / prior_version / new_version / migration_warnings fields are populated when a migration ran.
Errors
Raises XmemoryAPIError with .code non_additive_change_requires_plan (no plan) or destructive_confirmation_required (drops data without confirm_destructive=True).
update_instance_metadataUpdate instance name and description
update_instance_metadata(instance_id, name, description, *, agent_owner_instructions=UNSET, expected_owner_instructions_epoch=UNSET, timeout=None)→InstanceInfoParameters
instance_idstrrequirednamestrrequireddescriptionstr | NonerequiredNone to clear).agent_owner_instructionsstr | NoneNone to clear it. Max 2000 characters. Defaults to UNSET.expected_owner_instructions_epochintagent_owner_instructions_epoch your edit was composed from. Defaults to UNSET.timeoutfloatReturns
Omitting an argument sends no such key at all, so a rename never disturbs the standing preference.
Errors
When expected_owner_instructions_epoch is sent and the stored instructions changed after you read them, the save is refused rather than applied: XmemoryAPIError with .status 409 and .code INVALID_INPUT. Re-read the instance, reapply your change, and save again.
patch_instance_metadataChange some metadata fields, leaving the rest alone
patch_instance_metadata(instance_id, *, name=UNSET, description=UNSET, agent_surfaces=UNSET, agent_default_binding_tier=UNSET, agent_engagement_hints=UNSET, agent_owner_instructions=UNSET, timeout=None)→InstanceInfoParameters
instance_idstrrequirednamestrUNSET.descriptionstr | NoneNone to clear). Defaults to UNSET.agent_surfacesSequence[str] | NoneAgentSurface. Advisory: it orders the connect steps and grants nothing. Defaults to UNSET.agent_default_binding_tierstr | NoneBindingTier. Advisory. Defaults to UNSET.agent_engagement_hintsSequence[str] | None"a convention is learned or corrected". At most 16, each at most 200 characters. Defaults to UNSET.agent_owner_instructionsstr | Noneupdate_instance_metadata for this field — it is the only call that accepts expected_owner_instructions_epoch, so an edit sent here cannot be guarded against a concurrent one. Defaults to UNSET.timeoutfloatReturns
Every argument is independent: omit one and the stored value is untouched, pass None to clear it. This is the only call that accepts the three agent_* hints; update_instance_metadata is the only one that accepts the epoch guard.
get_setup_instructionsHow to connect this instance on an agent surface, most likely first
get_setup_instructions(instance_id, *, format=SetupFormat.AGENT, timeout=None)→AgentSetupResultParameters
instance_idstrrequiredformatSetupFormat | strAGENT returns the steps to run now. PROJECT additionally returns the files a team commits once, so nobody sets the instance up by hand. Defaults to SetupFormat.AGENT.Details
Answers "how do I also reach this memory somewhere else" — a desktop client, another
editor, a teammate's machine — rather than how to reach it from here. The same payload
the get_setup_instructions MCP tool serves and xmemcli instance setup prints.
Also on an instance handle as instance.setup_instructions(), without the id.
Computed from the instance's current metadata, so editing its surface hints changes the next response.
Carries no credential. The steps tell a reader to sign in themselves, out of band, so nothing returned is a secret and an instance id stays an identifier rather than a key.
Read result.format rather than assuming: a server older than the format parameter
ignores it and still answers 200, so asking for PROJECT is not the same as receiving it.
Advisory values tolerate a newer server. step.kind, fragment.merge and format come
back as enum members when this release knows them and as plain strings when it does not,
so a value added later cannot make the whole result unparseable. A step.kind you do not
recognise is not something to execute.
delete_instanceDelete an instance
delete_instance(instance_id, *, timeout=None)→list[str]Parameters
instance_idstrrequiredtimeoutfloatReturns
The deleted instance IDs.
dry_run_migrationPreview a migration's DDL
dry_run_migration(instance_id, schema_text, schema_type, *, migration_plan=None, confirm_destructive=False, timeout=None)→DryRunResultParameters
instance_idstrrequiredschema_textstrrequiredYML or JSON).migration_planMigrationPlan | dictconfirm_destructiveboolFalse.timeoutfloatReturns
statements are the planned DDL; nothing is applied.
list_migrationsList applied migrations (newest first)
list_migrations(instance_id, *, limit=50, before_id=None, include_yaml=False, timeout=None)→ListMigrationsResultParameters
instance_idstrrequiredlimitint50.before_idstrnext_before_id.include_yamlboolFalse.timeoutfloatReturns
Page with next_before_id — pass it as before_id to fetch the next page.
get_migrationGet a single migration record
Parameters
instance_idstrrequiredmigration_idstrrequiredinclude_yamlboolFalse.timeoutfloatReturns
yaml_before / yaml_after are populated only when include_yaml=True.
Instance methods
Section titled “Instance methods”Data-plane methods on a bound instance (client.instance(id)) — writing, reading, extracting, describing, and the suggestion engine.
writePersist data — extract objects from text, or apply structured mutations
write(text="", *, structured_mutations=None, extraction_logic=ExtractionLogic.FAST, diff_engine=None, scope=None, timeout=None)→WriteResultParameters
textstrtext / structured_mutations must be provided (enforced with ValueError).structured_mutationslistObjectMutation / RelationMutation models (or equivalent wire-form dicts): deterministic object create/update/delete and relation create/delete mutations — see Structured writes. Mutually exclusive with text.extraction_logicExtractionLogicFAST default, or DEEP). Text writes only. Defaults to ExtractionLogic.FAST.diff_engineboolscopeWriteScope | Nonestructured_mutations raises ValueError); the server accepts it with fast extraction only, and it additionally requires read permission on the instance. See Scoped writes.timeoutfloatwrite_asyncEnqueue a write; returns write_id immediately
write_async(text="", *, structured_mutations=None, extraction_logic=ExtractionLogic.FAST, diff_engine=None, scope=None, timeout=None)→AsyncWriteResultParameters
textstrtext / structured_mutations must be provided.structured_mutationslistwrite. Mutually exclusive with text.extraction_logicExtractionLogicFAST default, or DEEP). Text writes only. Defaults to ExtractionLogic.FAST.diff_engineboolscopeWriteScope | Nonestructured_mutations raises ValueError); the server accepts it with fast extraction only, and it additionally requires read permission on the instance. See Scoped writes.timeoutfloatReturns
Poll for completion with write_status using the returned write_id.
write_statusPoll the status of an async write
Parameters
write_idstrrequiredwrite_async.timeoutfloatreadQuery the instance in natural language
read(query, *, read_mode=ReadMode.SINGLE_ANSWER, scope=None, include_related_types=None, related_types_depth=None, read_id=None, timeout=None)→ReadResultParameters
querystrrequiredread_modeReadModeSINGLE_ANSWER default, XRESPONSE, or RAW_TABLES). Defaults to ReadMode.SINGLE_ANSWER.scopeReadScope | Noneinclude_related_typesRelatedTypesMode | NoneRelatedTypesMode.TYPES asks for related_types on the result. Sent only when set. Needs instance.get_own on the API key. See Related types.related_types_depthint | NoneRelatedTypesMode.TYPES, how many relation levels related_types.objects follows out from the touched types, 1 to 3. Sent only when set. See Related types.read_idstrtimeoutfloatReturns
reader_result is shaped by read_mode (e.g. {"answer": ...}).
A question holding several independent parts is split server-side into sub-questions, and reader_results then holds one entry per sub-question — each { sub_query, reader_result, error }, with every reader_result in the requested mode. reader_result stays the combined value. Unlike the REST response, which omits the key entirely for a single-intent question, the client always gives you a list here: it is empty unless the question was split. A sub-question that could not be answered sets its own error and leaves the others intact.
related_types is a RelatedTypes when the read asked for it with include_related_types, and None otherwise. See Related types.
extractExtract objects without writing them
Parameters
textstrrequiredextraction_logicExtractionLogicFAST default, or DEEP). Defaults to ExtractionLogic.FAST.timeoutfloatdescribeGet agent-facing tool descriptions (cached 5 min)
Parameters
timeoutfloatclear_describe_cacheForce next describe() to fetch fresh data
clear_describe_cache()→Noneget_schemaGet this instance's schema
Parameters
timeoutfloatsetup_instructionsHow to connect *this* instance somewhere else, most likely surface first
setup_instructions(*, format=SetupFormat.AGENT, timeout=None)→AgentSetupResultParameters
formatSetupFormat | strAGENT returns the steps to run now; PROJECT also returns the files a team commits once. Defaults to SetupFormat.AGENT.Details
The instance-scoped counterpart of admin.get_setup_instructions(), which takes an id.
On both because the MCP instance connection serves the same tool, so a caller holding an
instance handle need not reach through admin for it.
Not cached, unlike describe(): computed from the instance's current metadata, so an
owner who edits a surface hint expects the next call to show it.
review_suggestionsGet the rolling schema-improvement proposal
Parameters
session_idstrtimeoutfloatReturns
On status evolution_in_progress, back off for retry_after_seconds and retry — it's a status, not an error.
decide_suggestionsRecord accept/reject/defer decisions in bulk
decide_suggestions(proposal_version, decisions, *, session_id=None, timeout=None)→DecideSuggestionsResultParameters
proposal_versionstrrequiredDecisionInput(item_fingerprint, decision, edits=None).session_idstrtimeoutfloatReturns
Pass next_proposal_version straight to apply_pending_decisions without re-reviewing.
Errors
Raises XmemoryAPIError with .code stale_proposal_version (token out of date) or invalid_decision_input.
apply_pending_decisionsApply accepted decisions as one migration
apply_pending_decisions(proposal_version, *, session_id=None, timeout=None)→ApplyPendingDecisionsResultParameters
proposal_versionstrrequirednext_proposal_version from the decision step.session_idstrtimeoutfloatReturns
status nothing_to_apply means no accepted items were left to apply.
Errors
Raises XmemoryAPIError with .code stale_proposal_version, dependency_closure_failed, or apply_failed.
Data types
ClusterInfoA cluster.
Fields
idstrorg_idstrnamestrdescriptionstr | NoneInstanceInfoAn instance's metadata.
Fields
idstrcluster_idstrnamestrdescriptionstr | Nonedata_schemadict | Noneagent_surfaceslist[str] | NoneNone means no opinion. Read as plain strings, so a value newer than your library version is returned rather than rejected.agent_default_binding_tierstr | Noneautoload / available). Advisory.agent_engagement_hintslist[str] | Noneagent_owner_instructionsstr | Noneagent_owner_instructions_epochintexpected_owner_instructions_epoch to make an edit conflict-safe.migration_idstr | Noneupdate_instance_schema when a migration ran.prior_versionint | Nonenew_versionint | Nonemigration_warningslist[str] | NoneInstanceSchemaInfoAn instance's schema.
Fields
data_schemadictWriteResultResult of a synchronous write.
Fields
write_idstrchangesAnycreated / updated / deleted (null on responses from an older server).trace_idstr | NoneAsyncWriteResultAcknowledgement of an enqueued write.
Fields
write_idstrwrite_status.WriteStatusResultStatus of an async write.
Fields
write_idstrwrite_statusWriteQueueStatuserror_detailstr | Nonewrite_status is FAILED.completed_atdatetime | NoneScopeObjectOne concrete object a read or write scope may touch.
Fields
typestrkeydictrequiredReadScopeA read's scope.
Fields
objectslist[ScopeObject]relations_scopestr"no_relations" (default) or "all_relations", which also exposes the relations among the scoped objects.WriteScopeA write's scope.
Fields
objectslist[ScopeObject]ReadResultResult of a read.
Fields
reader_resultAnyread_mode (e.g. {"answer": ...}).trace_idstr | Nonerelated_typesRelatedTypes | Noneinclude_related_types; None otherwise.RelatedTypesWhat else the memory could answer about, for a read that asked with `include_related_types`. The shape is documented on the [REST API](/api/#related-types).
Fields
touchedlist[RelatedTypesTouched]{ object_type, fields_not_returned, related, omitted_related } with related a list of RelatedTypesLink (object_type, relation, touched_role, related_role, cardinality). Empty when the read executed nothing.objectsdict[str, RelatedTypesObjectType]touched or reached within depth relation levels, once: description, primary_key, fields, and — on a read that asked for a depth above 1 — distance (listed edges from the nearest touched type; None otherwise), its own related edges below the last level, and omitted_related.relationsdict[str, RelatedTypesRelation]description.depthintrelated_types_depth; 1 when unset). Never lowered by the server: what its budgets kept the walk from listing is counted on omitted_objects.omitted_touchedintomitted_objectsintdepth relation levels in the schema that the catalog does not list: the route to them ran through an edge the server's budget cut, or their level was dropped for its byte budget.truncatedboolTrue when any type, edge or level was dropped for the budget.ExtractResultResult of an extract (nothing persisted).
Fields
objects_extractedAnyobjects / relations).trace_idstr | NoneDescribeResultAgent-facing tool descriptions, with `as_text(include_http=False)`, `as_anthropic_tools()`, and `as_openai_tools()` format helpers.
Fields
instance_idstrinstance_namestrschema_summarystrtoolslist[ToolDescription]purposestr | Noneas_text().owner_instructionsstr | Noneas_text(), labelled with where it came from rather than as the library's own words.usage_briefstr | NoneNone until generated, and cleared again by a schema change. Left out of as_text(), since it restates schema_summary.GenerateSchemaResultA generated schema.
Fields
data_schemadictEnhanceSchemaResultAn evolved schema plus an executor-ready migration plan.
Fields
data_schemadictmigration_planMigrationPlan | Noneupdate_instance_schema / dry_run_migration.summarystr | Nonewarningslist[dict]repair_loglist[dict]DryRunResultPlanned migration DDL, with nothing applied.
Fields
status"ok""ok" for a successful dry run.instance_idstrcurrent_versionintstatementslist[str]warningslist[str]plan_summaryPlanSummaryrequires_metadata_syncboolMigrationRecordOne applied-migration history row.
Fields
idstrapplied_atdatetimesourcestrdirect or suggestion_engine.decided_bystr | Noneprior_versionintnew_versionintopslist[dict]ops_summaryPlanSummarynotesstr | Noneyaml_beforestr | Noneinclude_yaml=True).yaml_afterstr | Noneinclude_yaml=True).ListMigrationsResultA page of migration records, newest first.
Fields
status"ok""ok".instance_idstritemslist[MigrationRecord]next_before_idstr | NoneNone at the tail.has_moreboolReviewSuggestionsResultThe rolling schema-improvement proposal (or an in-progress status).
Fields
status"ok" | "evolution_in_progress"evolution_in_progress means a migration is in flight.instance_idstrproposalConsolidatedProposal | Nonestatus is "ok".retry_after_secondsint | Noneevolution_in_progress.DecideSuggestionsResultResult of recording a batch of decisions.
Fields
status"ok""ok".instance_idstrdecisions_recordedlist[RecordedDecision]warningslist[DependencyWarning]next_proposal_versionstrapply_pending_decisions.ApplyPendingDecisionsResultResult of applying accepted decisions as one migration.
Fields
status"ok" | "nothing_to_apply"nothing_to_apply when no accepted items remained.instance_idstrmigration_idstr | Noneprior_versionintnew_versionintapplied_itemslist[str]summarystrwarningslist[str]noteslist[str]MigrationPlanAn ordered sequence of migration ops — the wire format emitted by `enhance_schema` and consumed by `update_instance_schema` / `dry_run_migration`.
Fields
opslist[MigrationOp]MigrationOpOne migration operation — a discriminated union keyed on `op_type`.
Fields
op_typestradd_object, remove_object, rename_object, change_object, add_field, remove_field, rename_field, change_field, add_relation, remove_relation, rename_relation, change_relation — each variant carries operation-specific parameters.PlanSummaryPer-op-type breakdown of a migration plan.
Fields
count_by_op_typedict[str, int]op_type.totalintConsolidatedProposalThe single rolling improvement proposal for an instance.
Fields
instance_idstrproposal_versionstrdecide_suggestions / apply_pending_decisions.schema_versionintitemslist[ProposalItem]generated_atdatetimenoteslist[str]ProposalItemOne decide-able item inside a proposal.
Fields
item_fingerprintstrDecisionInput.opdictparse_migration_op).evidence_feedback_idslist[str]evidence_query_sampleslist[str]frequencyintdepends_onlist[str]current_decisionstr | NonerationalestrDecisionInputOne decision in a `decide_suggestions` batch.
Fields
item_fingerprintstrProposalItem to decide on.decisionstraccept, reject, defer.editsdict | Noneop_type).RecordedDecisionA persisted decision.
Fields
item_fingerprintstrdecision_idstrDependencyWarningAn advisory (non-blocking) dependency warning from `decide_suggestions`.
Fields
kindstritem_fingerprintstrrelated_fingerprintslist[str]related_summarieslist[str]guidancestrToolDescriptionOne agent-callable tool in a DescribeResult.
Fields
namestrdescriptionstrwhen_to_usestrparameterslist[ToolParameterDescription]http_methodstrhttp_pathstrToolParameterDescriptionOne parameter of a ToolDescription.
Fields
namestrtypestrdescriptionstrrequiredboolenumlist[str] | Nonedefaultstr | NoneEnums
| Enum | Values |
|---|---|
SchemaType | YML, JSON |
ExtractionLogic | FAST, DEEP |
ReadMode | SINGLE_ANSWER, RAW_TABLES, XRESPONSE |
RelatedTypesMode | NONE, TYPES |
RelationCardinality | ONE_TO_ONE, ONE_TO_MANY, MANY_TO_ONE, MANY_TO_MANY, UNCONSTRAINED |
WriteQueueStatus | QUEUED, PROCESSING, EXTRACTING, EXTRACTED, APPLYING, COMPLETED, FAILED, NOT_FOUND |
Exceptions
| Exception | Parent | Attributes |
|---|---|---|
XmemoryAPIError | Exception | .status, .code, .details, .retry_after (each None when absent) |
XmemoryHealthCheckError | XmemoryAPIError | .status (HTTP status code or None) |