How to Represent Code Relationships (CALLS, READS_FROM, WRITES_TO) in Code-Graph-RAG

The CALLS, READS_FROM, and WRITES_TO relationships in Code-Graph-RAG are defined in the RelationshipType enum in codebase_rag/types_defs.py, where CALLS captures function-to-function invocations by default, while READS_FROM and WRITES_TO are optional I/O relationships that connect callables to external resources when the io capture group is enabled.

Code-Graph-RAG transforms software repositories into queryable knowledge graphs, making code relationships explicit and searchable. Understanding how the schema represents CALLS, READS_FROM, and WRITES_TO edges is essential for building accurate code intelligence queries, security audits, and dependency analyses. This guide covers where these relationships are defined, how they're generated, and practical Cypher queries to leverage them.


Where Code Relationships Are Defined in the Schema

The relationship types are centrally declared in codebase_rag/types_defs.py as Python enum values. The source shows the RelationshipType enum starting at line 900, with key members including:


# From codebase_rag/types_defs.py, lines 930-942

class RelationshipType(Enum):
    # The calls capture group (static and dynamic)

    CALLS = "CALLS"

    # The *read* (IO) & *write* (IO) capture group (optional)

    READS_FROM = "READS_FROM"
    WRITES_TO = "WRITES_TO"

    # The **flow** capture group (optional)

    FLOWS_TO = "FLOWS_TO"

These string values are used throughout the codebase to tag edges in the graph database. The __repr__ method at lines 905-906 ensures clean string output for logging and debugging.


Understanding the Three Core Relationship Types

CALLS: The Default Function Invocation Edge

CALLS is the foundational relationship in Code-Graph-RAG. It represents any invocation from one callable entity to another, whether discovered through static analysis or runtime tracing.

Key characteristics:

  • Captured by the default calls capture group — always enabled during repository ingestion
  • Applies to functions, methods, classes, enums, and other callable nodes
  • Static analysis discovers syntactic call sites via Tree-sitter parsing
  • Dynamic tracing (cgr trace) can augment edges with runtime provenance and flag static_missed: true for calls only observed at execution time

READS_FROM: I/O Resource Consumption

READS_FROM connects a callable to a Resource node it reads from. This is the primary mechanism for tracking data flow into your code from external systems.

What triggers READS_FROM edges:

  • Environment variable access (os.getenv, process.env, etc.)
  • File read operations (open(), fs.readFile, etc.)
  • Database queries and network reads
  • Any API recognized by the I/O capture group analyzers

Unlike CALLS, READS_FROM requires explicit opt-in via the io capture group.

WRITES_TO: I/O Resource Production

WRITES_TO is the counterpart to READS_FROM, marking callables that produce output to external resources.

Common WRITES_TO scenarios:

  • File write operations
  • Network responses and database writes
  • Standard output streams
  • Logging frameworks writing to external systems

Both I/O relationships point to Resource nodes with qualified_name values following the pattern resource::<KIND>::<identity>.


Enabling READS_FROM and WRITES_TO Capture

The I/O relationships are not generated by default. You must enable the io capture group during graph construction.

Configuration via Environment Variable

export CGR_CAPTURE_IO=1
cgr load /path/to/repository

Configuration via Python SDK

from codegraph import GraphLoader, Config

# Enable io capture group explicitly

cfg = Config(capture_groups=['io'])
loader = GraphLoader(config=cfg)
loader.load_repo('/path/to/repo')

With this configuration, the Tree-sitter based analyzers scan for known I/O API patterns across supported languages and emit READS_FROM and WRITES_TO edges alongside the default CALLS edges.


Querying Code Relationships with Cypher

Once your graph is loaded in Memgraph, these Cypher patterns unlock powerful code intelligence.

Find All Callers of a Specific Function

MATCH (caller:Function)-[:CALLS]->(callee:Function)
WHERE callee.name = 'store_embedding'
RETURN caller.qualified_name AS caller_function,
       caller.file_path AS source_file
ORDER BY caller.qualified_name;

This query identifies direct call dependencies — useful for impact analysis when modifying the target function.

Discover Environment Variable Dependencies

MATCH (fn:Function)-[:READS_FROM]->(res:Resource)
WHERE res.qualified_name STARTS WITH 'resource::ENV'
RETURN fn.name AS function_name,
       res.qualified_name AS env_var,
       fn.file_path AS location
ORDER BY fn.file_path;

Results show which code depends on which environment variables, aiding documentation and deployment configuration.

Trace Data Flow from Secret to Network Output

MATCH (fn:Function)-[:READS_FROM]->(secret:Resource)
WHERE secret.qualified_name = 'resource::ENV::API_SECRET'
MATCH (fn)-[:WRITES_TO]->(out:Resource)
WHERE out.kind IN ['NETWORK', 'STDOUT', 'FILE']
RETURN fn.qualified_name AS potential_leak_point,
       out.qualified_name AS output_target,
       fn.file_path AS location;

This security audit pattern identifies functions that read sensitive values and write to external destinations — a critical query for secret scanning.

Find Functions with Both Read and Write I/O

MATCH (fn:Function)-[:READS_FROM]->(src:Resource)
WITH fn, count(src) AS read_count
MATCH (fn)-[:WRITES_TO]->(dst:Resource)
RETURN fn.qualified_name AS io_heavy_function,
       read_count AS sources_read,
       count(dst) AS destinations_written,
       collect(DISTINCT src.kind) AS read_kinds,
       collect(DISTINCT dst.kind) AS write_kinds
ORDER BY read_count + count(dst) DESC;

Functions appearing in results with high counts are complex I/O coordinators — candidates for refactoring or careful testing.


Validating I/O Relationships in Tests

The test suite validates correct READS_FROM and WRITES_TO generation. In codebase_rag/tests/test_io_libc_edges.py, constants reference the enum values:


# Lines 15-16 of test_io_libc_edges.py

READS_FROM = cs.RelationshipType.READS_FROM.value
WRITES_TO = cs.RelationshipType.WRITES_TO.value

Line 120 shows validation logic checking for these relationship types in edge assertions:

if str(c.arg[1]) in (READS_FROM, WRITES_TO, FLOWS_TO):
    # Test assertion logic...

These tests ensure language-agnostic I/O detection works correctly across C, Python, and other supported languages.


Comparing Relationship Characteristics

Aspect CALLS READS_FROM / WRITES_TO
Capture group Default (always on) Optional io capture group
Source entity Any callable Any callable
Target entity Callable nodes Resource nodes
Static analysis Yes — Tree-sitter call sites Yes — I/O API patterns
Dynamic tracing Yes — runtime augmentation Limited — depends on tracer
Use cases Call graphs, dead code, impact analysis Data flow, security, resource mapping

Summary

  • CALLS relationships in Code-Graph-RAG are always captured, representing function-to-function invocations discovered through static and dynamic analysis
  • READS_FROM and WRITES_TO relationships require enabling the io capture group and connect callables to Resource nodes representing external I/O
  • All three are defined in codebase_rag/types_defs.py as RelationshipType enum values and used throughout the ingestion pipeline
  • Practical Cypher queries combining these edges enable security audits, dependency analysis, and data flow tracking across your codebase

Frequently Asked Questions

What's the difference between CALLS and FLOWS_TO relationships?

CALLS represents direct invocation between code entities, while FLOWS_TO tracks data propagation paths — often through intermediate variables and transformations. CALLS answers "who calls whom"; FLOWS_TO answers "where does this data end up". Both are defined in RelationshipType, but FLOWS_TO requires additional taint analysis to compute.

How do I know if READS_FROM or WRITES_TO relationships were generated for my repository?

Query your graph for Resource nodes connected via these edge types: MATCH ()-[r:READS_FROM|WRITES_TO]->(res:Resource) RETURN type(r), count(*). Zero results indicate the io capture group was disabled during ingestion. Re-run cgr load with CGR_CAPTURE_IO=1 or Config(capture_groups=['io']) to generate them.

Can CALLS edges distinguish between static and dynamic dispatch?

Yes. When dynamic tracing is enabled (cgr trace), CALLS edges include provenance metadata. The static_missed: true flag on an edge indicates the call was observed at runtime but not discovered by static analysis — typically polymorphic dispatch, reflection, or dynamic module loading.

Why are my WRITES_TO edges pointing to resources I don't recognize?

Code-Graph-RAG's I/O capture group uses pattern matching against known API signatures. Some standard library calls (like print() or logging frameworks) may be abstracted to canonical resource names such as resource::STDOUT::default or resource::FILE::<inferred_path>. Check resource.kind and resource.qualified_name properties to understand the abstraction layer applied.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →