Skip to content

Integrate with CrewAI

Use ZettelForge as the CTI memory layer for a CrewAI crew. Three tools wrap MemoryManager so your agents can persist findings, recall prior intel, and synthesize answers across stored notes — all backed by a local SQLite + LanceDB store you control.

Prerequisites

  • Python 3.10 or newer
  • ZettelForge installed with the CrewAI extra:
pip install zettelforge[crewai]

This pulls crewai>=1.14.0. If you already have CrewAI installed, verify the version:

python -m pip show crewai | grep Version

The three tools

Tool CrewAI tool name What it does
ZettelForgeRecallTool zettelforge_recall Blended vector + graph search across stored notes
ZettelForgeRememberTool zettelforge_remember Persist a finding; auto-extracts CVEs, actors, ATT&CK, IOCs
ZettelForgeSynthesizeTool zettelforge_synthesize LLM-synthesized answer over retrieved memory

Import all three from zettelforge.integrations.crewai.

Quick start

Minimal setup for a single analyst agent with recall and memory:

from crewai import Agent
from zettelforge import MemoryManager
from zettelforge.integrations.crewai import (
    ZettelForgeRecallTool,
    ZettelForgeRememberTool,
)

mm = MemoryManager()

analyst = Agent(
    role="CTI analyst",
    goal="Investigate threat-actor activity using prior intel",
    backstory="Senior analyst with access to the team's knowledge base.",
    tools=[
        ZettelForgeRecallTool(memory_manager=mm, k=5),
        ZettelForgeRememberTool(memory_manager=mm),
    ],
)

MemoryManager() reads from and writes to ~/.amem/ by default. Notes written in one run are available in every subsequent run.

Two-agent investigation pattern

For investigation crews, split recall and synthesis across two agents. The first agent pulls relevant intel and persists the question as an auditable note; the second composes the final analyst-facing answer from what the first surfaced.

from crewai import Agent, Crew, Task
from zettelforge import MemoryManager
from zettelforge.integrations.crewai import (
    ZettelForgeRecallTool,
    ZettelForgeRememberTool,
    ZettelForgeSynthesizeTool,
)

mm = MemoryManager()

retrieval_analyst = Agent(
    role="CTI retrieval analyst",
    goal=(
        "Pull all prior intel relevant to the investigation question and "
        "persist the question itself as a note for future audits."
    ),
    backstory=(
        "Junior analyst whose strength is exhaustive recall. Cites note ids "
        "verbatim and never invents context."
    ),
    tools=[
        ZettelForgeRecallTool(memory_manager=mm, k=5),
        ZettelForgeRememberTool(memory_manager=mm),
    ],
    allow_delegation=False,
)

senior_analyst = Agent(
    role="Senior CTI analyst",
    goal=(
        "Synthesize a clear, sourced answer for the team using only the "
        "memory ZettelForge surfaces. If the synthesis is empty, say so."
    ),
    backstory=(
        "10 years SOC and threat intel. Refuses to speculate beyond what "
        "the cited notes support. Answers in plain English."
    ),
    tools=[
        ZettelForgeSynthesizeTool(memory_manager=mm, k=8),
        ZettelForgeRecallTool(memory_manager=mm, k=5),
    ],
    allow_delegation=False,
)

question = "What do we know about APT28 spear-phishing tradecraft?"

crew = Crew(
    agents=[retrieval_analyst, senior_analyst],
    tasks=[
        Task(
            description=(
                f"Investigate: {question!r}. First call zettelforge_recall to "
                "surface all relevant notes. Then call zettelforge_remember to "
                "persist the question as a tracked investigation. Return the "
                "recalled notes verbatim for the senior analyst to consume."
            ),
            expected_output="A structured list of relevant note ids and their content.",
            agent=retrieval_analyst,
        ),
        Task(
            description=(
                f"Using the prior memory the retrieval analyst surfaced, answer: "
                f"{question!r}. Use zettelforge_synthesize for the main composition. "
                "Cite note ids inline. If memory is insufficient, say so explicitly."
            ),
            expected_output="A short, cited answer suitable for an analyst Slack channel.",
            agent=senior_analyst,
        ),
    ],
)

result = crew.kickoff()
print(result)

A complete runnable version of this pattern is at examples/crewai_cti_crew.py in the ZettelForge repo.

Tool output format

ZettelForgeRecallTool

Returns a text block with one numbered entry per note:

Found 3 note(s) for query: 'APT29 OAuth phishing'

[1] id=note_20260624_190306_0693 tier=A confidence=1.0 domain=cti
    source: docs-agent-verification
    entities: apt29
    content: APT29 (Cozy Bear) used OAuth device-code phishing against
             Microsoft 365 tenants in 2025-Q2.

Content is truncated at 500 characters. Entity names are normalized to canonical lowercase form during indexing: "APT29" is stored and returned as apt29. Keep this in mind when you chain a recall result into a recall_actor() call — use the canonical form the tool surfaced, not the original casing.

ZettelForgeRememberTool

Returns a one-line status string:

Stored note id=note_20260624_190306_0693 status=created tier=A entities=apt29

Status is one of: created, updated, corrected, noop.

ZettelForgeSynthesizeTool

Returns the LLM's synthesized answer, a confidence score, and source note ids. Requires a configured LLM provider (see Configuration). Without a provider, the tool returns an empty answer with confidence: 0.0.

The format constructor parameter controls output structure:

Format Available in ZettelForge OSS Notes
direct_answer Yes Default. Returns answer, confidence, and sources.
synthesized_brief ThreatRecall.ai SaaS only Falls back to direct_answer in OSS.
timeline_analysis ThreatRecall.ai SaaS only Falls back to direct_answer in OSS.
relationship_map ThreatRecall.ai SaaS only Falls back to direct_answer in OSS.

Tool constructor reference

ZettelForgeRecallTool

Parameter Type Default Description
memory_manager MemoryManager required The ZettelForge memory instance
k int 10 Maximum notes to return per call
domain str \| None None Filter to a specific domain (e.g. "cti"). None returns all domains.

ZettelForgeRememberTool

Parameter Type Default Description
memory_manager MemoryManager required The ZettelForge memory instance
domain str "cti" Domain tag for stored notes
source_type str "crewai_agent" Source type tag for provenance tracking
evolve bool False Run the two-phase evolution pipeline (see below)

ZettelForgeSynthesizeTool

Parameter Type Default Description
memory_manager MemoryManager required The ZettelForge memory instance
k int 10 Notes to retrieve as synthesis context
format str "direct_answer" Synthesis output format (see format table above)
tier_filter list[str] \| None None Restrict synthesis to specific tiers. None uses the config default (["A", "B"]).

The evolve flag

By default, ZettelForgeRememberTool stores each note with no LLM call. The note is indexed immediately with entity extraction, deduplication, and tier assignment handled offline.

Set evolve=True on the constructor to run the two-phase evolution pipeline: the LLM reads the new finding against the existing knowledge graph and decides whether to add, update, deduplicate, or discard it. This produces a tighter graph over time, but requires a configured LLM provider and adds one LLM call per remember invocation.

remember = ZettelForgeRememberTool(memory_manager=mm, evolve=True)

Reserve evolve=True for the agent role whose job is to maintain the long-term knowledge base, not for every retrieval or intermediate-step agent in the crew.

Sharing memory across agents and runs

MemoryManager() defaults to ~/.amem/ as its data directory. Notes persist between runs automatically. Any crew that shares the same MemoryManager instance (or the same data directory) reads the same notes.

To isolate crews by subject area, filter by domain:

# Only store and recall notes in the "red_team" domain
recall = ZettelForgeRecallTool(memory_manager=mm, domain="red_team")
remember = ZettelForgeRememberTool(memory_manager=mm, domain="red_team")

Pass domain=None on a recall tool to search across all domains regardless of how notes were originally stored.

Further reading