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¶
- Memory Manager API — full
recall(),remember(),synthesize()signatures - Retrieval policies — how blended vector + graph ranking works
- LLM budgets and timeouts — configuring the LLM provider for synthesize and evolve
- Integrate with your LLM agent — raw API integration without a CrewAI framework