Entity Binding & Relationship Graph Architecture
Date: 2026-06-09
Version: 1.0.0
Authority: Nirmal (Founder)
Status: Canonical Reference
Overview
The Spine is IntegrateWise's relationship graph - the single source of truth that binds emails, documents, chats, and all entities together. This document describes how entity resolution, binding, and relationships work across the entire system.
The Three Layers
Layer 1: Structured Truth (Spine Entities)
- Source: Flow A (Connectors → Normalizer → Spine)
- Writer: Pipeline service (SOLE writer to Spine)
- Storage: D1 (edge cache) + Supabase (fortress backup for promoted users)
- Entity Types: Person, Account, Deal, Task, Ticket, Invoice, Project, etc.
- How It Works: HubSpot Contact + Jira Reporter + Gmail sender → SAME Person entity
Layer 2: Unstructured Context (Linked to Entities)
- Source: Flow B (Documents, emails, Slack, meetings, files)
- Writer: Knowledge service
- Storage: R2 (raw content) + Vectorize (embeddings) + D1 (metadata + entity_refs)
- Table:
context_extractionswithentity_refscolumn - How It Works: Email about Account X → linked to Account X via entity_refs
Layer 3: AI Memory (Linked to Entities)
- Source: Flow C (Twin sessions, AI learnings)
- Writer: Triage Bot (SOLE writer to memory after governance)
- Storage: Supabase
memory.*tables + D1 mirror + Vectorize - Tables:
conversational_memory,org_memory,personal_memory - How It Works: Twin learns about Account X → memory entry linked to Account X
Entity Resolution (How We Bind)
The 18 Core Traits
Every entity from every tool is analyzed for these traits:
const CORE_TRAITS = [
"name",
"email",
"domain",
"phone",
"address",
"company",
"title",
"identifier",
"status",
"stage",
"amount",
"date",
"assignee",
"priority",
"description",
"url",
"type",
"category",
];
The 7 Canonical Resource Types
All entities map to one of 7 types:
- Person - HubSpot Contact, Salesforce Lead, Jira Reporter, Gmail sender
- Account - HubSpot Company, Salesforce Account, Stripe Customer
- Deal - HubSpot Deal, Salesforce Opportunity, PandaDoc Proposal
- Task - Jira Issue, Asana Task, GitHub Issue, Linear Issue
- Communication - Email, Slack message, Intercom conversation
- Document - Google Doc, Notion page, Confluence page, PDF upload
- Event - Calendar meeting, Zoom call, webhook event
Resolution Algorithm
Normalizer Stage 4: Entity Resolution
For each incoming entity:
1. Extract traits (name, email, domain, etc.)
2. Detect resource type (Person, Account, Deal, etc.)
3. Query Spine for potential matches:
- Exact email match (high confidence)
- Domain + name fuzzy match (medium confidence)
- Identifier match (tool-specific ID, low confidence)
4. If match found → UPDATE existing entity
5. If no match → CREATE new entity
6. Record provenance: which tools contributed to this entity
Relationship Graph
Core Relationships Table
entity_relationships (D1)
CREATE TABLE entity_relationships (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
from_entity_id TEXT NOT NULL, -- spine_id
from_entity_type TEXT NOT NULL, -- Person, Account, etc.
to_entity_id TEXT NOT NULL, -- spine_id
to_entity_type TEXT NOT NULL,
relationship_type TEXT NOT NULL, -- see types below
strength REAL DEFAULT 1.0, -- 0.0-1.0 confidence
metadata JSONB, -- context, source, timestamps
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_from_entity (from_entity_id),
INDEX idx_to_entity (to_entity_id),
INDEX idx_tenant (tenant_id),
INDEX idx_relationship_type (relationship_type)
);
Relationship Types
Primary relationships (auto-detected by Normalizer):
const RELATIONSHIP_TYPES = {
// Person → Account
works_at: "Person works at Account",
contacts_with: "Person is contact at Account",
// Person → Deal
owns_deal: "Person owns Deal",
involved_in: "Person involved in Deal",
// Account → Deal
has_deal: "Account has Deal",
// Person → Task
assigned_to: "Person assigned to Task",
created_task: "Person created Task",
// Account → Task
related_to_account: "Task related to Account",
// Communication → Any
mentions: "Communication mentions Entity",
about: "Communication about Entity",
// Document → Any
references: "Document references Entity",
belongs_to: "Document belongs to Entity",
// Memory → Any
learned_about: "AI learned about Entity",
};
Context Linking (Flow B)
How Emails/Docs/Chats Get Linked
context_extractions table (D1):
CREATE TABLE context_extractions (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
content_type TEXT NOT NULL, -- email, slack, doc, meeting
source_tool TEXT NOT NULL, -- gmail, slack, notion, etc.
source_id TEXT NOT NULL, -- original ID from source
title TEXT,
summary TEXT, -- AI-generated summary
content_hash TEXT, -- R2 object key
entity_refs JSONB NOT NULL, -- [{entity_id, entity_type, confidence}]
extracted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_entity_refs (entity_refs), -- GIN index for JSONB queries
INDEX idx_tenant (tenant_id),
INDEX idx_source (source_tool, source_id)
);
Example: Email about Account
{
"id": "ctx_abc123",
"tenant_id": "iw-customer-zero",
"content_type": "email",
"source_tool": "gmail",
"source_id": "msg_xyz789",
"title": "RE: Q4 Renewal Discussion",
"summary": "Discussion about Q4 contract renewal with pricing options",
"content_hash": "r2://emails/msg_xyz789.json",
"entity_refs": [
{
"entity_id": "spine_account_001",
"entity_type": "Account",
"confidence": 0.95,
"extraction_method": "email_domain_match"
},
{
"entity_id": "spine_person_042",
"entity_type": "Person",
"confidence": 0.98,
"extraction_method": "email_sender_match"
},
{
"entity_id": "spine_deal_017",
"entity_type": "Deal",
"confidence": 0.85,
"extraction_method": "subject_line_analysis"
}
],
"extracted_at": "2026-06-09T10:30:00Z"
}
Entity Extraction Process
Knowledge Service Pipeline:
1. Receive document/email/chat
2. Extract text content
3. Run AI extraction (via Workers AI + OpenRouter):
- Identify mentioned entities (people, accounts, deals)
- Extract key topics and intents
- Generate summary
4. Query Spine for entity matches:
- Email address → Person
- Company name → Account
- Deal ID / subject keywords → Deal
5. Create context_extractions record with entity_refs
6. Store raw content in R2
7. Embed and index in Vectorize
8. Update Entity 360 cache in D1
Entity 360 View
What It Is
Entity 360 is the unified view of everything related to an entity:
- Structured data (Spine)
- Unstructured context (linked emails, docs, chats)
- AI memory (learnings, signals)
- Relationship graph (connected entities)
How It's Built
L2 Service: Entity 360 Worker
async function buildEntity360(entityId: string, tenantId: string, env: Env) {
// 1. Get core entity from Spine (D1)
const entity = await env.D1.prepare("SELECT * FROM spine_entities WHERE id = ? AND tenant_id = ?")
.bind(entityId, tenantId)
.first();
// 2. Get relationships
const relationships = await env.D1.prepare(
`
SELECT * FROM entity_relationships
WHERE (from_entity_id = ? OR to_entity_id = ?)
AND tenant_id = ?
`
)
.bind(entityId, entityId, tenantId)
.all();
// 3. Get linked context (emails, docs, chats)
const context = await env.D1.prepare(
`
SELECT * FROM context_extractions
WHERE entity_refs @> ?
AND tenant_id = ?
ORDER BY extracted_at DESC
LIMIT 50
`
)
.bind(JSON.stringify([{ entity_id: entityId }]), tenantId)
.all();
// 4. Get AI memory
const memory = await env.D1.prepare(
`
SELECT * FROM org_memory
WHERE metadata->>'entity_id' = ?
AND tenant_id = ?
AND governance_state = 'approved'
ORDER BY updated_at DESC
LIMIT 20
`
)
.bind(entityId, tenantId)
.all();
// 5. Get signals
const signals = await env.D1.prepare(
`
SELECT * FROM signals
WHERE entity_id = ?
AND tenant_id = ?
AND processed = 0
ORDER BY created_at DESC
LIMIT 10
`
)
.bind(entityId, tenantId)
.all();
return {
entity,
relationships: relationships.results,
context: context.results,
memory: memory.results,
signals: signals.results,
_cached_at: new Date().toISOString(),
};
}
MCP Tool Exposure
Spine Tools (Available to Twin via MCP)
// Read single entity + relationships
spine.entity.get({ entity_id: "spine_account_001" });
// Search entities by trait/type
spine.entity.search({
type: "Account",
traits: { domain: "acme.com" },
});
// List relationships for an entity
spine.relationship.list({
entity_id: "spine_person_042",
relationship_type: "works_at",
});
// Get Entity 360 view
spine.entity360.get({ entity_id: "spine_account_001" });
Data Flow Summary
Flow A: Structured Entities (Repeat Loop)
HubSpot → Connector → Loader → Pipeline (Normalizer 8 stages)
→ Spine D1 (entity resolution + relationships)
→ Signal detection → Think → HITL → Act → back to HubSpot
Flow B: Unstructured Context (No Repeat Loop)
Gmail/Slack/Docs → Connector → Loader → Knowledge service
→ Extract entities → Link via entity_refs → context_extractions D1
→ R2 (raw content) + Vectorize (embeddings)
→ Enriches Entity 360 (read-only, no loop back)
Flow C: AI Memory (No Repeat Loop, Governed)
Twin session → Twin proposes memory → Triage Bot scores
→ HITL approval → Triage Bot writes org_memory
→ Linked to entities → Vectorize (retrieval)
→ Twin reads via memory.* MCP tools
Current Implementation Status
✅ Implemented (Phase 1)
- Spine entity resolution (Normalizer stages 1-8)
- Relationship graph (entity_relationships table)
- Context extraction (context_extractions table)
- Entity 360 caching (D1)
- Memory linking (org_memory metadata)
- MCP tools: spine.entity.get, spine.relationship.list
⏳ Pending (Phase 2-4)
- Auto-linking for emails (Gmail connector Flow B)
- Auto-linking for Slack messages
- Auto-linking for documents (Drive, Notion)
- Relationship strength scoring algorithm
- Entity merge/split UI
- Relationship visualization
Key Rules
-
Single Source of Truth: Spine entities are canonical. Everything else links TO them, never replaces them.
-
One Writer Per Layer:
- Spine: Pipeline service only
- Context: Knowledge service only
- Memory: Triage Bot only
-
No Direct Access: Twin never writes to Spine. Twin proposes → Govern approves → Act executes → Pipeline writes.
-
Entity Refs Everywhere: Every piece of unstructured content MUST have entity_refs. No orphaned context.
-
Provenance Tracking: Every entity records which tools contributed to it. Every relationship records its source.
-
Edge-First: D1 for speed, Supabase for durability (promoted users only).
Related Documents
INTEGRATEWISE_PRODUCT_ARCHITECTURE.md- Overall system designMCP_ADK_ROUTING_SPINE_MEMORY_ARCHITECTURE.md- MCP tools and routingCANONICAL_TAXONOMY.md- S/U/V/CZ layersLAYER_OWNERSHIP_AND_TWIN_RUNTIME.md- Twin responsibilities
Document Status: CANONICAL REFERENCE
Last Updated: 2026-06-09
Next Review: After Phase 2 Queue Wiring