> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.nvidia.com/sdgm/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nvidia.com/sdgm/_mcp/server.

# Graphs

> Connect tables through key relationships and materialize the graph used for training and prediction

Connect your [tables](/kumo-engine/1.0.0/user-code-guide/set-up-a-graph/tables) so Kumo can learn from relationships between entities and events.
These calls are the same on Linux and in Snowflake or Databricks notebooks. The first example continues from the AdventureWorks tables defined in [Tables](/kumo-engine/1.0.0/user-code-guide/set-up-a-graph/tables#create-tables-from-a-source).

## Plan Your Graph Configuration

Review these choices before creating your graph and materializing a snapshot:

* **Column roles and types:** Use [Tables](/kumo-engine/1.0.0/user-code-guide/set-up-a-graph/tables#inspect-and-adjust-column-types) to inspect columns and set the semantic types that describe how Kumo should interpret them.
* **Feature encoding:** Use [Encoder Selection](/kumo-engine/1.0.0/user-code-guide/set-up-a-graph/encoder-selection) to compare compatible encoders, missing-value strategies, and configuration options for each semantic type.
* **Relationships:** Connect foreign keys to the corresponding primary keys as shown in the section below.

Set column types on your tables before constructing the graph.
Apply optional encoder overrides later through the [Model Plan Guide](/kumo-engine/1.0.0/user-code-guide/train-a-model/model-plan-guide#column-processing), before training.

## Create a Graph

Use the same `connector`, `customer`, `product`, and `sales_order_header` objects from Tables.
Define the order-detail table, using `ModifiedDate` as its line-item timestamp, then pass the tables as a sequence and describe each relationship with an `Edge`:

```python
from kumo.engine import Graph, Table
from kumoapi.graph import Edge
from kumoapi.typing import Dtype, Stype

sales_order_detail = Table.from_source_table(
    connector["SalesOrderDetail"],
    ["SalesOrderDetailID", "SalesOrderID", "ProductID", "ModifiedDate",
     "OrderQty", "UnitPrice", "LineTotal"],
    primary_key="SalesOrderDetailID",
    time_column="ModifiedDate",
)
sales_order_detail = sales_order_detail.with_column("SalesOrderID", stype=Stype.ID)
sales_order_detail = sales_order_detail.with_column("ProductID", stype=Stype.ID)
sales_order_detail = sales_order_detail.with_column("ModifiedDate", dtype=Dtype.date)

graph = Graph(
    tables=[customer, product, sales_order_header, sales_order_detail],
    edges=[
        Edge(
            src_table="SalesOrderHeader",
            fkey="CustomerID",
            dst_table="Customer",
        ),
        Edge(
            src_table="SalesOrderDetail",
            fkey="SalesOrderID",
            dst_table="SalesOrderHeader",
        ),
        Edge(
            src_table="SalesOrderDetail",
            fkey="ProductID",
            dst_table="Product",
        ),
    ],
)
graph.validate()
```

`src_table` names the table containing the foreign key, and `fkey` names that column.
`dst_table` names the table whose primary key it references.
For a sequence, use each object's `table_name` in edges and predictive queries.
Each name must be distinct. To give one source multiple roles, use a mapping as shown in [Use One Source in Multiple Roles](#use-one-source-in-multiple-roles).

Self-relations, where `src_table` and `dst_table` identify the same graph table, are not supported.
Kumo emits a warning and omits these relationships from the snapshot.

`validate()` checks the schema, key references, and compatible key types before ingestion.
A successful return produces no result object. If validation fails, correct the named table, column, or type before requesting a snapshot.
It does not establish that every source row has a matching relationship.

Treat a graph as immutable after construction. Do not mutate its exposed `tables` or `edges` lists. To change tables or relationships, construct a new `Graph` with the revised sequence or mapping.

## Use One Source in Multiple Roles

Use graph aliases when the same source has different roles, such as buyers and sellers in a transaction.
Pass a mapping from each graph name to its `Table`. The mapping keys are the names used in edges and predictive queries; the underlying source name and location stay unchanged.
Aliases give each role its own graph table. They do not filter the source rows into different groups.

This standalone local-file example requires an installed Kumo Engine environment and a new, writable directory for two small CSV files.
It creates three customers with a numeric `label` to predict and two orders with separate `buyer_id` and `seller_id` references:

```python
from pathlib import Path
import kumo.engine as kumo
from kumoapi.graph import Edge

source_dir = Path("alias-example-data").resolve()
source_dir.mkdir()
(source_dir / "customers.csv").write_text("id,label\n1,0\n2,1\n3,0\n")
(source_dir / "orders.csv").write_text(
    "id,buyer_id,seller_id\n10,1,2\n11,2,3\n"
)

alias_connector = kumo.FileConnector(root_dir=str(source_dir))
customer_roles = kumo.Table.from_source_table(
    alias_connector["customers"], primary_key="id"
)
orders = kumo.Table.from_source_table(
    alias_connector["orders"], primary_key="id"
)
role_graph = kumo.Graph(
    tables={
        "buyers": customer_roles,
        "sellers": customer_roles,
        "orders": orders,
    },
    edges=[
        Edge(src_table="orders", fkey="buyer_id", dst_table="buyers"),
        Edge(src_table="orders", fkey="seller_id", dst_table="sellers"),
    ],
)
role_graph.validate()
print("Graph names:", list(role_graph.tables))
print("Source name:", customer_roles.table_name)

buyer_query = kumo.PredictiveQuery(
    graph=role_graph,
    query="PREDICT buyers.label FOR EACH buyers.id",
)
buyer_query.validate()
print("Graph and query schemas validated")
```

Successful validation returns `None`; the example prints:

```text
Graph names: ['buyers', 'sellers', 'orders']
Source name: customers
Graph and query schemas validated
```

Both aliases refer to the same `customer_roles` object and read `customers.csv` from `source_dir`.
Use `buyers` or `sellers` in this graph's edges and PQL, not `customers`.
For the seller role, the corresponding query is `PREDICT sellers.label FOR EACH sellers.id`.
If validation fails, check that edge table names match the mapping keys and that each foreign-key column exists and matches the destination primary-key type.

These calls validate definitions; they do not generate training rows, train a model, or return predictions.
To materialize this example, configure local working storage as described under [Create a Snapshot](#create-a-snapshot), then call `role_graph.snapshot()`.
Continue with [Predictive Queries](/kumo-engine/1.0.0/user-code-guide/train-a-model/predictive-queries) to generate training data for the selected role.

## Import a Prepared RelBench Database

Convert an in-memory RelBench `Database` into a local graph by exporting its tables to Parquet and carrying their declared keys and timestamps into Kumo.
This recipe starts with prepared data. Dataset downloads, benchmark splits, and evaluation tasks are separate steps.

Use an installed Kumo Engine environment with RelBench, pandas, and PyArrow available.
Configure local scratch as described under [Create a Snapshot](#create-a-snapshot) before continuing.
Choose a new, writable source directory outside scratch; neither directory can contain the other.
The [local-file connector guide](/kumo-engine/1.0.0/user-code-guide/connect-your-data-linux#local-files) explains file naming and access requirements.

The following small database illustrates the required metadata: three customers and two timestamped orders, with each order referencing a customer.
If you already have a prepared `relbench.base.Database`, use it as `db` and skip this block.

```python
import pandas as pd
from relbench.base import Database, Table as RelBenchTable

db = Database(table_dict={
    "customers": RelBenchTable(
        df=pd.DataFrame({"id": [0, 1, 2], "label": [0, 1, 0]}),
        pkey_col="id",
        fkey_col_to_pkey_table={},
        time_col=None,
    ),
    "orders": RelBenchTable(
        df=pd.DataFrame({
            "id": [0, 1],
            "customer_id": [0, 2],
            "ordered_at": pd.to_datetime(["2026-01-01", "2026-01-02"]),
            "amount": [10.0, 20.0],
        }),
        pkey_col="id",
        fkey_col_to_pkey_table={"customer_id": "customers"},
        time_col="ordered_at",
    ),
})
```

For your own database, this recipe assumes each table has a declared primary-key column and every referenced table is included in `db.table_dict`.
Keys must be ordinary DataFrame columns, not only index values. Preserve the key values and compatible types across related tables.
Use `time_col=None` for a table without a time role; otherwise, the named column must contain timestamps with a compatible type.
Self-relations are unsupported and omitted from snapshots, as explained under [Create a Graph](#create-a-graph). Do not use this recipe to preserve them.

Export each DataFrame without its pandas index, then map the RelBench metadata to Kumo tables and edges.
The database's table names become both Parquet filenames and graph names, so use simple names such as `customers` and `orders`.
Change `relbench-example-data` to a new source directory for your data:

```python
from pathlib import Path
import kumo.engine as kumo
from kumoapi.graph import Edge

source_dir = Path("relbench-example-data").resolve()
source_dir.mkdir()
for name, rel_table in db.table_dict.items():
    rel_table.df.to_parquet(source_dir / f"{name}.parquet", index=False)

relbench_connector = kumo.FileConnector(root_dir=str(source_dir))
relbench_tables = {
    name: kumo.Table.from_source_table(
        relbench_connector[name],
        primary_key=rel_table.pkey_col,
        time_column=rel_table.time_col,
    )
    for name, rel_table in db.table_dict.items()
}
relbench_edges = [
    Edge(src_table=name, fkey=column, dst_table=target)
    for name, rel_table in db.table_dict.items()
    for column, target in rel_table.fkey_col_to_pkey_table.items()
]
```

`pkey_col` sets the primary-key role, `time_col` sets the time role, and each entry in `fkey_col_to_pkey_table` creates an edge to the destination table's primary key.
For the example, the relationship is `orders.customer_id` to `customers.id`, and only `orders` has a time column.

Before constructing the graph, [inspect the resolved column types](/kumo-engine/1.0.0/user-code-guide/set-up-a-graph/tables#inspect-and-adjust-column-types) on each table in `relbench_tables`.
Inferred types for other columns may need adjustment; a small numeric feature can be inferred as categorical.
Apply any `with_column()` overrides by assigning the returned table back to `relbench_tables[name]`.

Validate the resulting schema, then materialize the local index:

```python
relbench_graph = kumo.Graph(tables=relbench_tables, edges=relbench_edges)
relbench_graph.validate()
relbench_snapshot = relbench_graph.snapshot()
print("Snapshot:", relbench_snapshot.id)
print("Local index:", relbench_snapshot.index_path)
```

Successful validation returns `None`; snapshot creation returns an object identifying the materialized index and its local directory.
If validation fails, check the declared key and time columns, each foreign-key column, and the referenced table names and key types.
Schema validation does not check every row's relationship or establish that arbitrary RelBench datasets need no further preparation.
Keep the index files while using the snapshot, as described below, and continue to [Predictive Queries](/kumo-engine/1.0.0/user-code-guide/train-a-model/predictive-queries) to define your prediction task.

## Inspect a Graph Definition

Use an already constructed `graph` to review its table names and declared relationships before creating a snapshot:

```python
from collections.abc import Mapping
import pandas as pd

if isinstance(graph.tables, Mapping):
    table_names = list(graph.tables)
else:
    table_names = [table.table_name for table in graph.tables]
print("Graph tables:", table_names)

relationships = pd.DataFrame(
    [
        {
            "src_table": edge.src_table,
            "fkey": edge.fkey,
            "dst_table": edge.dst_table,
        }
        for edge in graph.edges
    ],
    columns=["src_table", "fkey", "dst_table"],
)
print(relationships.to_string(index=False))
```

For a sequence of tables, the names come from each table's `table_name`.
For a mapping, its keys are the graph aliases, which can differ from the source table names.
The DataFrame has one row per declared edge, showing the source table, foreign-key column, and destination table.
A graph without edges produces an empty DataFrame with the same three columns.
This inspects the supplied definition; it does not validate relationships or show which source rows match.
To inspect each table's column types and key or time roles, use the [metadata DataFrame example](/kumo-engine/1.0.0/user-code-guide/set-up-a-graph/tables#inspect-and-adjust-column-types).
To draw the declared keys and relationships, continue to [Visualize Table Relationships](#visualize-table-relationships).

## Visualize Table Relationships

Render the declared table relationships as an SVG with the external [Graphviz Python package](https://graphviz.readthedocs.io/en/stable/manual.html).
The diagram shows table names, primary keys, and foreign keys used in relationships. It describes the schema, not individual records or proof that every key joins.
No snapshot or trained model is needed.

Install the Python package in the environment where you use Kumo:

```bash
python -m pip install graphviz
```

Graphviz also needs the separate `dot` rendering executable on that environment's `PATH`.
On Ubuntu Linux with administrator access, install and check it with:

```bash
sudo apt-get install graphviz
dot -V
```

`dot -V` prints the renderer version. For other systems, follow the [official Graphviz installation instructions](https://graphviz.org/download/).
Managed Snowflake and Databricks notebooks may not allow `sudo` or operating-system package installation. Use an environment where the renderer is already available, or ask your environment administrator to provide it before running this recipe.
Installing the Python package alone does not install `dot`.

Continue with the AdventureWorks `graph` from [Create a Graph](#create-a-graph).
Every table must already have a resolved `definition`. If a table's definition is `None`, [resolve its metadata](/kumo-engine/1.0.0/user-code-guide/set-up-a-graph/tables#refresh-metadata) and construct the graph again with the returned table.
To declare a primary key, use `Table.from_source_table(..., primary_key=...)` as shown in [Tables](/kumo-engine/1.0.0/user-code-guide/set-up-a-graph/tables#create-tables-from-a-source).
Resolving metadata can read the source; the rendering helper below only reads the existing definition.

```python
from collections.abc import Mapping
from graphviz import Digraph, escape


def table_relationship_diagram(graph):
    entries = (
        list(graph.tables.items())
        if isinstance(graph.tables, Mapping)
        else [(table.table_name, table) for table in graph.tables]
    )
    tables = dict(entries)
    if len(tables) != len(entries):
        raise ValueError("Use distinct table names or a graph alias mapping.")
    for name, table in entries:
        if table.definition is None:
            raise ValueError(
                f"Resolve metadata and declared keys for {name!r}, "
                "then rebuild the graph before rendering."
            )

    foreign_keys = {name: [] for name in tables}
    for edge in graph.edges:
        if edge.src_table not in tables or edge.dst_table not in tables:
            raise ValueError("Each edge must name tables in this graph.")
        if edge.src_table == edge.dst_table:
            raise ValueError("Remove unsupported self-relations before rendering.")
        if tables[edge.dst_table].definition.pkey is None:
            raise ValueError(f"Declare a primary key for {edge.dst_table!r}.")
        if edge.fkey not in foreign_keys[edge.src_table]:
            foreign_keys[edge.src_table].append(edge.fkey)

    diagram = Digraph("table_relationships", format="svg")
    diagram.attr(rankdir="TB")
    diagram.attr("node", shape="box")
    node_ids = {name: f"n{i}" for i, name in enumerate(tables)}
    for name, table in entries:
        lines = [name]
        if table.definition.pkey is not None:
            lines.append(f"PK: {table.definition.pkey}")
        lines.extend(f"FK: {key}" for key in foreign_keys[name])
        diagram.node(node_ids[name], label=escape("\n".join(lines)))
    for edge in graph.edges:
        primary_key = tables[edge.dst_table].definition.pkey
        label = f"{edge.fkey} → {primary_key}"
        diagram.edge(
            node_ids[edge.src_table], node_ids[edge.dst_table],
            label=escape(label),
        )
    return diagram


diagram = table_relationship_diagram(graph)
svg_path = diagram.render(filename="adventureworks-relationships", cleanup=True)
print(svg_path)
```

Open the printed `adventureworks-relationships.svg` file in a browser or SVG viewer.
If rendering raises `ExecutableNotFound`, install the separate Graphviz renderer and make sure `dot -V` works in the same environment as Python.

The AdventureWorks example produces four table nodes and three arrows. Each arrow runs from a foreign-key table to its referenced primary-key table, with the two key names on the arrow:

![AdventureWorks table relationships showing SalesOrderDetail pointing to SalesOrderHeader and Product, and SalesOrderHeader pointing to Customer.](/sdgm/_fern-files/nvidia-sdgm.docs.buildwithfern.com/586e0a18f237fd7d5286fd0c90452e90c432853659a14285bcda460470b465f9/assets/adventureworks-relationships.svg)

Mapping keys remain distinct graph aliases, including when two aliases share the same `Table` object.
A table without relationships still appears; a table without a declared primary key has no PK label.
The helper uses separate internal node identifiers and escapes display labels so names containing colons, quotes, backslashes, or angle brackets remain literal text.
It does not validate source rows or change the graph. Use `graph.validate()` for schema checks as described above.

## Create a Snapshot

A graph definition describes what to read. A snapshot materializes the source data into the local index used by training and prediction:

```python
snapshot = graph.snapshot()
print("Snapshot:", snapshot.id)
print("Local index:", snapshot.index_path)
```

This call runs synchronously and can take time for large datasets.
Configure sufficient local working storage before constructing your graph. Follow [Linux machine setup](/kumo-engine/1.0.0/get-started-with-kumo-engine/installation/linux-machine-deployment), [Snowflake notebook setup](/kumo-engine/1.0.0/get-started-with-kumo-engine/installation/snowflake-notebook), or [Databricks Notebook Setup](/kumo-engine/1.0.0/get-started-with-kumo-engine/installation/databricks-notebook) for your environment.

| Snapshot attribute | Meaning                                                           |
| ------------------ | ----------------------------------------------------------------- |
| `id`               | Identifies the graph definition and source data version.          |
| `index_path`       | Local directory containing the materialized graph index.          |
| `graph_config`     | Graph schema and relationship configuration used during modeling. |

Keep the complete index directory while the snapshot is in use.
The snapshot object does not copy its files into the Python process, and deleting scratch makes those files unavailable.
In a Snowflake notebook, the graph index remains in the notebook compute environment. Creating a snapshot does not store that index in the source schema.
In a Databricks notebook, the index also remains in local scratch. A model bundle copied to a Unity Catalog volume does not include the index. Rebuild a compatible snapshot after a cluster restart unless you have separately retained and validated its files.

## Refresh Data and Reuse Snapshots

Call `graph.snapshot()` again to materialize the source version currently reported by the connector.
If the same valid version is already present, Kumo reuses its index; changed data versions produce a separate snapshot without replacing an earlier one.
Source preparation may still access the connector before Kumo determines that the index can be reused.

For local files, version detection uses the latest file modification time.
After updating, adding, or removing local source files, make sure the table's latest file modification time changes before calling `graph.snapshot()` again.

Pass the selected snapshot explicitly to query generation, training, and prediction.
A generated table preserves its rows and labels separately from graph features, so later training or prediction can use a compatible updated snapshot.
The model's target, entity types, and feature requirements must still be compatible; a new snapshot is not a new model.

Continue to [Predictive Queries](/kumo-engine/1.0.0/user-code-guide/train-a-model/predictive-queries) to define the outcome you want to predict.