The Zettelkasten philosophy in ZettelForge¶
ZettelForge's name reveals its intellectual lineage. The system applies Niklas Luhmann's Zettelkasten ("slip box") method to AI agent memory, translating principles designed for a 20th-century sociologist's index cards into a modern CTI knowledge architecture.
Luhmann produced more than 70 books and 400 articles using 90,000 handwritten index cards. The cards were not organized by topic. They were connected by relationship — a network, not a hierarchy. ZettelForge inherits that intuition and adds the machinery to make it work at machine scale.
Five principles, translated¶
1. Atomic notes¶
Luhmann's rule: each card captures one idea, expressed in your own words.
ZettelForge's translation: each MemoryNote captures one piece of intelligence. The FactExtractor enforces this by decomposing long reports — a 3,000-word threat writeup, a 40-page DFIR report — into discrete facts. The default is up to five facts per call. Each fact becomes a separate note with its own vector embedding, its own confidence score, and its own place in the knowledge graph.
"APT28 shifted to edge device exploitation" is one note. "DROPBEAR is no longer in active use" is another. This atomicity is what makes retrieval precise: a vector search for "edge device exploitation" returns the specific note, not the entire report.
2. Meaningful links¶
Luhmann did not file cards by topic — he connected them by relationship. Card 21/3a linked to 15/7b not because they shared a subject but because one idea led to another.
ZettelForge implements this through the SQLite knowledge graph. Each entity extracted from a note (actors, malware families, CVEs, IP addresses) becomes a node. A MENTIONED_IN edge connects that node back to the note that sourced it. Additional edges encode CTI-domain relationships between entities: uses, targets, attributed_to, causes, enables, exploits, and more.
These are not generic "related-to" tags. Each relationship type carries a specific directional meaning. When APT28 and Cobalt Strike share a uses edge, that edge is a first-class object in the graph — it has its own confidence score, a source note provenance, and a position in the temporal history of the threat actor.
ThreatRecall.ai additionally backs its knowledge graph with TypeDB, which enables typed inference across multi-hop relationship chains and alias-of resolution. In ZettelForge OSS, the graph uses SQLite storage and BFS traversal.
3. Emergent structure¶
Luhmann famously said his Zettelkasten surprised him. He did not impose a hierarchy — structure emerged from the connections between notes.
ZettelForge achieves this through multi-hop graph traversal and blended retrieval. When recall() runs, the BlendedRetriever does not just scan the vector store. It also queries the knowledge graph: starting from the entities named in your query, it traverses edges outward — finding notes that mention related entities you did not name.
A question about APT28's capabilities returns not just directly stored facts but notes about campaigns attributed to APT28, tools used in those campaigns, and CVEs those tools exploit. The knowledge graph grows smarter as it grows larger — each new note adds edges that open new traversal paths.
The BlendedRetriever combines three sources: a VectorRetriever (semantic similarity over LanceDB), a GraphRetriever (entity-scoped KG traversal up to two hops), and a KeywordRetriever (deterministic intent classification). Which blend you get depends on the query's classified intent — factual, relational, or exploratory.
4. Unique identifiers¶
Every Luhmann card had a unique address. Without stable addresses, links break.
ZettelForge uses two parallel ID schemes:
Note IDs are timestamp-based strings generated at storage time:
note_20260620_190237_2616
The format is note_{YYYYMMDD}_{HHMMSS}_{0000-9999} where the suffix is a random four-digit integer. These IDs are stable once written — a note's ID never changes after it is created.
Entity keys are composite strings formed from the entity type and value:
actor:apt28
cve:cve-2024-4577
malware:cobalt_strike
In SQLite, the (entity_type, entity_value) pair has a UNIQUE constraint, ensuring the same entity is always the same node regardless of which note surfaced it. In-memory graph nodes use node_{uuid4()[:12]} as an internal handle, but the entity_type + entity_value tuple is the stable canonical identity.
The MENTIONED_IN edge connects the two worlds: an entity node in the knowledge graph points to the note IDs where that entity appears.
5. Evolution over deletion¶
Luhmann rarely discarded cards. He added new cards that refined, corrected, or superseded old ones — creating a visible intellectual history.
ZettelForge follows this exactly. When MemoryUpdater determines that a new fact contradicts an existing note, it does not delete the old note. Instead, mark_note_superseded() links the old note to the new one via links.superseded_by, and the new note records the old one in links.supersedes.
The old note remains in storage, available for historical queries. recall() filters superseded notes by default (exclude_superseded=True). To see the history, pass exclude_superseded=False:
from zettelforge import MemoryManager
mm = MemoryManager()
# Default: returns only current notes
current = mm.recall("APT28 infrastructure")
# History mode: includes superseded notes
with_history = mm.recall("APT28 infrastructure", exclude_superseded=False)
An analyst can always ask "what did we think about this six months ago?" The evolution history is preserved in the graph.
Where the analogy breaks¶
Two important differences between Luhmann's system and ZettelForge:
Scale. Luhmann's slip box held ~90,000 cards over 30 years. ZettelForge can ingest thousands of notes per hour. At this scale, manual linking is impossible. Entity extraction and heuristic relationship inference replace the human act of connecting ideas.
Retrieval. Luhmann browsed his cards by following links from a starting point. ZettelForge adds vector similarity search: you can find relevant notes even when you do not know the right entity name to start from. The BlendedRetriever combines both approaches — graph traversal (Luhmann's method) and semantic search (the modern addition). At larger scale, the graph paths become the signal that filters vector noise.
The deeper claim¶
These five principles are design constraints, not marketing copy. They explain why ZettelForge makes certain trade-offs:
- Notes are atomic because merged notes produce imprecise retrieval.
- Links are typed because generic "related-to" edges give the graph no structure to traverse.
- Structure is emergent because a pre-imposed hierarchy would require someone to maintain it.
- IDs are stable because links break when addresses change.
- Deletion is avoided because old intelligence still has value as context.
The test of any knowledge architecture is whether it survives the analyst who is no longer there to maintain it. A ZettelForge instance should be as useful to the analyst who inherits it as to the one who built it.