Skip to content

Graphiti Project Namespaces

What are Project Namespaces?

Project namespaces enable multiple projects to share a single Graphiti instance while maintaining complete knowledge isolation. Each project gets its own prefixed knowledge groups, preventing cross-contamination while allowing shared system-level knowledge.


Table of Contents


The Problem It Solves

Multi-Project Knowledge Contamination

Without namespacing, multiple projects sharing a Graphiti instance face knowledge collision:

Project A: Adds architecture decision about authentication
Project B: Searches for "authentication"
Project B: Incorrectly retrieves Project A's architecture
Project B: Implements wrong authentication approach

Real-World Example

Scenario: GuardKit development repository contains multiple features being developed in parallel using Conductor:

Workspace Project Risk Without Namespacing
feat-auth-impl Authentication feature Shares knowledge with billing feature
feat-billing Billing feature Contaminates auth decisions
feat-admin-ui Admin UI Retrieves mixed context from both

Impact: - Feature-specific decisions leak between projects - Parallel development corrupts each other's knowledge - Context becomes increasingly noisy and unreliable

What Namespacing Provides

With Project Namespaces:

Project A:
  - Group: "project-a__architecture"
  - Knowledge: Authentication decisions isolated to project-a

Project B:
  - Group: "project-b__architecture"
  - Knowledge: Billing decisions isolated to project-b

System:
  - Group: "role_constraints" (no prefix)
  - Knowledge: Shared quality gates, templates

Result: Perfect isolation with shared system knowledge


Quick Start

from guardkit.knowledge.graphiti_client import init_graphiti, get_graphiti

# Initialize with auto-detection from directory name
await init_graphiti()  # Detects project ID from current directory

# Get client
client = get_graphiti()

# Add project-specific knowledge
await client.add_episode(
    name="Authentication Decision",
    episode_body="Using JWT with refresh tokens",
    group_id="project_architecture"  # Automatically prefixed
)

How it works: - Current directory: /home/user/my-awesome-app - Auto-detected project_id: my-awesome-app - Group becomes: my-awesome-app__project_architecture

2. Explicit Configuration

Create .guardkit/graphiti.yaml:

# .guardkit/graphiti.yaml
enabled: true
neo4j_uri: bolt://localhost:7687
neo4j_user: neo4j
neo4j_password: password123
project_id: my-custom-project-id  # Override auto-detection

3. Environment Variable Override

# Highest priority - overrides everything
export GUARDKIT_PROJECT_ID=production-deployment

# Now all projects use this ID
python your_script.py

Core Concepts

Project ID Format

Normalization Rules: - Lowercase only - Spaces → hyphens - Non-alphanumeric (except hyphens) removed - Max 50 characters - Alphanumeric + hyphens only

Examples:

"My Project"  "my-project"
"Project v2.0!"  "project-v20"
"Super_Long_Project_Name_123"  "super-long-project-name-123"
"a" * 100  "a" * 50  # Truncated

Group ID Prefixing

Project Groups (Auto-Prefixed)

Standard project-specific groups:

PROJECT_GROUP_NAMES = [
    "project_overview",
    "project_architecture",
    "feature_specs",
    "project_decisions",
    "project_constraints",
    "domain_knowledge",
]

Prefixing behavior:

project_id = "my-app"

# Project group
client.get_group_id("project_overview")
# Result: "my-app__project_overview"

# Custom project group (defaults to project scope)
client.get_group_id("custom_knowledge")
# Result: "my-app__custom_knowledge"

System Groups (Never Prefixed)

Shared across all projects:

SYSTEM_GROUP_IDS = [
    "role_constraints",
    "quality_gate_configs",
    "implementation_modes",
    "guardkit_templates",
    "guardkit_patterns",
]

No prefixing:

project_id = "my-app"

# System group
client.get_group_id("role_constraints", scope="system")
# Result: "role_constraints"  # No prefix

# Any guardkit_* group
client.get_group_id("guardkit_custom")
# Result: "guardkit_custom"  # Auto-detected as system

Configuration Priority

Order of precedence (highest to lowest):

  1. Explicit parameter to GraphitiClient
  2. Environment variable: GUARDKIT_PROJECT_ID
  3. YAML config: .guardkit/graphiti.yaml
  4. Auto-detection: Current directory name

Example:

# Priority 1: Explicit parameter (highest)
config = GraphitiConfig(project_id="explicit-id")
client = GraphitiClient(config)
# Result: "explicit-id"

# Priority 2: Environment variable
os.environ["GUARDKIT_PROJECT_ID"] = "env-id"
settings = load_graphiti_config()
# Result: "env-id"

# Priority 3: YAML config
# .guardkit/graphiti.yaml contains: project_id: yaml-id
settings = load_graphiti_config()
# Result: "yaml-id"

# Priority 4: Auto-detection (fallback)
# Current directory: /home/user/my-project
client.get_project_id(auto_detect=True)
# Result: "my-project"


Configuration

Basic Configuration

Auto-Detection (Zero Config)

from guardkit.knowledge.graphiti_client import init_graphiti, get_graphiti

# Uses current directory name as project_id
await init_graphiti()

client = get_graphiti()
print(client.project_id)  # e.g., "guardkit"

YAML Configuration

# .guardkit/graphiti.yaml
enabled: true
neo4j_uri: bolt://localhost:7687
neo4j_user: neo4j
neo4j_password: password123
project_id: my-project-name  # Optional: override auto-detection

Programmatic Configuration

from guardkit.knowledge.graphiti_client import GraphitiConfig, GraphitiClient

config = GraphitiConfig(
    enabled=True,
    neo4j_uri="bolt://localhost:7687",
    neo4j_user="neo4j",
    neo4j_password="password123",
    project_id="my-app"  # Explicit project ID
)

client = GraphitiClient(config)
await client.initialize()

Advanced Configuration

Multi-Project Setup

For development environments with multiple projects:

# Project A
config_a = GraphitiConfig(project_id="project-a")
client_a = GraphitiClient(config_a)
await client_a.initialize()

# Project B
config_b = GraphitiConfig(project_id="project-b")
client_b = GraphitiClient(config_b)
await client_b.initialize()

# Both clients share same Neo4j instance
# Knowledge is isolated by project_id prefix

Disabling Auto-Detection

# Disable auto-detection (use system scope)
config = GraphitiConfig(project_id=None)
client = GraphitiClient(config, auto_detect_project=False)

# This client operates in system scope only
# All groups must be explicitly system groups

Working with Namespaces

Adding Knowledge

Project-Specific Knowledge

# Automatically prefixed
await client.add_episode(
    name="Feature Spec",
    episode_body="User authentication using OAuth2",
    group_id="feature_specs"  # → "my-project__feature_specs"
)

# Explicit project scope
await client.add_episode(
    name="Custom Knowledge",
    episode_body="Domain-specific rules",
    group_id="custom_group",
    scope="project"  # → "my-project__custom_group"
)

System-Level Knowledge

# System group (no prefix)
await client.add_episode(
    name="Quality Gate",
    episode_body="Minimum 80% test coverage required",
    group_id="quality_gate_configs"  # → "quality_gate_configs"
)

# Explicit system scope
await client.add_episode(
    name="Template",
    episode_body="React component pattern",
    group_id="custom_template",
    scope="system"  # → "custom_template"
)

Searching Knowledge

# Search current project only
results = await client.search(
    query="authentication",
    group_ids=["project_architecture", "feature_specs"]
    # → ["my-project__project_architecture", "my-project__feature_specs"]
)
# Search system knowledge only
results = await client.search(
    query="quality gates",
    group_ids=["quality_gate_configs", "guardkit_templates"]
    # → ["quality_gate_configs", "guardkit_templates"] (no prefix)
)
# Search both project and system knowledge
results = await client.search(
    query="patterns",
    group_ids=[
        "project_architecture",  # Project group (prefixed)
        "guardkit_patterns"      # System group (not prefixed)
    ]
    # → ["my-project__project_architecture", "guardkit_patterns"]
)
# Search specific other projects (explicit prefixes)
results = await client.search(
    query="shared patterns",
    group_ids=[
        "project-a__architecture",  # Explicit prefix
        "project-b__architecture",  # Explicit prefix
        "guardkit_patterns"         # System group
    ]
)

FalkorDB limitation: On FalkorDB, group_ids=None searches only the default graph (typically empty), not all graphs. You must pass explicit group_ids to search across multiple groups. This differs from Neo4j where group_ids=None searches all groups.

# Neo4j: searches all groups
# FalkorDB: searches default graph only (returns 0 results)
results = await client.search(
    query="architecture",
    group_ids=None
)

# FalkorDB: explicitly list all groups to search
results = await client.search(
    query="architecture",
    group_ids=[
        "project_architecture",   # Project group (prefixed)
        "architecture_decisions",  # System group
        "feature_build_architecture",
        "guardkit_patterns",
    ]
)

Preventing Double-Prefixing

The client automatically detects and prevents double-prefixing:

project_id = "my-app"

# Already prefixed
await client.add_episode(
    name="Episode",
    episode_body="Content",
    group_id="my-app__project_overview"  # Already has prefix
)
# Result: "my-app__project_overview" (not "my-app__my-app__project_overview")

# Detection logic
def _is_already_prefixed(group_id: str) -> bool:
    """Check if group_id already has {project_id}__ prefix."""
    return f"{self.project_id}__" in group_id

MCP Access and Group ID Namespacing

When accessing the knowledge graph via the Graphiti MCP server (Claude Code sessions), the group ID behaviour differs from the Python client.

The Key Difference

Python client auto-prefixes project groups:

# Python client — pass logical (unprefixed) name
client.search(group_ids=["project_architecture"])
# → internally searches: "guardkit__project_architecture"

MCP server is pass-through — no auto-prefix:

# MCP server — must pass fully qualified group ID
mcp__graphiti__search_nodes(group_ids=["guardkit__project_architecture"])  # Correct ✅
mcp__graphiti__search_nodes(group_ids=["project_architecture"])  # Wrong ❌ → returns nothing

Why This Difference Exists

The MCP server (graphiti-core) has no knowledge of your project's project_id beyond what you configure in --group-id. It does not apply prefixes automatically. The Python client's GraphitiClient.get_group_id() method is a GuardKit abstraction on top of graphiti-core that handles prefixing transparently.

Using Both Access Methods Together

When a project uses both the Python client (CLI / AutoBuild) and the MCP server (Claude Code sessions), the same data is accessible from both — as long as group IDs are specified correctly:

Access Method How to Reference Project Groups
Python client Logical name: project_architecture → auto-prefixed
MCP server Full name: guardkit__project_architecture → explicit
System groups Same in both: product_knowledge, command_workflows

Group ID Reference for MCP Use

The .claude/rules/graphiti-knowledge-graph.md file in each project contains the complete list of group IDs to use in MCP tool calls. This file is loaded automatically by Claude Code and instructs it to always pass explicit group_ids with the correct prefix.

If you add a new project group via the Python client (e.g., task_outcomes), it will be stored as {project_id}__task_outcomes in FalkorDB. To search it via MCP, pass that full name: guardkit__task_outcomes.

Common Mistake: Searching Without group_ids

Both the Python client and MCP server require explicit group_ids for FalkorDB searches. Omitting group_ids returns no results (FalkorDB limitation — unlike Neo4j which searches all groups):

# Wrong — returns 0 results on FalkorDB
mcp__graphiti__search_nodes(query="authentication patterns")

# Correct — pass all relevant group_ids
mcp__graphiti__search_nodes(
    query="authentication patterns",
    group_ids=["product_knowledge", "guardkit__project_architecture"]
)

Best Practices

1. Use Auto-Detection in Development

# Good: Let directory name drive project ID
await init_graphiti()  # Uses current directory

# Avoid: Hardcoding project IDs in code
config = GraphitiConfig(project_id="hardcoded-project")

Rationale: Auto-detection works seamlessly with Conductor workspaces and multi-project setups.

2. Override with YAML for Deployment

# .guardkit/graphiti.yaml
project_id: production-auth-service

Rationale: Deployment environments need stable, explicit IDs.

3. Use Environment Variables for Multi-Tenancy

# Tenant A
export GUARDKIT_PROJECT_ID=tenant-a
python app.py

# Tenant B
export GUARDKIT_PROJECT_ID=tenant-b
python app.py

Rationale: Same codebase, different namespaces.

4. Prefix Custom Groups Explicitly

# Good: Explicit scope for custom groups
await client.add_episode(
    name="Custom",
    episode_body="Data",
    group_id="my_custom_group",
    scope="project"  # Explicit
)

# Okay: Relies on default (project scope)
await client.add_episode(
    name="Custom",
    episode_body="Data",
    group_id="my_custom_group"  # Defaults to project
)

5. Use System Groups for Templates

# Good: Templates are system-level
await client.add_episode(
    name="React Component Template",
    episode_body="Standard component structure",
    group_id="guardkit_templates"  # System group
)

# Bad: Templates in project scope
await client.add_episode(
    name="React Component Template",
    episode_body="Standard component structure",
    group_id="project_templates"  # Isolated to project
)

6. Search Narrowly

# Good: Specific groups
results = await client.search(
    query="auth",
    group_ids=["project_architecture"]  # Narrow search
)

# Avoid: Global search (noisy results)
results = await client.search(
    query="auth",
    group_ids=None  # All groups
)

FAQ

Q: What happens if I don't set a project_id?

A: Depends on configuration:

# Auto-detection enabled (default)
client = GraphitiClient(config)
# Uses current directory name as project_id

# Auto-detection disabled
client = GraphitiClient(config, auto_detect_project=False)
# project_id is None
# Can only use system groups
# Project groups will raise ValueError

Q: Can I change project_id after client creation?

A: No, GraphitiConfig is immutable (frozen dataclass):

config = GraphitiConfig(project_id="original")
config.project_id = "new"  # ❌ Raises AttributeError

# Must create new config and client
new_config = GraphitiConfig(project_id="new")
new_client = GraphitiClient(new_config)

Q: How do I share knowledge between projects?

A: Use explicit cross-project group IDs or system groups:

# Option 1: System groups
await client.add_episode(
    name="Shared Pattern",
    episode_body="Common approach",
    group_id="guardkit_patterns",  # System group
    scope="system"
)

# Option 2: Cross-project search
results = await client.search(
    query="pattern",
    group_ids=[
        "project-a__architecture",  # Project A
        "project-b__architecture"   # Project B
    ]
)

Q: What if two projects have the same directory name?

A: Use explicit configuration:

# Project A: .guardkit/graphiti.yaml
project_id: project-a-auth

# Project B: .guardkit/graphiti.yaml
project_id: project-b-auth

Or use environment variables:

# Workspace 1
cd workspace-1/auth
export GUARDKIT_PROJECT_ID=workspace-1-auth

# Workspace 2
cd workspace-2/auth
export GUARDKIT_PROJECT_ID=workspace-2-auth

Q: Can I use special characters in project_id?

A: Only alphanumeric and hyphens:

# Valid
GraphitiConfig(project_id="my-project-123")  # ✅

# Invalid (raises ValueError)
GraphitiConfig(project_id="my@project")      # ❌
GraphitiConfig(project_id="my_project")      # ❌
GraphitiConfig(project_id="my.project")      # ❌

Normalization auto-fixes auto-detected IDs:

# Directory: "my_project@v2.0"
# Normalized: "my-projectv20"

Q: How do I list all groups for a project?

A: Query Neo4j directly or use client methods:

# Search all project groups
results = await client.search(
    query="",  # Empty query
    group_ids=[
        "project_overview",
        "project_architecture",
        "feature_specs",
        "project_decisions",
        "project_constraints",
        "domain_knowledge"
    ]
)

# Or use Neo4j Cypher query (advanced)
# MATCH (e:Episode)
# WHERE e.group_id STARTS WITH "my-project__"
# RETURN DISTINCT e.group_id

Q: What's the performance impact of prefixing?

A: Negligible:

  • Prefixing: Simple string concatenation (O(1))
  • Storage: ~10-20 extra bytes per group ID
  • Indexing: Neo4j indexes on group_id handle prefixes efficiently
  • Search: No performance difference vs unprefixed groups

Benchmark (10,000 episodes):

Unprefixed: 1.23s
Prefixed:   1.24s (0.8% slower)

Q: Can I migrate existing knowledge to namespaced groups?

A: Yes, use a migration script:

async def migrate_to_namespaces(client, old_group, new_project_id):
    """Migrate knowledge from unprefixed to prefixed groups."""
    # Get all episodes in old group
    episodes = await client.search(query="", group_ids=[old_group])

    # Re-add with project prefix
    for episode in episodes:
        await client.add_episode(
            name=episode.name,
            episode_body=episode.content,
            group_id=old_group,  # Will be prefixed with new_project_id
            project_id=new_project_id
        )

    # Optionally: Delete old unprefixed episodes

See Also


Implementation Reference

File Locations

  • Client: guardkit/knowledge/graphiti_client.py
  • Config: guardkit/knowledge/config.py
  • Tests:
  • tests/knowledge/test_graphiti_client_project_id.py (54 tests)
  • tests/knowledge/test_graphiti_group_prefixing.py (37 tests)

Key Functions

# Normalization
def normalize_project_id(name: str) -> str:
    """Normalize project ID to valid format."""

# Group ID prefixing
def get_group_id(self, group_name: str, scope: str = "project") -> str:
    """Get correctly prefixed group ID."""

# Project group detection
def is_project_group(self, group_name: str) -> bool:
    """Check if group should be prefixed."""

# Auto-detection
def get_current_project_name() -> str:
    """Get current directory name for auto-detection."""

Last Updated: 2026-01-31 Version: 1.0.0 Status: Production-ready