Skip to main content

Context Chain Design

Info
Last Updated: 2026-01-08

Architecture of hierarchical context chains for multi-memory-space workflow coordination with cross-space delegation support.

Overview

Context chains enable hierarchical task delegation with cross-memory-space access control. Memory spaces can share workflow state without breaking isolation boundaries. Each context in a chain has visibility into its parent, children, and root context - even across memory space boundaries.

Context Chain Hierarchy
Root Context (depth=0)

Purpose: "Process customer refund" Memory Space: supervisor-space Data: amount: 500, userId: "user-123"

Child 1 (depth=1)

Purpose: "Approve refund" Memory Space: finance-space (Different space!) Can access: Root data (via grantedAccess) Participant: finance-bot

Child 2 (depth=1)

Purpose: "Send apology email" Memory Space: customer-relations-space Can access: Root data (via grantedAccess) Participant: relations-bot

Child 3 (depth=1)

Purpose: "Update CRM" Memory Space: crm-space Can access: Root data (via grantedAccess) Participant: crm-bot

Root context delegates to children across different memory spaces

Key Insight: Context chains enable cross-space delegation while maintaining memory space isolation - they're a controlled access mechanism, not a backdoor.


Data Structure

Context Document

{
_id: "ctx_abc123",

// Identity & hierarchy
contextId: "ctx_abc123",
parentId: "ctx_parent", // Can be in different memory space
rootId: "ctx_root", // Self if root
depth: 2, // 0=root

// Ownership & Isolation
memorySpaceId: "finance-space", // Memory space this context lives in
tenantId: "tenant-acme", // Multi-tenancy
userId: "user-123", // User association (GDPR-enabled)

// Purpose
purpose: "Approve $500 refund",
description: "Review and approve customer refund request",

// Children tracking
childIds: ["ctx_child1", "ctx_child2"],
participants: ["finance-space", "legal-space"], // Memory spaces involved

// Cross-space access control (NEW)
grantedAccess: [
{
memorySpaceId: "legal-space", // Legal space can access
scope: "read-only", // Read-only permission
grantedAt: 1729900000000,
},
{
memorySpaceId: "audit-space", // Audit space can access
scope: "context-only", // Only context data, not memories
grantedAt: 1729900100000,
},
],

// Optional: Link to originating conversation
conversationRef: {
conversationId: "conv-456",
messageIds: ["msg-089"],
},

// Workflow data (flexible)
data: {
amount: 500,
reason: "defective product",
ticketId: "TICKET-456",
approvalRequired: true,
// Any custom fields...
},

// Metadata
metadata: {
importance: 85,
tags: ["refund", "approval"],
},

// Status tracking
status: "active", // active | completed | cancelled | blocked

// Timestamps
createdAt: 1729900000000,
updatedAt: 1729900500000,
completedAt: null,

// Versioning
version: 2,
previousVersions: [
{
version: 1,
status: "active",
data: { ... },
timestamp: 1729900000000,
updatedBy: "finance-bot",
},
],
}

Hierarchy Management

Creating Root Context

// Create root context
const rootContext = await cortex.contexts.create({
purpose: "Process customer refund",
memorySpaceId: "supervisor-space",
userId: "user-123",
data: {
amount: 500,
ticketId: "TICKET-456",
},
});

// Create child context (automatically inherits rootId and depth)
const childContext = await cortex.contexts.create({
purpose: "Approve refund",
memorySpaceId: "finance-space",
parentId: rootContext._id, // Links to parent
data: {
approvalRequired: true,
},
});

Traversing the Chain

// Get complete chain from any context
export const getChain = query({
args: { contextId: v.id("contexts") },
handler: async (ctx, args) => {
const current = await ctx.db.get(args.contextId);

if (!current) {
throw new Error("CONTEXT_NOT_FOUND");
}

// Get root
const root = await ctx.db.get(current.rootId);

// Get parent
const parent = current.parentId ? await ctx.db.get(current.parentId) : null;

// Get children
const children = await Promise.all(
current.childIds.map((id) => ctx.db.get(id)),
);

// Get siblings
const siblings = parent
? await Promise.all(
parent.childIds
.filter((id) => id !== current._id)
.map((id) => ctx.db.get(id)),
)
: [];

// Get all ancestors (walk up)
const ancestors = [];
let node = parent;
while (node) {
ancestors.unshift(node);
node = node.parentId ? await ctx.db.get(node.parentId) : null;
}

// Get all descendants (recursive)
const descendants = await getAllDescendants(ctx, current._id);

return {
current,
root,
parent,
children,
siblings,
ancestors,
descendants,
depth: current.depth,
totalNodes: 1 + ancestors.length + descendants.length,
};
},
});

async function getAllDescendants(
ctx: any,
contextId: string,
): Promise<Context[]> {
const context = await ctx.db.get(contextId);
const children = await Promise.all(
context.childIds.map((id) => ctx.db.get(id)),
);

// Recursive
const grandchildren = await Promise.all(
children.map((child) => getAllDescendants(ctx, child._id)),
);

return [...children, ...grandchildren.flat()];
}

Status Management

Status Transitions

Info
Status transition validation is not currently enforced in the implementation. The update method accepts any status transition. This validation is planned for a future release.
// Planned: Valid transitions (not currently enforced)
const STATUS_TRANSITIONS = {
active: ["completed", "cancelled", "blocked"],
blocked: ["active", "cancelled"],
completed: [], // Terminal state
cancelled: [], // Terminal state
};

// Current implementation: Use SDK update method
await cortex.contexts.update(contextId, {
status: "completed",
data: { result: "success" },
});

Auto-Complete Parent

Info
Auto-completion of parent contexts when all children complete is not currently implemented. This feature is planned for a future release.
// Planned: When all children complete, complete parent
// This function does not exist in the current implementation
export const checkAndCompleteParent = mutation({
args: { contextId: v.id("contexts") },
handler: async (ctx, args) => {
// Implementation planned for future release
},
});

Context Propagation

Data Inheritance

Info
Automatic inherited data merging is not currently implemented. You can manually traverse the chain using getChain() to access parent data. This feature is planned for a future release.
// Planned: Children can access parent data automatically
// This function does not exist in the current implementation
export const getWithInheritedData = query({
args: { contextId: v.id("contexts") },
handler: async (ctx, args) => {
// Implementation planned for future release
},
});

// Current implementation: Manually traverse chain
const chain = await cortex.contexts.getChain(childContextId);

// Manually merge data from chain
const inheritedData = {};
for (const context of [chain.root, ...chain.ancestors, chain.current]) {
Object.assign(inheritedData, context.data || {});
}

console.log(inheritedData);
// {
// amount: 500, // From root
// ticketId: "TICKET-456", // From root
// approvedBy: "finance-agent", // From parent
// confirmationNumber: "REF-789", // From current
// }

Participant Propagation

Info
Automatic participant propagation up the chain is not currently implemented. Use addParticipant() to add participants to individual contexts. Chain-wide propagation is planned for a future release.
// Current implementation: Add participant to single context
await cortex.contexts.addParticipant(contextId, "legal-space");

// Planned: Add participant to context and all ancestors
// This function does not exist in the current implementation
export const addParticipantToChain = mutation({
args: {
contextId: v.id("contexts"),
memorySpaceId: v.string(),
},
handler: async (ctx, args) => {
// Implementation planned for future release
},
});

Conversation Linking

Contexts Reference Conversations

Info
There is no separate createFromConversation function. Use the standard create() method with conversationRef parameter.
// Current implementation: Create context with conversation reference
const context = await cortex.contexts.create({
purpose: "Process customer refund",
memorySpaceId: "supervisor-space",
userId: "user-123",
conversationRef: {
conversationId: "conv-456",
messageIds: ["msg-089"],
},
});

Retrieve Original Conversation

Info
Use get() with includeConversation: true option to retrieve context with conversation data.
// Current implementation: Get context with conversation
const result = await cortex.contexts.get(contextId, {
includeChain: true,
includeConversation: true,
});

// Result includes conversation and triggerMessages if conversationRef exists
if (result.conversation) {
console.log(result.conversation);
console.log(result.triggerMessages);
}

Memory Integration

Linking Memories to Contexts

Info
There is no separate storeWithContext function. Use the standard memory storage API and include contextId in metadata.
// Current implementation: Store memory with context reference
const context = await cortex.contexts.get(contextId);

const memory = await cortex.memories.store({
memorySpaceId: "finance-space",
content: "Refund approved for $500",
metadata: {
contextId: context._id, // ← Link to context
workflowPurpose: context.purpose,
},
});

Finding Memories by Context

Info
There is no getMemoriesForWorkflow function. Query memories by filtering on metadata.contextId manually.
// Current implementation: Find memories for a workflow
// Get all contexts in workflow
const chain = await cortex.contexts.getChain(rootContextId);
const allContextIds = [
chain.root._id,
...chain.descendants.map((c) => c._id),
];

// Query memories from all memory spaces involved
const allMemories = [];
for (const context of [chain.root, ...chain.descendants]) {
const memories = await cortex.memories.list({
memorySpaceId: context.memorySpaceId,
});

// Filter memories linked to this workflow
const workflowMemories = memories.filter(
(m) => m.metadata?.contextId && allContextIds.includes(m.metadata.contextId)
);

allMemories.push(...workflowMemories);
}

// Sort by creation time
allMemories.sort((a, b) => a.createdAt - b.createdAt);

Workflow Patterns

Sequential Workflow

// Step 1 -> Step 2 -> Step 3
async function createSequentialWorkflow(purpose: string, steps: any[]) {
// Create root
const root = await ctx.db.insert("contexts", {
purpose,
rootId: null,
depth: 0,
childIds: [],
status: "active",
...
});

await ctx.db.patch(root, { rootId: root });

// Create steps as siblings (all same parent)
const stepIds = [];
for (const step of steps) {
const stepContext = await cortex.contexts.create({
purpose: step.purpose,
memorySpaceId: step.memorySpaceId,
parentId: root._id,
data: { stepNumber: stepIds.length + 1 },
});

stepIds.push(stepContext._id);
}

// Update root with all children
await ctx.db.patch(root, { childIds: stepIds });

return root;
}

Approval Chain

// Request -> Manager -> Finance (nested)
async function createApprovalChain(request: any) {
// Level 1: Request
const requestCtx = await cortex.contexts.create({
purpose: "Expense approval request",
memorySpaceId: "employee-space",
data: { amount: request.amount },
});

// Level 2: Manager review
const managerCtx = await cortex.contexts.create({
purpose: "Manager review",
memorySpaceId: "manager-space",
parentId: requestCtx._id, // ← Nested
data: { approved: null }, // To be filled
});

// Level 3: Finance approval
const financeCtx = await cortex.contexts.create({
purpose: "Finance approval",
memorySpaceId: "finance-space",
parentId: managerCtx._id, // ← Nested deeper
data: { allocated: null },
});

return {
depth: 3,
chain: [requestCtx, managerCtx, financeCtx],
};
}

Parallel Workflow

// Fork: One parent, multiple parallel children
async function createParallelWorkflow(purpose: string, tasks: any[]) {
const root = await cortex.contexts.create({
purpose,
memorySpaceId: "supervisor-space",
});

// Create all children in parallel
const children = await Promise.all(
tasks.map((task) =>
cortex.contexts.create({
purpose: task.purpose,
memorySpaceId: task.memorySpaceId,
parentId: root._id,
data: task.data,
}),
),
);

return { root, children };
}

Query Patterns

Finding Contexts by Status

// Current implementation: Get all active workflows for a memory space
const activeWorkflows = await cortex.contexts.list({
memorySpaceId: "finance-space",
status: "active",
});

// Get blocked workflows (needs attention)
const blockedWorkflows = await cortex.contexts.list({
status: "blocked",
});

Finding by Depth

// Get all root contexts
export const getRoots = query({
handler: async (ctx) => {
return await ctx.db
.query("contexts")
.withIndex("by_depth", (q) => q.eq("depth", 0))
.collect();
},
});

// Get all leaf contexts (no children)
export const getLeaves = query({
handler: async (ctx) => {
const all = await ctx.db.query("contexts").collect();
return all.filter((c) => c.childIds.length === 0);
},
});

Finding by Conversation

// Current implementation: Get all workflows from a conversation
const contexts = await cortex.contexts.getByConversation("conv-456");

Orphan Detection

Finding Orphaned Contexts

export const findOrphaned = query({
handler: async (ctx) => {
const allContexts = await ctx.db.query("contexts").collect();
const orphaned = [];

for (const context of allContexts) {
if (context.parentId) {
// Check if parent exists
const parent = await ctx.db.get(context.parentId);

if (!parent) {
orphaned.push(context);
}
}
}

return orphaned;
},
});

Cleanup Orphans

export const cleanupOrphans = mutation({
handler: async (ctx) => {
const orphaned = await ctx.runQuery("contexts:findOrphaned");

for (const context of orphaned) {
// Option 1: Delete
await ctx.db.delete(context._id);

// Option 2: Promote to root
// await ctx.db.patch(context._id, {
// parentId: null,
// rootId: context._id,
// depth: 0,
// });
}

return { cleaned: orphaned.length };
},
});

GDPR Cascade

Contexts Support userId

// Create context for user
const context = await cortex.contexts.create({
purpose: "Handle user request",
memorySpaceId: "support-space",
userId: "user-123", // ← GDPR-enabled
});

// GDPR cascade (planned for Cloud Mode)
await cortex.users.delete("user-123", { cascade: true });

// Contexts with userId are deleted
// Children without explicit userId are preserved (workflow metadata)

Selective Deletion

export const deleteUserContexts = mutation({
args: { userId: v.string() },
handler: async (ctx, args) => {
// Find all contexts for user
const userContexts = await ctx.db
.query("contexts")
.withIndex("by_userId", (q) => q.eq("userId", args.userId))
.collect();

for (const context of userContexts) {
// Delete context and descendants
await deleteWithDescendants(ctx, context._id);
}

return { deleted: userContexts.length };
},
});

async function deleteWithDescendants(ctx: any, contextId: string) {
const context = await ctx.db.get(contextId);

// Delete all children first (recursive)
for (const childId of context.childIds) {
await deleteWithDescendants(ctx, childId);
}

// Delete this context
await ctx.db.delete(contextId);
}

Performance Considerations

Index Strategy

Required indexes:

  • by_memorySpace - Find memory space's contexts
  • by_userId - GDPR cascade
  • by_status - Filter by status
  • by_parentId - Get children
  • by_rootId - Get entire workflow

Compound indexes:

  • by_memorySpace_status - Memory space's active workflows (common query)

Depth Limits

Info
Depth limit enforcement is not currently implemented in the SDK. This validation is planned for a future release.
// Planned: Enforce maximum depth
const MAX_DEPTH = 10;

// Current implementation: No depth limit enforced
// You can manually check depth before creating child contexts
const parent = await cortex.contexts.get(parentId);
if (parent.depth >= MAX_DEPTH) {
throw new Error("Maximum context depth exceeded");
}

Lazy Loading Children

// Current implementation: Use getChain() for full chain, or getChildren() for children
// Get single context (lightweight)
const context = await cortex.contexts.get(contextId);

// Get full chain (includes children, ancestors, descendants)
const chain = await cortex.contexts.getChain(contextId);

// Get just children (with optional recursion)
const children = await cortex.contexts.getChildren(contextId, {
recursive: false, // Set to true for all descendants
});

Next Steps


Questions? Ask in GitHub Discussions.