Declaring Memory Types
SmartMemory ships with built-in memory types (semantic, episodic, procedural, zettel, and the extended and structured types). When your application has its own record shape, such as a support ticket or a lab result, you can declare it as a memory type of your own and SmartMemory will validate every write against the schema you gave it.
A declared type is an ontology class with kind="record". The ontology is the schema: there is no second registry and no separate type system to learn.
The core idea
An item's memory_type is the class name. When you declare a class called SupportTicket, you write items with memory_type: "SupportTicket" and SmartMemory resolves the class, checks the item against its schema, and stores it.
Two consequences are worth knowing up front:
- Built-in types cannot be shadowed. Writes resolve against the process registry first, so declaring a class named
semanticwill not override the built-in behaviour. - Declarations are per workspace. A class declared in one workspace is invisible to another, which is what makes declared types safe in a multi-tenant deployment.
Declaring a type
The declaration surface is POST /memory/ontology/types, with both SDKs wrapping it.
curl -X POST "$SMARTMEMORY_API_URL/memory/ontology/types" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Workspace-Id: $WORKSPACE_ID" \
-H "Content-Type: application/json" \
-d '{
"name": "SupportTicket",
"kind": "record",
"properties_schema": {
"subject": {"type": "string", "indexed": true},
"severity": {"type": "integer"},
"opened_at":{"type": "datetime"},
"tags": {"type": "list", "of": "string"},
"resolved": {"type": "boolean", "default": false}
},
"required_properties": ["subject", "severity"],
"storage_strategy": "full",
"storage_searchable": true
}'The same call from the Python SDK:
from smartmemory_client import SmartMemoryClient
client = SmartMemoryClient(api_key=..., workspace_id=...)
client.declare_type(
"SupportTicket",
kind="record",
properties_schema={
"subject": {"type": "string", "indexed": True},
"severity": {"type": "integer"},
"opened_at": {"type": "datetime"},
"tags": {"type": "list", "of": "string"},
"resolved": {"type": "boolean", "default": False},
},
required_properties=["subject", "severity"],
storage_strategy="full",
storage_searchable=True,
)And from the JavaScript SDK:
await client.ontology.declareType({
name: 'SupportTicket',
kind: 'record',
propertiesSchema: {
subject: { type: 'string', indexed: true },
severity: { type: 'integer' },
opened_at: { type: 'datetime' },
tags: { type: 'list', of: 'string' },
resolved: { type: 'boolean', default: false },
},
requiredProperties: ['subject', 'severity'],
storageStrategy: 'full',
storageSearchable: true,
});Declaration fields
| Field | Default | Purpose |
|---|---|---|
name | required | Class name. For kind="record" this is the value you write as memory_type |
kind | entity | entity for extraction vocabulary, record for a concrete record class |
properties_schema | none | Map of field name to field spec (see below) |
required_properties | none | Field names that must be present on every item |
storage_strategy | none | full, indexed, or append. Record classes only |
storage_searchable | none | Whether items are embedded for vector search by default. Record classes only |
tier | confirmed | Governance tier. Use working to route the class through review instead of taking effect immediately |
display_name, definition, description, aliases, examples, parent_types, iri, and wikidata_qid are also accepted and are descriptive only.
Field specs
Each entry in properties_schema maps a field name to a spec:
| Key | Values | Meaning |
|---|---|---|
type | string, integer, number, boolean, datetime, object, list | Required. The field's type |
of | string, integer, number, boolean, datetime, object | Element type, for list fields only |
default | any JSON value | Applied when the field is absent. Must be compatible with type |
indexed | true or false | Creates a database index on the field |
append_only | true or false | The field may be set once and never changed |
Whether a field is required is expressed in required_properties, not in the field spec.
Lists of scalars are stored as native array values. Lists of objects are stored JSON serialised, and per-element schemas are not validated.
Storage strategy
storage_strategy controls how items of the class are persisted:
fullstores the item and, whenstorage_searchableis true, embeds it for vector search.indexedstores the item with indexed fields for structured lookup.appendis an append-only log shape. Items written withappendare never embedded, regardless ofstorage_searchable, so do not expect them in vector search results.
Writing items
Once the class exists, write items exactly as you would for a built-in type, using the class name as memory_type:
client.add(
content="Checkout fails on the payment step for EU customers.",
memory_type="SupportTicket",
metadata={
"subject": "Checkout failure",
"severity": 2,
"opened_at": "2026-08-07T09:00:00Z",
"tags": ["billing", "eu"],
},
)Validation is strict by default
Writes are checked against the declared schema. A missing required field, a wrong type, or a value that violates append_only is rejected with a 400, and nothing is stored. This is deliberate: a silently dropped field is far more expensive to discover later than a rejected write.
If you need the transitional behaviour instead, setting SMARTMEMORY_STRICT_SCHEMA_VALIDATION=false downgrades violations to warnings. Treat this as a migration aid rather than a steady state, because it lets malformed records into the store.
Updates are revalidated on the same rules, so a field cannot be corrupted after the fact.
Declaring relations
Relations between classes are declared with POST /memory/ontology/relations (declare_relation in Python, declareRelation in JavaScript). A declared relation carries a domain, a range, and a cardinality, and links are validated against them when they are created.
As with schema validation, relation validation is strict by default and SMARTMEMORY_STRICT_RELATION_VALIDATION=false downgrades it to warnings.
Lifecycle
Declared classes are governed rather than simply deleted, so that history stays auditable.
Counting. GET /memory/ontology/types/{type_id} returns the class, including an instance_count of the items currently held by it. The count resolves the effective class, so after a merge, asking about a merged-away name answers for the surviving class.
To list only your record classes, pass the kind filter: GET /memory/ontology/types?kind=record.
Migrating. POST /memory/ontology/types/{from_id}/migrate-to/{to_id} moves instances from one class to another:
client.migrate_ontology_type_instances(
"SupportTicket",
"Ticket",
reason="Consolidating on the shorter class name",
on_violation="refuse",
)on_violation controls what happens when a source item does not satisfy the destination schema:
refuse(the default) validates every source item before any rewrite and refuses the whole migration if any item fails. There is no partial state.skipmigrates the conforming items and returns the ids it skipped.
Migrations between classes of different kinds are refused. Flipping a declared class to a built-in type, or the reverse, is also refused. Use a migration instead.
Merging. Merging two record classes refuses if the source class still holds items, so a merge can never strand data.
Retiring. POST /memory/ontology/types/{type_id}/retire retires a class. The route accepts private record classes only, so a shared or built-in class cannot be retired out from under other callers. Entity classes are retired through the ontology curation queue instead.
client.retire_ontology_type("SupportTicket", reason="Superseded by Ticket")A reason is required on both migration and retirement, and is recorded as audit evidence. The REST surface also accepts superseded_by on retirement to record an explicit redirect to a surviving class.
Portability
Exported OKF bundles carry your class declarations, not only your items. A bundle exported from one workspace therefore imports cleanly into a fresh workspace, because the declarations arrive with the data and the destination can validate what it is being given.
Imports are staged, and a conflict with an existing declaration refuses the whole import rather than merging silently. Older v0.1 bundles, which carry items only, still import.
See OKF Portability for the bundle format.
API reference
| Route | Python SDK | JavaScript SDK |
|---|---|---|
POST /memory/ontology/types | declare_type | declareType |
GET /memory/ontology/types | list_ontology_types | listTypes |
GET /memory/ontology/types/{id} | get_ontology_type | getType |
GET /memory/ontology/types/{id}/audit | get_ontology_type_audit | getTypeAudit |
POST /memory/ontology/types/{from}/migrate-to/{to} | migrate_ontology_type_instances | migrateTypeInstances |
POST /memory/ontology/types/{id}/retire | retire_ontology_type | retireType |
POST /memory/ontology/relations | declare_relation | declareRelation |
All routes require Authorization: Bearer <token> and X-Workspace-Id.
Related
- Memory Types for the built-in types
- Ontology Management for entity vocabulary and extraction guidance
- OKF Portability for exporting and importing workspaces