Most AI applications are built around a simple idea:
Give a model enough information, ask a good question, and hope it figures everything out.
Sometimes that works.
But once you start building agents, copilots, or enterprise AI products, you hit a predictable problem: the model can retrieve information, but it struggles to understand how that information fits together.
A pull request mentions a feature flag. A Slack thread explains why it exists. An incident reveals what happens when it fails. A Jira ticket documents the original requirement.
Individually, those artifacts are useful.
Together, they tell a story.
The problem is that most AI architectures treat them as isolated chunks of text.
Graph engineering is how we reconnect the story.
Table of Contents
- What Is Graph Engineering?
- Why Traditional RAG Hits a Ceiling
- Graphs Preserve the Relationships AI Needs
- Graph Engineering Is Not Just Picking a Graph Database
- Building an Engineering Graph
- Where Graph Engineering Becomes Valuable
- The Hard Parts Nobody Puts in the Demo
- A Practical Starting Point
- The Bigger Shift
What Is Graph Engineering?
Graph engineering is the practice of designing systems around entities and the relationships between them.
Instead of representing information as disconnected documents, you explicitly model the structure of a domain.
A graph contains:
- Nodes: Things that exist, such as services, pull requests, incidents, engineers, and documents.
- Edges: Relationships between those things.
- Properties: Metadata describing nodes and edges.
- Temporal context: Information about when relationships formed, changed, or expired.
For example:
type EngineeringNode =
| { type: "service"; id: string; name: string }
| { type: "pull_request"; id: string; title: string }
| { type: "incident"; id: string; severity: string }
| { type: "engineer"; id: string; name: string };
type EngineeringEdge = {
sourceId: string;
targetId: string;
relationship:
| "OWNS"
| "MODIFIED"
| "DEPENDS_ON"
| "CAUSED"
| "RESOLVED";
createdAt: Date;
confidence?: number;
};
These structures make relationships queryable.
Instead of asking:
Which documents mention the payments service?
You can ask:
Which recent changes affected services that depend on payments, and have any of those services been involved in incidents?
That is a different category of question.
It requires connected reasoning, not just text similarity.
Why Traditional RAG Hits a Ceiling
Retrieval-augmented generation typically follows this workflow:
- Break documents into chunks.
- Convert each chunk into an embedding.
- Store embeddings in a vector database.
- Retrieve chunks similar to the user’s question.
- Pass those chunks to a language model.
This architecture is useful, especially when the goal is to find relevant passages.
But similarity is not the same thing as understanding.
Imagine asking:
Is it safe to remove this Redis cache?
A conventional RAG system might retrieve:
- A pull request introducing Redis.
- Documentation mentioning caching.
- An incident involving timeouts.
- A Slack discussion about checkout performance.
Those documents may contain the answer.
But the system still has to infer how they relate.
Did the cache prevent the timeout? Was the incident before or after Redis was introduced? Does checkout still depend on it? Did a later architecture change make it obsolete?
If those connections are not modeled, the language model has to reconstruct them from whatever happens to fit inside its context window.
Sometimes it gets there.
Sometimes it confidently tells you to delete production infrastructure.
That is not exactly the kind of excitement we want.
Graphs Preserve the Relationships AI Needs
Now imagine representing the same information as connected entities:
- A pull request introduced the Redis cache.
- The Redis cache supports the checkout service.
- A timeout incident affected the checkout service.
- The pull request responded to that incident.
- The checkout service depends on the Payments API.
- An engineer authored the original change.
In TypeScript, those connections might look like this:
const engineeringGraph = {
nodes: [
{ id: "cache:redis-checkout", type: "cache" },
{ id: "service:checkout", type: "service" },
{ id: "service:payments", type: "service" },
{ id: "pr:482", type: "pull_request" },
{ id: "incident:52", type: "incident" },
{ id: "engineer:bobby", type: "engineer" },
],
edges: [
{
source: "pr:482",
relationship: "INTRODUCED",
target: "cache:redis-checkout",
},
{
source: "cache:redis-checkout",
relationship: "SUPPORTS",
target: "service:checkout",
},
{
source: "incident:52",
relationship: "AFFECTED",
target: "service:checkout",
},
{
source: "pr:482",
relationship: "RESPONDED_TO",
target: "incident:52",
},
{
source: "service:checkout",
relationship: "DEPENDS_ON",
target: "service:payments",
},
{
source: "engineer:bobby",
relationship: "AUTHORED",
target: "pr:482",
},
],
};
The AI system no longer has to guess whether these artifacts are related.
The relationships are part of the data model.
It can traverse the graph, inspect the evidence, and produce an answer such as:
The Redis cache was introduced after checkout timeouts caused by repeated calls to the Payments API. Checkout still depends on the cache, and no later change has replaced that behavior. Removing it could increase latency and recreate the original failure condition.
More importantly, every part of that answer can point back to supporting evidence.
That is what makes a graph useful: not prettier storage, but better reasoning and explainability.
Graph Engineering Is Not Just Picking a Graph Database
One common mistake is treating graph engineering as a storage decision.
It is not.
You can represent graphs using:
- PostgreSQL tables.
- Recursive SQL queries.
- Graph databases.
- Search indexes.
- Application-layer adjacency lists.
- Hybrid architectures combining several approaches.
The hard part is deciding:
- Which entities matter?
- Which relationships are meaningful?
- Where does each relationship come from?
- How confident are we that it is correct?
- How do we handle relationships that change over time?
- How do we resolve duplicate identities across systems?
- What evidence supports each connection?
A graph database does not answer those questions for you.
Graph engineering is the discipline of designing the answers.
Building an Engineering Graph
Let’s make this concrete.
Suppose you want to build an AI assistant that helps engineers understand an unfamiliar codebase.
Your data sources might include GitHub, Slack, Jira, deployment logs, and incident reports.
1. Define the Core Entities
Start with the objects engineers already reason about:
type NodeType =
| "repository"
| "file"
| "service"
| "pull_request"
| "commit"
| "deployment"
| "incident"
| "ticket"
| "engineer"
| "conversation";
Keep the initial schema narrow.
You do not need to model every conceivable object before the system becomes useful.
Pick the questions you want to answer first, then model the entities required to answer them.
2. Define Relationships
Relationships capture how engineering work actually happens.
type RelationshipType =
| "AUTHORED"
| "REVIEWED"
| "MODIFIED"
| "MENTIONS"
| "DEPENDS_ON"
| "DEPLOYED"
| "TRIGGERED"
| "RESOLVED"
| "DISCUSSED_IN"
| "IMPLEMENTS";
For example:
const relationships = [
{
source: "engineer:bobby",
relationship: "AUTHORED",
target: "pr:482",
},
{
source: "pr:482",
relationship: "MODIFIED",
target: "service:checkout",
},
{
source: "pr:482",
relationship: "IMPLEMENTS",
target: "ticket:ENG-913",
},
{
source: "incident:52",
relationship: "DISCUSSED_IN",
target: "conversation:slack-incident-52",
},
];
Now the assistant can connect a code change to its author, its business requirement, and the services it affected.
3. Attach Evidence to Relationships
Relationships should not exist simply because a model thought they sounded plausible.
A stronger graph stores evidence for every important edge.
type Evidence = {
source: "github" | "slack" | "jira" | "deployment_log";
reference: string;
excerpt?: string;
observedAt: Date;
};
type GraphRelationship = {
sourceId: string;
targetId: string;
type: RelationshipType;
confidence: number;
evidence: Evidence[];
};
For example:
const deploymentIncidentRelationship: GraphRelationship = {
sourceId: "deployment:2026-08-20-14-32",
targetId: "incident:52",
type: "TRIGGERED",
confidence: 0.91,
evidence: [
{
source: "deployment_log",
reference: "deployments/2026-08-20-14-32",
excerpt: "Checkout latency increased after deployment.",
observedAt: new Date("2026-08-20T14:36:00Z"),
},
{
source: "slack",
reference: "incident-channel/message/8831",
excerpt: "Errors started immediately after the checkout rollout.",
observedAt: new Date("2026-08-20T14:38:00Z"),
},
],
};
This matters because causation is often uncertain.
A deployment happening before an incident does not prove it caused the incident.
Good graph engineering distinguishes observation from inference and preserves confidence accordingly.
4. Model Time Explicitly
Software systems evolve.
An engineer who owned a service last year may not own it today. A dependency may have been removed. A workaround may no longer be necessary.
Without time, graphs become confidently outdated.
type TemporalRelationship = GraphRelationship & {
validFrom: Date;
validUntil?: Date;
};
This lets you ask questions such as:
const query = {
question: "Who owned checkout when incident 52 happened?",
at: new Date("2026-08-20T14:35:00Z"),
};
That is different from asking who owns checkout today.
The distinction matters during incident reviews, compliance investigations, and architecture analysis.
5. Combine Graph Traversal With Semantic Search
Graphs and vector search solve different problems.
Semantic search helps answer:
Which documents discuss checkout latency?
Graph traversal helps answer:
Which services depend on checkout, which deployments modified those services, and which incidents followed?
The strongest architecture often combines both.
async function answerEngineeringQuestion(question: string) {
const semanticMatches = await vectorSearch(question);
const entityIds = await extractEntities({
question,
documents: semanticMatches,
});
const graphContext = await graph.expand({
nodeIds: entityIds,
relationships: [
"DEPENDS_ON",
"MODIFIED",
"TRIGGERED",
"RESOLVED",
],
maxDepth: 2,
});
const evidence = await rankEvidence({
question,
documents: semanticMatches,
graph: graphContext,
});
return generateAnswer({
question,
graphContext,
evidence,
requireCitations: true,
});
}
Semantic retrieval finds relevant information.
Graph traversal supplies structure.
The model turns both into an explanation.
Where Graph Engineering Becomes Valuable
Graph-based architectures are especially useful when the answer depends on relationships across multiple systems.
Engineering Intelligence
An engineering graph can connect:
- Pull requests to tickets.
- Deployments to incidents.
- Services to dependencies.
- Code ownership to subject-matter expertise.
- Architecture decisions to the discussions that produced them.
That enables questions like:
Why does this service exist?
What might break if I change this function?
Who understands this part of the system?
Which release introduced the behavior customers are reporting?
This is the difference between searching your engineering history and actually understanding it.
AI Agents
Agents need more than a prompt and a toolbelt.
They need to understand:
- Who owns a task.
- Which systems they can access.
- What dependencies block execution.
- Which decisions require approval.
- How prior actions affected the current state.
A graph can represent those relationships explicitly, allowing agents to operate within a defined business context.
type AgentContext = {
employeeId: string;
assignedWorkflows: string[];
availableTools: string[];
approvalPolicyId: string;
relevantContacts: string[];
};
When this context comes from a graph, the agent can discover connections dynamically instead of relying on a giant, fragile system prompt.
For example:
async function buildAgentContext(employeeId: string) {
const assignments = await graph.findNeighbors({
nodeId: employeeId,
relationship: "ASSIGNED_TO",
});
const tools = await graph.findNeighbors({
nodeId: employeeId,
relationship: "AUTHORIZED_TO_USE",
});
const approvalPolicies = await graph.findNeighbors({
nodeId: employeeId,
relationship: "GOVERNED_BY",
});
return {
assignments,
tools,
approvalPolicies,
};
}
The agent’s behavior now reflects relationships defined elsewhere in the system.
Change the graph, and the available context changes with it.
Customer Intelligence
A customer graph might connect:
- Accounts.
- Users.
- Support tickets.
- Product activity.
- Contracts.
- Renewal dates.
- Sales conversations.
Now an AI assistant can explain not just that an account is at risk, but why:
This customer has an upcoming renewal, declining product usage, three unresolved support tickets, and a recent conversation expressing concern about reporting features.
That answer comes from relationships, not keyword overlap.
The Hard Parts Nobody Puts in the Demo
Graph engineering sounds clean on a whiteboard.
Production is messier.
Entity Resolution
The same person might appear as:
bobby@company.com
bobbyhalljr
Bobby Hall
@bobby
Are those the same person?
Sometimes yes. Sometimes absolutely not.
You need identity mapping, confidence scores, and correction mechanisms.
A basic identity model might look like this:
type IdentityRecord = {
canonicalId: string;
source: "github" | "slack" | "jira";
externalId: string;
email?: string;
displayName?: string;
confidence: number;
};
If identity resolution is wrong, every relationship downstream becomes less trustworthy.
Relationship Extraction
Some edges are explicit.
A GitHub API can tell you who authored a pull request.
Other edges require interpretation.
A Slack conversation might imply that a deployment contributed to an incident without ever stating it directly.
Those inferred relationships should be marked differently from directly observed facts.
type RelationshipOrigin = "observed" | "inferred";
type VerifiedRelationship = GraphRelationship & {
origin: RelationshipOrigin;
reviewedBy?: string;
};
Your users should be able to tell the difference between:
GitHub records Bobby as the author.
And:
The system inferred that this deployment contributed to the incident.
Those are not equally reliable claims.
Access Control
If an employee cannot access a private document, the AI system should not reveal information derived from that document through a graph traversal.
Permissions have to travel with the underlying data.
Otherwise, your intelligent knowledge layer becomes a very creative data leak.
type PermissionAwareEvidence = Evidence & {
allowedUsers?: string[];
allowedGroups?: string[];
visibility: "public" | "internal" | "restricted";
};
function canAccessEvidence(
evidence: PermissionAwareEvidence,
userId: string,
groups: string[],
): boolean {
if (evidence.visibility === "public") {
return true;
}
if (evidence.allowedUsers?.includes(userId)) {
return true;
}
return (
evidence.allowedGroups?.some((group) =>
groups.includes(group),
) ?? false
);
}
In a real system, permission checks should apply to nodes, edges, evidence, retrieval results, and generated answers.
Filtering only at the final step is not enough if restricted information influenced the output.
Freshness
A graph is only useful when it reflects reality closely enough for the decisions it supports.
That means designing for:
- Incremental updates.
- Deleted or changed source records.
- Webhook ingestion.
- Periodic reconciliation.
- Relationship expiration.
- Auditing and observability.
For example:
type GraphSyncEvent = {
source: "github" | "slack" | "jira";
eventType: "created" | "updated" | "deleted";
entityId: string;
occurredAt: Date;
};
async function handleGraphSync(event: GraphSyncEvent) {
if (event.eventType === "deleted") {
await graph.expireNode(event.entityId);
return;
}
const sourceRecord = await fetchSourceRecord(
event.source,
event.entityId,
);
await graph.upsertNode(sourceRecord);
await graph.refreshRelationships(sourceRecord);
}
The goal is not perfect real-time synchronization at all costs.
The goal is to keep the graph fresh enough for the decisions users make with it.
Query Explosion
Graphs can become expensive to traverse.
Following every relationship several levels deep can quickly produce huge, noisy neighborhoods.
Useful systems constrain traversal based on:
- Relationship type.
- Time window.
- Confidence.
- Permissions.
- Query intent.
- Maximum depth.
type TraversalOptions = {
startingNodeIds: string[];
relationshipTypes: RelationshipType[];
maxDepth: number;
minimumConfidence: number;
createdAfter?: Date;
};
const options: TraversalOptions = {
startingNodeIds: ["service:checkout"],
relationshipTypes: [
"DEPENDS_ON",
"MODIFIED",
"TRIGGERED",
],
maxDepth: 2,
minimumConfidence: 0.8,
createdAfter: new Date("2026-01-01"),
};
More context is not automatically better context.
A useful graph answers the question without dragging the entire organization into the prompt.
A Practical Starting Point
If you are building an AI product today, you probably do not need to launch with an enterprise knowledge graph.
Start smaller.
Choose three questions your users actually care about.
For an engineering assistant:
- Why was this code changed?
- What systems depend on it?
- Who can help me understand it?
Then model only the entities and relationships needed to answer those questions.
A simple PostgreSQL schema might be enough:
CREATE TABLE graph_nodes (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
properties JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE graph_edges (
id TEXT PRIMARY KEY,
source_id TEXT NOT NULL REFERENCES graph_nodes(id),
target_id TEXT NOT NULL REFERENCES graph_nodes(id),
relationship_type TEXT NOT NULL,
confidence DOUBLE PRECISION NOT NULL DEFAULT 1.0,
evidence JSONB NOT NULL DEFAULT '[]',
valid_from TIMESTAMPTZ NOT NULL DEFAULT NOW(),
valid_until TIMESTAMPTZ
);
CREATE INDEX graph_edges_source_idx
ON graph_edges (source_id);
CREATE INDEX graph_edges_target_idx
ON graph_edges (target_id);
CREATE INDEX graph_edges_relationship_idx
ON graph_edges (relationship_type);
With that foundation, you can begin querying direct relationships:
SELECT
edge.relationship_type,
target.id,
target.type,
target.properties
FROM graph_edges AS edge
JOIN graph_nodes AS target
ON target.id = edge.target_id
WHERE edge.source_id = 'service:checkout'
AND edge.valid_until IS NULL;
You can also explore dependencies recursively:
WITH RECURSIVE dependency_graph AS (
SELECT
source_id,
target_id,
relationship_type,
1 AS depth
FROM graph_edges
WHERE source_id = 'service:checkout'
AND relationship_type = 'DEPENDS_ON'
AND valid_until IS NULL
UNION ALL
SELECT
edge.source_id,
edge.target_id,
edge.relationship_type,
dependency_graph.depth + 1
FROM graph_edges AS edge
INNER JOIN dependency_graph
ON edge.source_id = dependency_graph.target_id
WHERE edge.relationship_type = 'DEPENDS_ON'
AND edge.valid_until IS NULL
AND dependency_graph.depth < 3
)
SELECT *
FROM dependency_graph;
For production systems, add cycle detection and tenant-aware access controls before relying on recursive traversal.
You can build a surprising amount without introducing another database into your infrastructure.
The important thing is not whether you adopt a graph database on day one.
It is whether your architecture preserves the relationships your application needs to reason about.
The Bigger Shift
For years, software systems have been good at recording events.
A commit happened.
A ticket was created.
A deployment shipped.
An incident occurred.
But recording an event is not the same as understanding its significance.
The value emerges when those events are connected:
This deployment changed a service that depends on a fragile integration, which caused an incident similar to one that occurred six months ago, and the engineer who previously resolved it now works on another team.
That is the difference between stored information and operational understanding.
As AI systems become responsible for more meaningful work, that difference will matter more.
The future is not just models with larger context windows.
It is systems that understand what information means, how it relates, and why it matters.
That is graph engineering.
What critical engineering knowledge is your team losing right now? See what Helix reveals →

Top comments (0)