SmartMemory
Guides

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 semantic will 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

FieldDefaultPurpose
namerequiredClass name. For kind="record" this is the value you write as memory_type
kindentityentity for extraction vocabulary, record for a concrete record class
properties_schemanoneMap of field name to field spec (see below)
required_propertiesnoneField names that must be present on every item
storage_strategynonefull, indexed, or append. Record classes only
storage_searchablenoneWhether items are embedded for vector search by default. Record classes only
tierconfirmedGovernance 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:

KeyValuesMeaning
typestring, integer, number, boolean, datetime, object, listRequired. The field's type
ofstring, integer, number, boolean, datetime, objectElement type, for list fields only
defaultany JSON valueApplied when the field is absent. Must be compatible with type
indexedtrue or falseCreates a database index on the field
append_onlytrue or falseThe 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:

  • full stores the item and, when storage_searchable is true, embeds it for vector search.
  • indexed stores the item with indexed fields for structured lookup.
  • append is an append-only log shape. Items written with append are never embedded, regardless of storage_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.
  • skip migrates 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

RoutePython SDKJavaScript SDK
POST /memory/ontology/typesdeclare_typedeclareType
GET /memory/ontology/typeslist_ontology_typeslistTypes
GET /memory/ontology/types/{id}get_ontology_typegetType
GET /memory/ontology/types/{id}/auditget_ontology_type_auditgetTypeAudit
POST /memory/ontology/types/{from}/migrate-to/{to}migrate_ontology_type_instancesmigrateTypeInstances
POST /memory/ontology/types/{id}/retireretire_ontology_typeretireType
POST /memory/ontology/relationsdeclare_relationdeclareRelation

All routes require Authorization: Bearer <token> and X-Workspace-Id.

On this page