Skip to content

Quickstart: your first memory

You will stand up a working ZettelForge instance, store three pieces of threat intelligence about APT28 and Lazarus Group, and recall them by natural-language query and by actor name. The whole path runs locally with no cloud account and no API keys.

What you learn

  • Storing CTI facts with remember()
  • Recalling notes with recall() (blended vector plus graph retrieval)
  • Looking up an actor directly with recall_actor()

Prerequisites

  • Python 3.10 or newer
  • Git and an internet connection for the clone, Python packages, and initial embedding-model download

The tutorial runs embeddings in-process and stores your notes in a local SQLite database plus a LanceDB vector index. You do not need Docker, TypeDB, Ollama, or an API key.


Step 1: Install ZettelForge

Clone the repository and install it into a virtual environment.

git clone https://github.com/rolandpg/zettelforge.git
cd zettelforge
python3 -m venv .venv
source .venv/bin/activate
pip install -e .

The virtual environment keeps ZettelForge isolated from your system packages. It also avoids the externally managed Python restriction used by current Debian and Ubuntu releases.

Confirm the install:

python -c "import zettelforge; print(zettelforge.__version__)"

You see the installed version:

2.8.0

Step 2: Keep this run offline

The base package enables optional LLM enrichment by default. Disable that slow path for this tutorial so ZettelForge does not try to contact an Ollama server:

export ZETTELFORGE_ENRICHMENT_ENABLED=false

Keep this shell open for the remaining steps.

Step 3: Store and recall your first memories

The embedding model downloads automatically the first time you construct a MemoryManager. This is a one-time download; subsequent runs load it from cache.

Create a file called quickstart.py with the following content. The CTI facts and example.com references are synthetic tutorial data.

from zettelforge import MemoryManager

mm = MemoryManager()

note1, status1 = mm.remember(
    "APT28 (Fancy Bear) deployed a modified X-Agent implant against "
    "Ukrainian government networks in March 2026. The implant used "
    "CVE-2025-21298 for initial access via spearphishing attachments.",
    source_type="report",
    source_ref="https://example.com/apt28-ukraine-2026",
    domain="cti",
)

note2, status2 = mm.remember(
    "Lazarus Group conducted Operation DreamJob targeting defense "
    "contractors in South Korea and Japan during Q1 2026. The campaign "
    "used trojanized job offer PDFs delivering the DreamJob backdoor "
    "via LinkedIn messages.",
    source_type="report",
    source_ref="https://example.com/lazarus-dreamjob-2026",
    domain="cti",
)

note3, status3 = mm.remember(
    "APT28 was also observed using Cobalt Strike beacons for lateral "
    "movement after initial X-Agent deployment. The beacons called back "
    "to infrastructure hosted on bulletproof providers in Moldova.",
    source_type="report",
    source_ref="https://example.com/apt28-cobalt-strike",
    domain="cti",
)

print(f"Stored: {status1}, {status2}, {status3}")

print("\nQuery recall:")
results = mm.recall("What tools does APT28 use?", domain="cti", k=5)
for note in results:
    print(f"- {note.content.source_ref}")

print("\nActor recall:")
apt28_notes = mm.recall_actor("APT28", k=5)
for note in sorted(apt28_notes, key=lambda item: item.content.source_ref):
    print(f"- {note.content.source_ref}")

Step 4: Run the tutorial

Run the file from the same shell:

python quickstart.py

The verified run produced this standard output:

Stored: created, created, created

Query recall:
- https://example.com/apt28-cobalt-strike
- https://example.com/apt28-ukraine-2026
- https://example.com/lazarus-dreamjob-2026

Actor recall:
- https://example.com/apt28-cobalt-strike
- https://example.com/apt28-ukraine-2026

remember() returns a (MemoryNote, status) tuple. The three created statuses confirm that the write path accepted each note. recall() blends vector and graph retrieval; the verified query ranks both APT28 notes before the unrelated Lazarus note. recall_actor() performs a direct entity-index lookup and returns the two notes indexed under the canonical actor name.

On the verified 2.8.0 build, startup also writes warnings to standard error for unset optional TypeDB credentials and the current FastEmbed model revision. Those warnings do not stop the SQLite tutorial.

Why the fast path matters

This tutorial disables optional LLM enrichment, but the core write path still embeds the note, writes it to SQLite and LanceDB, extracts heuristic entities, and updates heuristic graph edges. LLM causal extraction, richer entity extraction, and note evolution stay outside this success path.

Current offline lookup boundary

Use canonical actor labels such as APT28 for this tutorial. In the verified offline run, recall_cve("CVE-2025-21298") returned its note, but recall_actor("Fancy Bear"), recall_actor("Lazarus Group"), and recall_tool("Cobalt Strike") returned no matches. Configure and verify LLM enrichment before relying on alias or richer tool-name lookup. See the Configuration reference.

What you built

You now have a working ZettelForge instance with:

  • Three stored notes about APT28 and Lazarus Group activity, embedded and persisted to a local SQLite database and LanceDB vector index
  • Entity index entries for the canonical APT28 actor, the CVE, and several other heuristically extracted entities
  • Blended retrieval that combines vector similarity with graph traversal and ranks both APT28 notes first for the tutorial query

The tutorial data lives under ~/.amem by default in zettelforge.db and the vectordb/ directory. The verified run completed the core flow without an external service.

Next steps

ZettelForge is the open-source core documented on this site. ThreatRecall.ai is the hosted SaaS built on the same engine for teams that want managed recall without running infrastructure.