How Graphify's Obsidian Vault Export Generates Bidirectional Links

Graphify generates bidirectional links by treating every edge in its underlying undirected NetworkX graph as a reciprocal connection, automatically writing Obsidian wiki links ([[…]]) in both connected node files.

Graphify's Obsidian vault export feature transforms knowledge graphs into a folder of interlinked Markdown files. When you call to_obsidian from graphify/export.py, the tool creates bidirectional links between notes by leveraging the undirected nature of its nx.Graph data structure, ensuring that navigating your exported vault feels native to Obsidian's link-based interface.

The bidirectional linking is not explicitly coded as a pairwise operation. Instead, it emerges naturally from Graphify's use of an undirected NetworkX graph (nx.Graph). In this data structure, edges have no inherent direction, meaning an edge connecting Node A to Node B is identical to one connecting Node B to Node A.

When the to_obsidian function processes the graph, it retrieves neighbors for each node using G.neighbors(node_id). Because the graph is undirected, this method returns all adjacent nodes regardless of which node initiated the relationship. Consequently, when Node A's Markdown file is written, it contains a wiki link to Node B. When Node B's file is written, it contains a wiki link to Node A. This achieves true bidirectional linking without requiring explicit reverse-edge logic.

For bidirectional links to function in Obsidian, the filenames inside the wiki links must exactly match the actual files on disk. Graphify implements a two-stage sanitization process to guarantee this, even when node labels are long or contain special characters.

Safe Filename Generation (_cap_filename)

Node labels are first cleaned to remove unsafe characters, then capped to ≤ 200 bytes. If truncation occurs, the function appends a deterministic 8‑character hash to maintain uniqueness. This logic is implemented in graphify/export.py at lines 65‑73:


# Conceptual implementation from graphify/export.py

def _cap_filename(name, max_bytes=200):
    """Truncate to max_bytes and append hash if needed."""
    # Returns safe filename ≤ 200 bytes + hash if truncated

    pass

Collision Handling (_dedup_node_filenames)

After sanitizing labels, _dedup_node_filenames (lines 82‑100 in graphify/export.py) resolves any remaining filename collisions—such as case‑insensitive duplicates—by appending numeric suffixes. This ensures every node maps to a distinct file, preventing scenarios where two nodes might overwrite each other or create ambiguous wiki links.

Inside the main export loop, to_obsidian constructs each node's "Connections" section by iterating through its neighbors and formatting them as Obsidian wiki links. According to the source code at graphify/export.py lines 521‑529, each connection appears as:

- [[<neighbor-filename>]] - `<relation>` [<confidence>]

The <neighbor-filename> is retrieved from the same node_filename mapping that already applied _cap_filename and _dedup_node_filenames. This ensures that even if a label was truncated to fit filesystem limits, the wiki link points to an existing file. Because the underlying nx.Graph is undirected, the neighbor's own file will contain a reciprocal link back to the original node, creating the bidirectional structure.

Practical Example

The following code demonstrates how a single undirected edge becomes reciprocal wiki links in the exported vault:

import networkx as nx
from graphify.export import to_obsidian
import tempfile
import pathlib

# Build a simple undirected graph

G = nx.Graph()
G.add_node("A", label="Alpha")
G.add_node("B", label="Beta")
G.add_edge("A", "B", relation="calls", confidence="EXTRACTED")

# Export to temporary directory

out_dir = pathlib.Path(tempfile.mkdtemp())
to_obsidian(G, {0: ["A", "B"]}, str(out_dir))

# Result:

# Alpha.md contains: - [[Beta]] - `calls` [EXTRACTED]

# Beta.md contains:  - [[Alpha]] - `calls` [EXTRACTED]

In this example, Alpha.md links to Beta.md and vice versa, allowing seamless navigation in Obsidian's graph view.

Summary

  • Undirected Structure: Graphify uses nx.Graph from NetworkX, where G.neighbors(node_id) returns all adjacent nodes, causing every edge to be written in both incident node files.
  • Safe Filenames: The _cap_filename function (lines 65‑73) ensures labels fit within 200 bytes with deterministic hashing, while _dedup_node_filenames (lines 82‑100) prevents file collisions.
  • Automatic Reciprocity: Because the graph is undirected, the to_obsidian function inherently creates reciprocal [[…]] wiki links without explicit reverse-edge logic.
  • Validation: Tests in tests/test_obsidian_filename_cap.py and tests/test_obsidian_dangling_member.py verify that no dangling links are produced after filename truncation.

Frequently Asked Questions

No. Because Graphify stores relationships in an undirected nx.Graph structure as implemented in graphify/export.py, every edge is treated as bidirectional. Both nodes always reference each other in their Connections sections, regardless of the original extraction direction.

What happens if node labels are identical except for capitalization?

The _dedup_node_filenames function resolves such collisions by appending numeric suffixes to the filenames. This ensures that wiki links like [[Node]] and [[Node 1]] point to distinct files, preventing broken links in the Obsidian vault.

How does Graphify handle extremely long node labels?

The _cap_filename function truncates labels to 200 bytes and appends an 8‑character hash to maintain uniqueness. The wiki link generation uses this same truncated filename, ensuring that [[…]] references resolve correctly even when the original label was shortened.

The test suite includes tests/test_obsidian_filename_cap.py (verifying links resolve after truncation) and tests/test_obsidian_dangling_member.py (ensuring no dangling references exist). These confirm that the bidirectional links generated by to_obsidian always point to valid files.

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 →