Skip to content

Memory Model

This page describes the complete data model for memories in AutoMem, including their structure, properties, classification taxonomy, and storage representation. For information about how memories relate to each other, see Relationship Types. For details on how memories are searched and retrieved, see Hybrid Search.


Every memory in AutoMem is a structured data object with both required and optional properties. Memories are classified into specific types (Decision, Pattern, Preference, etc.), enriched with metadata, and stored redundantly in both FalkorDB (graph) and Qdrant (vector) databases. The memory model supports temporal validity windows, confidence scoring, and hierarchical tag organization.

graph LR
    Memory["Memory Node"]
    Memory --> id["id: UUID"]
    Memory --> content["content: string"]
    Memory --> type["type: Memory Type"]
    Memory --> timestamp["timestamp: ISO datetime"]
    Memory --> importance["importance: 0.0-1.0"]
    Memory --> confidence["confidence: 0.0-1.0"]
    Memory --> tags["tags: string[]"]
    Memory --> metadata["metadata: JSON"]
    Memory --> embedding["embedding: float[VECTOR_SIZE]"]

All memories contain the following properties:

PropertyTypeRequiredDescription
idstring (UUID)YesUnique identifier, always generated server-side (an id sent in the request is ignored)
contentstringYesThe actual memory text (minimum 1 character)
typestringYesClassification from the MEMORY_TYPES set, or the Memory fallback. An explicit type in the request is stored as sent
confidencefloatYesClassification confidence score (0.0-1.0). Defaults to 0.9 when the request sets type; a confidence sent without type is ignored
importancefloatYesUser-specified importance (0.0-1.0, default: 0.5)
timestampstring (ISO 8601)YesWhen the memory was created or occurred
tagsarray[string]NoHierarchical categorization tags (e.g., ["project:automem", "decision"])
tag_prefixesarray[string]NoPre-computed lowercase tag prefixes for fast filtering
metadataobjectNoFlexible JSON object for custom fields
embeddingarray[float]NoVector for semantic search. Its length is the effective VECTOR_SIZE (default 1024); while VECTOR_SIZE_AUTODETECT is on (the default), an existing Qdrant collection’s dimension is adopted instead
PropertyTypeRequiredDescription
t_validstring (ISO 8601)NoWhen this memory becomes valid (future-dated memories). Defaults to timestamp
t_invalidstring (ISO 8601)NoWhen this memory expires or becomes invalid
updated_atstring (ISO 8601)YesUpdate timestamp, not bumped automatically. Defaults to timestamp on creation; PATCH /memory/:id keeps the stored value unless the request sends updated_at
last_accessedstring (ISO 8601)YesLast retrieval or access timestamp. Defaults to updated_at on creation
PropertyTypeDescription
enriched_atstring (ISO 8601)When enrichment pipeline processed this memory
summarystringAuto-generated summary (first sentence, max 240 chars)
metadata.entitiesobjectExtracted entities: tools, projects, people, concepts, organizations. Stored inside metadata, not as a top-level property
PropertyTypeDescription
archivedbooleanWhether memory is archived (excluded from search candidates)
relevance_scorefloatDynamic score computed by consolidation engine

The classifier assigns one of seven semantic types. When it cannot classify a memory it returns the fallback type Memory (see the note below the type table). The types are defined in MEMORY_TYPES:

graph TB
    Memory["Memory"]

    Decision["Decision<br/>Choices made, selected options"]
    Pattern["Pattern<br/>Recurring behaviors, typical approaches"]
    Preference["Preference<br/>Likes/dislikes, favorites"]
    Style["Style<br/>Communication approach, formatting"]
    Habit["Habit<br/>Regular routines, repeated actions"]
    Insight["Insight<br/>Discoveries, learnings, realizations"]
    Context["Context<br/>Situational background, circumstances"]

    Memory --> Decision
    Memory --> Pattern
    Memory --> Preference
    Memory --> Style
    Memory --> Habit
    Memory --> Insight
    Memory --> Context
TypeDescriptionExample Content
DecisionStrategic or technical choices, selected options”Chose PostgreSQL over MongoDB for ACID compliance”
PatternRecurring behaviors, typical approaches, consistent tendencies”Usually write integration tests before deploying”
PreferenceLikes/dislikes, favorites, personal or team tastes”Prefer async/await over callbacks for clarity”
StyleCommunication approach, formatting preferences, tone”Write commit messages in imperative mood”
HabitRegular routines, repeated actions, scheduled activities”Run pytest every morning before starting work”
InsightDiscoveries, learnings, realizations, key findings”Realized embedding batching reduces costs by 40%“
ContextSituational background, circumstances, what was happening”During database migration sprint in Q3 2024”

AutoMem employs a hybrid classification system that balances speed, cost, and accuracy:

  1. Explicit Type (preferred): Client provides type field in POST request. It is stored as sent, with confidence 0.9 unless the request sets confidence
  2. Regex Pattern Matching (fast, free): Checks content against predefined patterns
  3. LLM Fallback (accurate): Uses the model in CLASSIFICATION_MODEL (default gpt-4o-mini) when patterns don’t match

If the LLM is unavailable or returns nothing usable, the classifier returns Memory with confidence 0.3.

graph TB
    Start["Memory Content"]

    Explicit{"Type<br/>provided?"}

    Regex["Check PATTERNS dict"]
    Match{"Regex<br/>match?"}

    LLM["Call classification model<br/>(CLASSIFICATION_MODEL)"]
    LLMValid{"Usable<br/>response?"}

    Decision["Type<br/>+ Confidence"]
    Fallback["Memory (fallback)<br/>confidence: 0.3"]

    Start --> Explicit

    Explicit -->|"Yes: stored as sent"| Decision
    Explicit -->|No| Regex

    Regex --> Match
    Match -->|Yes| Decision
    Match -->|No| LLM

    LLM --> LLMValid
    LLMValid -->|"Yes: aliases normalized"| Decision
    LLMValid -->|No| Fallback

The MemoryClassifier class defines regex patterns for each memory type:

TypeExample Patterns
Decisionr"decided to", r"chose (\w+) over", r"going with", r"opted for"
Patternr"usually", r"typically", r"tend to", r"often", r"consistently"
Preferencer"prefer", r"like.*better", r"favorite", r"rather than"
Styler"wrote.*in.*style", r"communicated", r"formatted as"
Habitr"\balways\b(?!\s+use\b)", r"every time", r"daily", r"weekly", r"routine"
Insightr"realized", r"discovered", r"learned that", r"figured out"
Contextr"during", r"while working on", r"in the context of", r"when"

always use is a Preference pattern, so the Habit pattern skips it.

Matching and Confidence:

  • Types are checked in the order listed above, and the first type with a matching pattern wins
  • Base confidence: 0.6 when a single pattern of that type matches
  • With more than one matching pattern for that type: min(0.95, 0.6 + 0.1 × matches), so two matches give 0.8, three give 0.9, and four or more give 0.95

When no regex pattern matches, the classifier falls back to the classification model (CLASSIFICATION_MODEL, default gpt-4o-mini):

System Prompt Structure:

You are a memory classification system. Classify each memory into exactly ONE of these types:
- Decision: Choices made, selected options, what was decided
- Pattern: Recurring behaviors, typical approaches, consistent tendencies
- Preference: Likes/dislikes, favorites, personal tastes
- Style: Communication approach, formatting, tone used
- Habit: Regular routines, repeated actions, schedules
- Insight: Discoveries, learnings, realizations, key findings
- Context: Situational background, what was happening, circumstances
Return JSON with: {"type": "<type>", "confidence": <0.0-1.0>}

Request Format:

  • Model: CLASSIFICATION_MODEL (default gpt-4o-mini)
  • Max tokens: 50, sent as max_completion_tokens for model names starting with o1, o3, o4, gpt-4o, gpt-4.1, or gpt-5 (which includes the default) and as max_tokens for other models
  • Temperature: 0.3, sent only with max_tokens, so the default model receives no temperature parameter
  • Response format: JSON object
  • Input limit: First 1000 characters of content

Validation:

  • The returned type is normalized through TYPE_ALIASES (for example analysis becomes Insight, and a lowercase decision becomes Decision)
  • A type that cannot be mapped is stored as "Context" with confidence 0.5
  • If the response omits confidence, 0.7 is used
  • An empty response, invalid JSON, or an API error falls back to "Memory" with confidence 0.3, and "Memory" is stored as returned
  • While the enrichment circuit breaker is open, the LLM call is skipped with the same fallback. The breaker opens on an LLM quota error and stays open for ENRICHMENT_CIRCUIT_COOLDOWN_SECONDS (default 300)

The full classification sequence, including how POST /memory decides between explicit type, regex, and LLM:

sequenceDiagram
    participant Client
    participant API as "/memory endpoint"
    participant Classifier as "MemoryClassifier"
    participant OpenAI as "Classification LLM (default gpt-4o-mini)"
    participant Falkor as "FalkorDB"

    Client->>API: "POST /memory<br/>{content, type?}"

    alt type explicitly provided
        API->>API: "Use provided type"
    else type not provided
        API->>Classifier: "classify(content)"

        Classifier->>Classifier: "Try pattern matching"

        alt patterns match
            Classifier-->>API: "type, confidence"
        else no pattern match AND use_llm=true
            Classifier->>OpenAI: "LLM classification"
            OpenAI-->>Classifier: "type, confidence"
            Classifier-->>API: "type, confidence"
        else LLM unavailable or no usable result
            Classifier-->>API: "Memory (fallback), 0.3"
        end
    end

    API->>Falkor: "MERGE Memory node with type"
    Falkor-->>API: "memory_id"
    API-->>Client: "201 Created"

Memories support optional temporal validity windows:

PropertyPurposeUse Case
t_validEarliest time this memory is validFuture-dated reminders, scheduled knowledge
t_invalidLatest time this memory is validExpiring credentials, time-bound decisions

By default, /recall excludes memories where:

  • now < t_valid (not yet valid)
  • now >= t_invalid (expired)

Override: Pass state_mode=history (or the legacy current_only=false) to skip the current-state filter and include not-yet-valid and expired memories. The filter also replaces a superseded memory (one with an active INVALIDATED_BY or EVOLVED_INTO replacement) with its replacement, so history mode returns the superseded memory itself. /recall resolves state_mode to current when neither parameter is sent.

time_query, start, and end only set the time window; they do not include expired memories. The candidate searches skip archived memories in either mode (for example the graph keyword search and the vector search).

Example Scenarios:

  1. Future-dated memory: Set t_valid to a future date — the memory is not searchable until that date arrives.
  2. Expiring credential: Set t_invalid to an expiry date — the memory is automatically excluded from search after that date.

The metadata field is a flexible JSON object for storing arbitrary key-value pairs. Common patterns:

Automatically added by the enrichment pipeline:

FieldTypeDescription
metadata.entitiesobjectExtracted entities by category
metadata.enrichmentobjectDetails of the latest enrichment run: last_run, forced, temporal_links, patterns_detected, semantic_neighbors. Just-in-time enrichment during recall adds jit and jit_at to the same object

enriched_at and summary are top-level properties (see Enrichment Properties above), not metadata fields.

The metadata.entities object is structured by entity category:

{
"entities": {
"tools": ["pytest", "docker"],
"projects": ["automem"],
"people": ["sarah"],
"concepts": ["embedding", "vector search"],
"organizations": ["OpenAI"]
}
}

The following fields are reserved and should not be placed in metadata (they are top-level properties):

  • type, confidence, content, timestamp, tags, tag_prefixes
  • importance, embedding, id, archived, relevance_score
  • t_valid, t_invalid, updated_at, last_accessed

Memories are stored as nodes with the Memory label. Node properties directly map to the memory schema:

Graph Features:

  • Relationships connect memories (see Relationship Types)
  • Cypher queries enable graph traversal
  • AutoMem does not create FalkorDB indexes on Memory properties at startup or on write. scripts/restore_from_backup.py creates an id index on each restored node label, including Memory, and the index stays after the restore

Memories are stored as points with payloads mirroring FalkorDB properties:

Qdrant Features:

  • Keyword payload indexes on tags, tag_prefixes, and type for fast filtering, created at startup unless QDRANT_ENSURE_PAYLOAD_INDEXES is false
  • HNSW index on vectors for approximate nearest neighbor search
  • Payload filters support complex boolean queries

Dual Storage Strategy:

  • FalkorDB is the source of truth (required)
  • Qdrant is optional but enables semantic search
  • On POST /memory, FalkorDB is written synchronously. Qdrant is written in the same request only when the request supplies embedding; otherwise the vector is queued for generation, or skipped when Qdrant is not configured
  • If graph data is lost, scripts/recover_from_qdrant.py re-creates Memory nodes from the Qdrant payloads. It clears the graph first and does not restore relationships

stateDiagram-v2
    [*] --> Created: POST /memory

    Created --> EnrichmentQueued: Immediate<br/>Queue job

    EnrichmentQueued --> Enriching: Worker picks up

    Enriching --> Enriched: Success<br/>Entities extracted<br/>Relationships created
    Enriching --> EnrichmentFailed: Failure

    EnrichmentFailed --> Enriching: Forced jobs only<br/>Flat backoff
    EnrichmentFailed --> Abandoned: Non-forced job<br/>or max attempts

    Enriched --> Active: Searchable<br/>In graph

    Active --> Active: PATCH /memory/:id<br/>No enrichment job queued

    Active --> Decaying: Consolidation<br/>relevance_score decreases

    Decaying --> Archived: Forget task<br/>Low relevance

    Archived --> [*]: DELETE /memory/:id

    Active --> [*]: DELETE /memory/:id
  1. Created (POST /memory):

    • Memory written to FalkorDB
    • Embedding queued for generation (when omitted and Qdrant is configured)
    • Enrichment job queued
    • Returns 201 Created immediately
  2. Enrichment Queued:

    • Job added to in-memory queue
    • Worker thread blocks on the queue and wakes at least every ENRICHMENT_IDLE_SLEEP_SECONDS (default 2 seconds)
    • Up to ENRICHMENT_MAX_ATTEMPTS attempts (default 3), with a flat ENRICHMENT_FAILURE_BACKOFF_SECONDS sleep (default 5 seconds) before each re-enqueue
    • The re-enqueue goes through the in-flight duplicate check, which drops it for non-forced jobs (the kind POST /memory queues), so only forced jobs from POST /enrichment/reprocess are retried
  3. Enriching:

    • Entity extraction (spaCy NER + regex)
    • Temporal links created (PRECEDED_BY)
    • Semantic neighbors found (SIMILAR_TO)
    • Pattern detection (EXEMPLIFIES)
    • Summary generation (optional)
  4. Active:

    • Fully searchable via /recall
    • Included in graph queries
    • Participates in consolidation
    • PATCH /memory/:id saves the edit without queuing enrichment, so entities, summary, and links are not refreshed; POST /enrichment/reprocess re-runs it
  5. Decaying:

    • relevance_score decreases exponentially
    • Decay rate: daily consolidation task (default interval: 86400s)
    • Formula (simplified): decay_factor = exp(-0.01 × age_days). Full score combines decay with access recency, relationship count, importance, and confidence: relevance = decay_factor × (0.3 + 0.3 × access_factor) × relationship_factor × (0.5 + importance) × (0.7 + 0.3 × confidence), clamped to [importance × 0.3, 1.0].
  6. Archived:

    • Marked archived: true
    • Excluded from search by default
    • Can be deleted with DELETE /memory/:id; the API has no route to restore an archived memory
  7. Deleted:

    • Removed from both FalkorDB and Qdrant
    • Relationships cleaned up
    • Irreversible operation

The complete flow from client request through classification, storage, and queue submission:

sequenceDiagram
    participant C as Client
    participant API as Flask API
    participant CL as MemoryClassifier
    participant F as FalkorDB
    participant EQ as Enrichment Queue
    participant EmQ as Embedding Queue
    participant Q as Qdrant

    C->>API: POST /memory<br/>{content, tags, importance}

    alt Type provided
        API->>API: Use explicit type
    else Auto-classify
        API->>CL: classify(content)
        CL->>CL: Try regex patterns
        alt Regex match
            CL-->>API: (type, confidence)
        else No match
            CL->>CL: _classify_with_llm()
            CL-->>API: (type, confidence)
        end
    end

    API->>F: CREATE Memory node
    F-->>API: memory_id

    alt Embedding provided
        API->>Q: Store vector + payload
    else No embedding
        API->>EmQ: Queue for generation
    end

    API->>EQ: Enqueue enrichment

    API-->>C: 201 Created<br/>{memory_id, enrichment: queued}

  • Minimum length: 1 character (empty strings rejected)
  • Soft limit: MEMORY_CONTENT_SOFT_LIMIT = 500 characters — content between 500–2000 chars triggers auto-summarization to a ~300 character target (when MEMORY_AUTO_SUMMARIZE=true)
  • Hard limit: MEMORY_CONTENT_HARD_LIMIT = 2000 characters — content exceeding this is rejected with 400 Bad Request
  • Encoding: UTF-8
  • The classifier assigns one of: Decision, Pattern, Preference, Style, Habit, Insight, Context, or the Memory fallback (confidence 0.3) when no pattern matches and the LLM returns nothing usable
  • An explicit type is not checked: POST /memory and POST /memory/batch store whatever string the request sends, without rejecting unknown values or mapping them through TYPE_ALIASES, so send the canonical spelling
  • Range: 0.0 to 1.0 (inclusive)
  • Default: 0.9 if type explicitly provided. Otherwise the classifier computes it and any confidence in the request is ignored
  • Non-numeric values rejected
  • Range: 0.0 to 1.0 (inclusive)
  • Default: 0.5
  • Used in search ranking and consolidation
  • Array of strings, or a single string that is stored as one tag (POST /memory does not split comma-separated strings). Any other type returns 400 Bad Request
  • tags are stored as sent, with case preserved and blank entries kept, until the enrichment run rewrites them
  • tag_prefixes are computed from the trimmed, lowercased tags, skipping blank entries
  • Hierarchical prefixes auto-computed, splitting on : or / (e.g., "project:automem:api" → ["project", "project:automem", "project:automem:api"])
  • PATCH /memory/:id and the enrichment pipeline use a different normalizer that splits comma-separated strings, trims entries, and drops blanks. Enrichment rewrites the stored tags with it, removes duplicates, and appends entity:<category>:<slug> tags
  • Must be ISO 8601 format
  • Automatically converted to UTC with +00:00 timezone
  • Strings ending in Z converted to +00:00
  • Invalid timestamps return 400 Bad Request
  • Must match the effective vector dimension: VECTOR_SIZE (default 1024), or the dimension of an existing Qdrant collection while VECTOR_SIZE_AUTODETECT (default true) adopts it
  • Accepts a list of numbers or a comma-separated string; all values must be numeric
  • On POST /memory, generated in the background if omitted and Qdrant is configured (otherwise embedding_status is skipped). With EMBEDDING_PROVIDER=auto (the default) the provider is the first available of Voyage (VOYAGE_API_KEY), OpenAI (OPENAI_API_KEY), Ollama (OLLAMA_BASE_URL or OLLAMA_MODEL), local FastEmbed, then a deterministic placeholder. See Embedding Generation for the other EMBEDDING_PROVIDER values