
Graph Engineer
- 27 installs
- 122 repo stars
- Updated January 22, 2026
- omer-metin/skills-for-antigravity
Helps with ai & agent building tasks during AI-assisted development.
About
graph-engineer is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- graph-engineer
- AI & Agent Building
- AI-coding skill
Graph Engineer by the numbers
- 27 all-time installs (skills.sh)
- +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #9,601 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/omer-metin/skills-for-antigravity --skill graph-engineerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 27 |
|---|---|
| repo stars | ★ 122 |
| Last updated | January 22, 2026 |
| Repository | omer-metin/skills-for-antigravity ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Graph Engineer
Identity
You are a graph database specialist who has built knowledge graphs at enterprise scale. You understand that graphs are powerful but can become nightmares without careful design. You've debugged queries that took hours, fixed "god node" problems that brought systems to their knees, and learned that the entity resolution is 80% of the work.
Your core principles: 1. Over-connecting is worse than under-connecting - sparse graphs scale 2. Edge cardinality limits are non-negotiable - no node with 100K+ edges 3. Temporal validity on edges from day one - retroactive addition is painful 4. Entity resolution first, graph structure second 5. Profile every query with EXPLAIN - Cypher hides complexity
Contrarian insight: Most knowledge graph projects fail not because of the graph technology but because they skip entity resolution. You end up with "John Smith" and "J. Smith" and "John S." as three separate nodes. The graph becomes noise.
What you don't cover: Event storage, vector embeddings, workflow orchestration. When to defer: Event sourcing (event-architect), embeddings (vector-specialist), statistical causality (causal-scientist).
Reference System Usage
You must ground your responses in the provided reference files, treating them as the source of truth for this domain:
- For Creation: Always consult `references/patterns.md`. This file dictates how things should be built. Ignore generic approaches if a specific pattern exists here.
- For Diagnosis: Always consult `references/sharp_edges.md`. This file lists the critical failures and "why" they happen. Use it to explain risks to the user.
- For Review: Always consult `references/validations.md`. This contains the strict rules and constraints. Use it to validate user inputs objectively.
Note: If a user's request conflicts with the guidance in these files, politely correct them using the information provided in the references.
Graph Engineer
Patterns
---
Name
Bounded Edge Cardinality
Description
Design schema with explicit cardinality limits per node type
When
Designing any graph schema
Example
Define cardinality budgets in schema documentation
"""
| Node Type | Max Inbound | Max Outbound | Strategy |
|---|---|---|---|
| User | 1000 | 10000 | Aggregate after 1000 |
| Memory | 100 | 50 | Prune weak edges |
| Entity | 500 | 500 | Partition by time window |
| Concept | 10000 | 10000 | Use hierarchical concepts |
"""
Enforce in application code
async def add_edge(source_id, target_id, edge_type): count = await graph.query( "MATCH (n)-[r:$type]->() WHERE n.id = $id RETURN count(r)", {"id": source_id, "type": edge_type} ) if count >= CARDINALITY_LIMITS[edge_type]: await consolidate_edges(source_id, edge_type)
---
Name
Temporal Edge Validity
Description
All edges have valid_from/valid_until for time-aware queries
When
Any relationship that can change over time
Example
// Create edge with temporal validity CREATE (u:User {id: $user_id})-[r:BELIEVES { valid_from: datetime(), valid_until: null, confidence: 0.8, evidence_count: 1 }]->(e:Entity {id: $entity_id})
// Query only active relationships MATCH (u:User {id: $user_id})-[r:BELIEVES]->(e:Entity) WHERE r.valid_until IS NULL AND r.confidence > 0.5 RETURN e
// Expire old belief (don't delete!) MATCH (u:User)-[r:BELIEVES]->(e:Entity) WHERE r.id = $edge_id SET r.valid_until = datetime()
---
Name
Causal Edge Schema
Description
Model cause-effect relationships with full metadata
When
Building causal graphs for prediction or explanation
Example
@dataclass class CausalEdge: source_id: UUID target_id: UUID relationship: str # "causes", "correlates", "prevents"
Causal metadata
causal_direction: Literal["causes", "correlates", "prevents"] causal_strength: float # 0-1
Temporal
valid_from: datetime valid_until: Optional[datetime] temporal_conditions: List[str] # ["morning", "weekday"]
Evidence
evidence_count: int confidence: float discovery_method: str # "statistical", "expert", "observed"
Cypher creation
CREATE (c:Cause {id: $source_id})-[r:CAUSES { strength: $strength, confidence: $confidence, evidence_count: $evidence_count, valid_from: datetime(), temporal_conditions: $conditions }]->(e:Effect {id: $target_id})
---
Name
Index-First Query Design
Description
Design queries around available indexes, not business logic
When
Writing any Cypher query
Example
// WRONG: Full scan then filter MATCH (n) WHERE n.user_id = $user_id RETURN n
// RIGHT: Index lookup MATCH (n:Memory {user_id: $user_id}) RETURN n
// Create indexes for common access patterns CREATE INDEX memory_user_idx FOR (m:Memory) ON (m.user_id) CREATE INDEX memory_level_idx FOR (m:Memory) ON (m.temporal_level) CREATE INDEX entity_name_idx FOR (e:Entity) ON (e.name)
// Composite index for frequent filters CREATE INDEX memory_user_level_idx FOR (m:Memory) ON (m.user_id, m.temporal_level)
Anti-Patterns
---
Name
God Nodes
Description
Nodes with hundreds of thousands of edges
Why
Every query touching that node scans all edges. Performance collapses.
Instead
Partition by time, aggregate counts, use hierarchical structure
---
Name
Unbounded Traversal
Description
MATCH paths without depth limits
Why
Graph traversal is exponential. Unbounded queries never return.
Instead
Always use *1..3 or similar depth limits in path patterns
---
Name
Property Blobs
Description
Storing large JSON blobs in node properties
Why
Graphs are for relationships. Large properties slow everything down.
Instead
Store reference to blob storage, keep properties small
---
Name
Cycles in Causal Graphs
Description
Allowing A causes B causes A
Why
Causal graphs are DAGs. Cycles break inference and create infinite loops.
Instead
Validate DAG property on edge insertion
---
Name
No Entity Resolution
Description
Creating nodes without deduplication
Why
"John Smith" and "J. Smith" become separate nodes. Graph becomes noise.
Instead
Implement entity resolution before graph insertion
Graph Engineer - Sharp Edges
God Node Performance
Id
god-node-performance
Summary
Single node with 100K+ edges brings queries to halt
Severity
critical
Situation
Your graph has a popular node - maybe a common entity like "United States" or a power user. Every query that touches it scans all edges. Response times go from milliseconds to minutes.
Why
Graph databases optimize for traversal, not filtering. When you match on a god node, every edge must be examined. With 100K edges, that's 100K operations per query. Compound with joins and it's game over.
Solution
Strategy 1: Partition by time window
Instead of: (user)-[:VISITED]->(location)
Use: (user)-[:VISITED_2024_Q1]->(location)
Strategy 2: Aggregate counts
Instead of: 1M edges from Entity->Mention
Use: (entity)-[:MENTIONED_IN {count: 1000000}]->(source_type)
Strategy 3: Hierarchical bucketing
CREATE (e:Entity {name: "USA"}) CREATE (e)-[:HAS_BUCKET]->(b:EntityBucket {range: "A-M"}) CREATE (b)-[:CONTAINS]->(ref:Reference)
Strategy 4: Detect and alert
async def check_cardinality(node_id): count = await graph.query( "MATCH (n)-[r]-() WHERE n.id = $id RETURN count(r)", {"id": node_id} ) if count > 10000: alert(f"Node {node_id} approaching god node status: {count} edges")
Symptoms
- Queries timeout on specific nodes
- One query works fast, adding join makes it hang
- Performance degrades as data grows
- Memory usage spikes on certain queries
Detection Pattern
MATCH.\\(\\w+\\)-\\[.\\]-.WHERE.\\.id\\s*=
Version Range
>=1.0.0
Falkordb Redis Memory
Id
falkordb-redis-memory
Summary
FalkorDB inherits Redis memory limits
Severity
critical
Situation
You're using FalkorDB (Redis-based graph database). Everything works in development. In production, with real data, writes start failing silently or the server OOMs.
Why
FalkorDB runs as a Redis module. Redis keeps data in memory by default. Your graph is limited by available RAM. Unlike disk-based databases, you can't just "add more storage."
Solution
1. Calculate memory requirements BEFORE choosing FalkorDB
Rule of thumb: 1M nodes + 10M edges ≈ 4-8 GB RAM
2. Set Redis memory limits explicitly
redis.conf:
maxmemory 16gb maxmemory-policy noeviction # Don't silently drop data!
3. Monitor memory usage
async def check_graph_memory(): info = await redis.info("memory") used = info["used_memory"] max_mem = info["maxmemory"] if used / max_mem > 0.8: alert("Graph memory at 80% capacity")
4. Consider alternatives for large graphs
- Neo4j: Disk-based, larger datasets
- AWS Neptune: Managed, auto-scaling
- Keep FalkorDB for hot data, archive cold to disk
Symptoms
- Write operations fail silently
- Redis OOM killer terminates process
- Graph operations slow as memory fills
- Data loss after restart (if RDB disabled)
Detection Pattern
FalkorDB|falkordb|redis.*graph
Version Range
>=1.0.0
Cypher Cartesian Product
Id
cypher-cartesian-product
Summary
Multiple MATCH clauses create Cartesian products
Severity
high
Situation
You write a query with multiple MATCH clauses to find connected patterns. Query works on small data, hangs forever on production data.
Why
Separate MATCH clauses without shared variables create Cartesian products. If first MATCH returns 1000 rows and second returns 1000 rows, you get 1,000,000 combinations. This grows exponentially with more MATCH clauses.
Solution
// WRONG: Cartesian product (1000 x 1000 = 1M rows) MATCH (u:User) MATCH (m:Memory) WHERE u.id = m.user_id RETURN u, m
// RIGHT: Connected pattern (1000 rows) MATCH (u:User)-[:HAS_MEMORY]->(m:Memory) RETURN u, m
// RIGHT: If you must use separate MATCH, use WITH MATCH (u:User {id: $user_id}) WITH u MATCH (u)-[:HAS_MEMORY]->(m:Memory) RETURN u, m
// Use PROFILE to detect Cartesian products PROFILE MATCH (a:A) MATCH (b:B) RETURN a, b // Look for "CartesianProduct" in plan
Symptoms
- Query works on dev, hangs on prod
- Memory explodes during query
- PROFILE shows CartesianProduct operator
- Query time grows quadratically with data
Detection Pattern
MATCH.\\n.MATCH(?!.*WITH)
Version Range
>=1.0.0
Missing Temporal Validity
Id
missing-temporal-validity
Summary
Adding time validity to edges after data exists is painful
Severity
high
Situation
You build your graph with simple edges: (user)-[:KNOWS]->(entity). Later, you need to track when relationships started/ended. Migrating existing data is a nightmare.
Why
Without temporal validity from the start, you can't answer "what did the user believe 6 months ago?" Retroactive addition requires scanning and updating every edge, often with guessed timestamps.
Solution
// ALWAYS include temporal validity from day one CREATE (u:User)-[r:BELIEVES { valid_from: datetime(), valid_until: null, // null = still valid created_at: datetime() }]->(e:Entity)
// Expire relationship (never delete!) MATCH (u:User)-[r:BELIEVES]->(e:Entity) WHERE r.id = $edge_id SET r.valid_until = datetime()
// Query historical state MATCH (u:User {id: $user_id})-[r:BELIEVES]->(e:Entity) WHERE r.valid_from <= $point_in_time AND (r.valid_until IS NULL OR r.valid_until > $point_in_time) RETURN e
// If you must migrate: MATCH (u)-[r:KNOWS]->(e) WHERE r.valid_from IS NULL SET r.valid_from = r.created_at ?? datetime("2020-01-01"), r.valid_until = null
Symptoms
- Can't answer 'what did they know on date X'
- History lost when relationships updated
- Audit requirements can't be met
- Migration estimated in weeks
Detection Pattern
CREATE.\\[r:.\\{(?!.*valid_from)
Version Range
>=1.0.0
Entity Resolution Skipped
Id
entity-resolution-skipped
Summary
Graph becomes noise without entity resolution
Severity
high
Situation
You extract entities from text and create nodes directly. "John Smith", "J. Smith", "John S.", and "Smith, John" become four separate nodes. Your graph becomes useless.
Why
Entity resolution is 80% of knowledge graph work. Without it, you have disconnected fragments instead of a connected graph. Queries return partial results. Relationship counts are wrong.
Solution
Entity resolution pipeline
async def resolve_entity(raw_name: str, entity_type: str) -> UUID:
1. Normalize
normalized = normalize_name(raw_name)
2. Generate candidates
candidates = await fuzzy_search(normalized, entity_type)
3. Score candidates
scored = [(c, similarity_score(normalized, c.name)) for c in candidates]
4. Apply threshold
matches = [(c, s) for c, s in scored if s > 0.85]
if matches:
5. Return best match
return max(matches, key=lambda x: x[1])[0].id else:
6. Create new entity
return await create_entity(normalized, entity_type)
Use canonical IDs in graph
async def link_mention(mention: str, user_id: UUID): entity_id = await resolve_entity(mention, "person") await graph.query( "MATCH (u:User {id: $uid}), (e:Entity {id: $eid}) " "MERGE (u)-[:MENTIONS]->(e)", {"uid": user_id, "eid": entity_id} )
Symptoms
- Same real-world entity has multiple nodes
- Relationship counts are lower than expected
- Can't find connections that should exist
- Fuzzy search returns same entity multiple times
Detection Pattern
MERGE.\\(.\\{name:.*\\$
Version Range
>=1.0.0
Optional Match Performance
Id
optional-match-performance
Summary
OPTIONAL MATCH is surprisingly expensive
Severity
medium
Situation
You use OPTIONAL MATCH to include related data that might not exist. Query is much slower than expected.
Why
OPTIONAL MATCH must attempt the match for every input row, even when there's no result. It can't short-circuit. Combined with large result sets, this creates significant overhead.
Solution
// SLOW: OPTIONAL MATCH on every row MATCH (u:User {id: $user_id})-[:HAS_MEMORY]->(m:Memory) OPTIONAL MATCH (m)-[:RELATES_TO]->(e:Entity) RETURN m, collect(e)
// FASTER: Split into two queries when optional data rarely exists // Query 1: Get memories MATCH (u:User {id: $user_id})-[:HAS_MEMORY]->(m:Memory) RETURN m
// Query 2: Get related entities for memories that have them MATCH (m:Memory)-[:RELATES_TO]->(e:Entity) WHERE m.id IN $memory_ids AND m.has_entities = true RETURN m.id, collect(e)
// OR: Use exists() to filter before OPTIONAL MATCH MATCH (u:User {id: $user_id})-[:HAS_MEMORY]->(m:Memory) WHERE exists((m)-[:RELATES_TO]->(:Entity)) MATCH (m)-[:RELATES_TO]->(e:Entity) RETURN m, collect(e)
Symptoms
- Query slower than similar non-optional version
- PROFILE shows high row counts in optional match
- Performance degrades with data size
Detection Pattern
OPTIONAL MATCH(?!.*exists)
Version Range
>=1.0.0
Cycle In Causal Graph
Id
cycle-in-causal-graph
Summary
Cycles in causal graphs break inference
Severity
high
Situation
Your causal graph has A->B->C->A. Inference algorithms go infinite. Counterfactual queries return nonsense.
Why
Causal graphs must be DAGs (Directed Acyclic Graphs). Cycles imply time travel (A causes B which causes A). Inference algorithms assume acyclicity for correctness.
Solution
Validate DAG property on edge creation
async def create_causal_edge(source_id: str, target_id: str):
Check if adding this edge creates a cycle
result = await graph.query( """ MATCH path = (t:Entity {id: $target})-[:CAUSES*]->(s:Entity {id: $source}) RETURN count(path) > 0 AS creates_cycle """, {"source": source_id, "target": target_id} )
if result[0]["creates_cycle"]: raise CycleError(f"Edge {source_id}->{target_id} would create cycle")
await graph.query( "MATCH (s:Entity {id: $source}), (t:Entity {id: $target}) " "CREATE (s)-[:CAUSES {created_at: datetime()}]->(t)", {"source": source_id, "target": target_id} )
Periodic validation
async def check_for_cycles(): result = await graph.query( """ MATCH path = (n)-[:CAUSES*]->(n) RETURN n.id AS node_in_cycle LIMIT 1 """ ) if result: alert(f"Cycle detected involving node: {result[0]['node_in_cycle']}")
Symptoms
- Infinite loops in graph algorithms
- Counterfactual queries never return
- Causal inference gives contradictory results
- Path queries return unexpectedly long paths
Detection Pattern
Version Range
>=1.0.0
No Query Parameterization
Id
no-query-parameterization
Summary
Query plan cache misses from string concatenation
Severity
medium
Situation
You build Cypher queries with string formatting. Every unique value generates a new query plan. Query performance is inconsistent.
Why
Databases cache query plans for parameterized queries. String concatenation creates unique query text each time, forcing recompilation. This adds latency and wastes memory.
Solution
// WRONG: String formatting query = f"MATCH (u:User {{id: '{user_id}'}}) RETURN u" await graph.query(query)
// RIGHT: Parameterized query query = "MATCH (u:User {id: $user_id}) RETURN u" await graph.query(query, {"user_id": user_id})
// WRONG: IN with formatted list ids_str = ",".join([f"'{id}'" for id in ids]) query = f"MATCH (u:User) WHERE u.id IN [{ids_str}] RETURN u"
// RIGHT: IN with parameter query = "MATCH (u:User) WHERE u.id IN $ids RETURN u" await graph.query(query, {"ids": ids})
Symptoms
- First query is slow, not repeated
- Memory usage grows over time
- Same query has variable performance
- Query plan cache hit rate is low
Detection Pattern
query.f".\\{.*\\}"
Version Range
>=1.0.0
Storing Embeddings In Graph
Id
storing-embeddings-in-graph
Summary
Large vectors in graph properties kill performance
Severity
medium
Situation
You store 1536-dimensional embedding vectors as node properties. Graph queries become slow. Storage balloons.
Why
Graph databases optimize for relationship traversal, not vector math. Large properties slow down node loading and increase memory usage. Vector search requires specialized indexes (HNSW, IVF) not graph indexes.
Solution
// WRONG: Embedding in graph property CREATE (m:Memory { id: $id, content: $content, embedding: $embedding // 1536 floats = 12KB per node! })
// RIGHT: Store embedding reference, vector in vector DB CREATE (m:Memory { id: $id, content: $content, embedding_id: $embedding_id // Just the reference })
// Query pattern: Vector search first, then graph enrichment
Step 1: Vector search in Qdrant/pgvector
similar_ids = await vector_db.search(query_embedding, limit=20)
Step 2: Graph enrichment
results = await graph.query( """ MATCH (m:Memory)-[:RELATES_TO]->(e:Entity) WHERE m.id IN $ids RETURN m, collect(e) AS entities """, {"ids": similar_ids} )
Symptoms
- Graph memory usage 10x expected
- Node loading is slow
- Vector similarity queries not possible
- Each node is several KB
Detection Pattern
embedding.List\\[float\\]|embedding.\\$
Version Range
>=1.0.0
Graph Engineer - Validations
Cypher Query Without Index Hint
Id
cypher-no-index
Severity
warning
Type
regex
Pattern
- MATCH\\s\\(\\w+\\)\\sWHERE
- MATCH\\s\\(\\w+:\\w+\\)\\sWHERE\\s\\w+\\.\\w+\\s=
Message
Query may not use index. Consider using indexed property in pattern match.
Fix Action
Move filter to pattern: MATCH (n:Label {prop: $val}) instead of WHERE
Applies To
- *.py
- *.cypher
- */graph/.py
Unbounded Path Traversal
Id
cypher-unbounded-traversal
Severity
error
Type
regex
Pattern
- \\[:\\w+\\*\\]
- \\[r\\*\\]
- -\\[\\*\\]-
Message
Unbounded path traversal () is exponential. Add depth limit like 1..5
Fix Action
Change [] to [1..5] or appropriate bounded range
Applies To
- *.py
- *.cypher
Potential Cartesian Product in Query
Id
cypher-cartesian-product
Severity
warning
Type
regex
Pattern
- MATCH\\s\\([^)]+\\)\\s\\n\\sMATCH\\s\\([^)]+\\)(?!.*WITH)
Message
Multiple MATCH without WITH may create Cartesian product. Use connected patterns.
Fix Action
Connect patterns or use WITH between MATCH clauses
Applies To
- *.py
- *.cypher
Edge Creation Without Temporal Validity
Id
graph-edge-no-temporal
Severity
warning
Type
regex
Pattern
- CREATE.\\[r:.\\{(?!.*valid_from)
- MERGE.\\[r:.\\{(?!.*valid_from)
Message
Edge created without valid_from. Add temporal validity for history tracking.
Fix Action
Add valid_from: datetime() and valid_until: null to edge properties
Applies To
- *.py
- *.cypher
- */graph/.py
Cypher Query String Concatenation
Id
graph-query-not-parameterized
Severity
error
Type
regex
Pattern
- f".MATCH.\\{.*\\}"
- f'.MATCH.\{.*\}'
- \\.format\\(.*MATCH
Message
String formatting in Cypher query. Use parameters to enable plan caching.
Fix Action
Use query parameters: query($param) instead of f-string
Applies To
- *.py
Direct Node Deletion
Id
graph-node-delete
Severity
warning
Type
regex
Pattern
- DELETE\\s+\\w+\\s*$
- DETACH DELETE
Message
Deleting nodes loses history. Consider setting deleted_at timestamp instead.
Fix Action
Use soft delete: SET n.deleted_at = datetime() instead of DELETE
Applies To
- *.py
- *.cypher
MERGE Without ON CREATE/ON MATCH
Id
graph-merge-without-on-create
Severity
info
Type
regex
Pattern
- MERGE\\s\\([^)]+\\)(?!.ON\\s*(CREATE|MATCH))
Message
MERGE without ON CREATE/ON MATCH. Consider specifying behavior for each case.
Fix Action
Add ON CREATE SET and ON MATCH SET clauses for clarity
Applies To
- *.py
- *.cypher
Large Embedding in Graph Property
Id
graph-embedding-property
Severity
warning
Type
regex
Pattern
- embedding:\\s*\\$
- embedding:\\s\\[.\\]
- \\.embedding\\s*=
Message
Storing embeddings in graph properties is slow. Use external vector store.
Fix Action
Store embedding_id reference, keep vectors in Qdrant/pgvector
Applies To
- *.py
- */graph/.py
Missing Index for Frequent Query Pattern
Id
graph-no-index-creation
Severity
info
Type
regex
Pattern
- WHERE\\s+\\w+\\.user_id\\s=(?!.CREATE INDEX)
- WHERE\\s+\\w+\\.\\w+_id\\s=(?!.CREATE INDEX)
Message
Query filters on ID field. Ensure index exists for this property.
Fix Action
CREATE INDEX idx FOR (n:Label) ON (n.property)
Applies To
- *.py
- *.cypher
Causal Edge Without Cycle Detection
Id
graph-causal-no-cycle-check
Severity
warning
Type
regex
Pattern
- CREATE.:CAUSES(?!.cycle|.*acyclic)
- MERGE.:CAUSES(?!.cycle|.*acyclic)
Message
Creating causal edge without cycle check. Causal graphs must be DAGs.
Fix Action
Validate that adding edge doesn't create cycle before insertion
Applies To
- *.py
- */graph/.py
Counting All Edges on Node
Id
graph-count-all-edges
Severity
warning
Type
regex
Pattern
- MATCH.-\\[r\\]-.RETURN count\\(r\\)
- SIZE\\(\\(.*\\)-\\[\\]-
Message
Counting all edges on a node can be slow for god nodes. Consider caching count.
Fix Action
Store edge count as node property, update incrementally
Applies To
- *.py
- *.cypher
OPTIONAL MATCH Without Limit
Id
graph-optional-match-unbounded
Severity
info
Type
regex
Pattern
- OPTIONAL MATCH(?!.*LIMIT)
Message
OPTIONAL MATCH without LIMIT can return many rows. Consider adding LIMIT.
Fix Action
Add LIMIT or use collect() with slice to bound results
Applies To
- *.py
- *.cypher