Skip to main content
Mode · Agentic memory

Quickstart — Agentic memory

You'll create a memory project, write your first observation, and query it with recall. Five minutes.

1. Create the memory project

uv run grail init ./my-memory --memory

The difference vs KB: instead of an input/ folder, GRAIL creates memories/. Final structure:

my-memory/
├── grail.yaml
├── meta.json ← project identity (ULID, mode, dates)
├── memories/ ← observations go here, organised by folders
└── output/ ← parquet, FAISS, etc.

The --memory flag flips memory mode in grail.yaml:

mode: memory

2. Write your first observation

From Python (the SDK is the natural path for agents):

import asyncio
from grail import MemoryProject

async def main():
mp = MemoryProject("./my-memory")

reply = await mp.add_observation(
title="Acme picked Postgres over DynamoDB",
content=(
"In Tuesday's architecture review, Acme committed to Postgres "
"for the order-history service because of transactional needs "
"across the inventory and payments tables."
),
category="work/clients/acme",
tags=["decision", "architecture"],
entities=[
{"name": "Acme", "type": "ORGANIZATION", "description": "Client"},
{"name": "Postgres", "type": "TECHNOLOGY", "description": "Chosen DB"},
{"name": "DynamoDB", "type": "TECHNOLOGY", "description": "Rejected alternative"},
],
relationships=[
{"source": "Acme", "target": "Postgres", "relationship_type": "CHOSE",
"description": "for transactional order-history"},
{"source": "Acme", "target": "DynamoDB", "relationship_type": "REJECTED",
"description": "lacks cross-table transactions"},
],
confidence=0.95,
)
print(reply.ok, reply.data["observation_id"])

asyncio.run(main())

What happens under the hood:

  1. GRAIL writes memories/work/clients/acme/acme-picked-postgres-over-dynamodb.md with YAML frontmatter (title, tags, observed_at, confidence, …).
  2. Entities and relationships are merged directly into parquet — no extraction LLM call.
  3. The folder work/clients/acme becomes a community (folders-as-communities).
  4. If you configured embeddings, entity descriptions are embedded into the vector store.

3. Recall

recall is the memory-only search mode — zero LLM, zero embedding, just a structural filter over the columns:

# Everything for Acme in the last 7 days tagged "decision"
uv run grail query ./my-memory --mode recall \
--since 7d \
--category "work/clients/acme/**" \
--tag decision

Or from Python:

recall = await mp.recall(
mode="recall",
category="work/clients/acme/**",
tags=["decision"],
since="7d",
)
for obs in recall.data["observations"]:
print(obs["observed_at"], obs["title"])

Filters also work as a modifier on any other mode:

# Cascade scoped to recent Acme observations
uv run grail query ./my-memory "Why did Acme rule out DynamoDB?" \
--mode cascade \
--since 30d \
--category "work/clients/acme/**"

4. Consolidate

When memory starts growing (say above 30 entities), run the proposal generator. It doesn't mutate anything — just writes suggestions you review:

uv run grail consolidate ./my-memory

Generates four kinds of proposals:

KindWhat it proposes
merge_aliases"These two entities look like the same person/thing"
discover_community"These entities form a new dense community"
move_entity"This entity belongs more to another folder"
split_folder"This folder has two distinct clusters"

You review them and apply:

uv run grail proposals list ./my-memory
uv run grail proposals apply ./my-memory --accept <proposal_id>

5. Wire it to your agent

The SDK is enough for agent code. If your agent is Claude Code, Codex, or OpenCode, the skill wires it automatically via tool calls — the agent writes observations declaring what it learns, without writing Python.

Compared to knowledge base

Mode · Knowledge base
Mode · Agentic memory
Who writesAn LLM reads documentsYour agent declares entities
Write cost$$ per chunk$0 (no LLM)
Folder structureFlat input/Hierarchical memories/<category>/
CommunitiesLeidenFolders + proposals
recall mode

See The two modes for the full picture.

Next step