Skip to content

This feature is currently in Preview.

Create a graph

A graph is a way to represent a set of things and the connections between them. The things are called nodes. The connections are called edges.

This sounds a bit like what you already do with concepts and relationships in a semantic model, and it is! The main difference is that a graph views nodes and edges as part of a single structure that you can use to run graph algorithms.

The Graph class is the container for a graph.

  • Nodes: The set of things that count as nodes for this question. In PyRel graph reasoning, nodes are instances of graph.Node.
  • Edges: The set of directed or undirected connections between nodes. Edges are instances of graph.Edge.
  • Optional edge weights: A non-negative numeric value per edge.
  • Graph algorithms: Methods that compute properties of the graph structure, like connectivity, centrality, similarity, and communities.

Graphs in PyRel are:

  • Labeled property graphs: Nodes and edges can have properties like id, name, or amount.
  • Simple graphs: There can only be one edge in a given direction between any two nodes. If your data has multiple distinct edges between the same nodes, use the aggregator argument to combine them for graph algorithms.

The most common workflow is to use the concepts and relationships you already have in your model to define a graph’s structure and then run algorithms on that structure to derive new facts that you can join back into your model for analysis.

The examples below define graph structure using small inline datasets. They belong in your model definition, not in a query script. To compute graph results, declare algorithm outputs in the model before you deploy it. Query the deployed outputs from a separate script after the refresh completes.

A Python Graph object does not become a separate graph database or a single graph object in Snowflake. Its node and edge concepts and relationships become regular model outputs: tables, views, or dynamic tables.

  • Nodes: With node_concept=Account, the graph reuses the Account concept and its deployed output. Without node_concept, PyRel creates a node concept with a generated name, such as graph1_Node.
  • Edges: By default, PyRel creates an edge concept with a generated name, such as graph1_Edge. Its output includes source and destination columns, and a weight column for weighted edges. With edge_concept=Transaction, the graph reuses Transaction and the endpoint and weight relationships you supply.

To find a particular graph’s outputs, start with its constructor in your model definition. If it supplies node_concept or edge_concept, look for outputs named after those concepts. Otherwise, match the generated node and edge names by their shared number, such as GRAPH1_NODE and GRAPH1_EDGE with default Snowflake naming. The number is assigned when the Graph object is constructed, so a Python variable name such as payment_graph is not the deployed object name. Adding or reordering graph constructors can change generated names. If several graphs reuse the same node concept, use their edge concepts and endpoint relationships to distinguish them.

Use Monitor a deployed model to inspect the actual output objects. Internal graph helper relationships belong in the internal schema, not in application queries.

When you create a Graph object, you must choose the type of graph you want to create. Graphs can be:

  • Directed: Each edge has an arrow from src to dst. For example, a payment edge might point from payer → payee.
  • Undirected: Edges do not have arrows. You can treat a link between two nodes as going both ways.
  • Weighted: Each edge has a number called a weight. For example, that weight might be payment amount or interaction frequency.
  • Unweighted: Edges do not have weights. The graph only cares whether a connection exists.

Which type of graph you create is determined by the directed and weighted arguments you are required to pass when you construct the Graph object.

Use this table to help you pick which type of to create:

What to useWhen to use it
Graph(m, directed=True, weighted=False)Use when direction matters, but weights would mislead, like follower → followed relationships.
Graph(m, directed=False, weighted=False)Use when links are mutual and only link existence matters, like friendship or collaboration.
Graph(m, directed=False, weighted=True)Use when links are mutual and weights matter, like co-authorship with number of joint papers as weight.
Graph(m, directed=True, weighted=True)Use when direction and weights matter, like payer → payee payments with amount as weight.
  • In weighted graphs, every edge must have a weight. Graph algorithms assume weights are non-negative.

Create an undirected graph by setting directed=False when you construct the Graph object:

from relationalai.semantics import Integer, Model
from relationalai.semantics.reasoners.graph import Graph
m = Model("UndirectedGraph")
# Declare the model schema.
Account = m.Concept("Account", identify_by={"id": Integer})
Person = m.Concept("Person", identify_by={"id": Integer})
Account.owner = m.Relationship(f"{Account} is owned by {Person}")
Person.account = Account.owner.alt(f"{Person} owns {Account}")
# Define base facts.
13 collapsed lines
people_data = m.data([
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"},
])
m.define(Person.new(people_data.to_schema()))
account_data = m.data([
{"id": 101, "owner_id": 1},
{"id": 102, "owner_id": 2},
{"id": 103, "owner_id": 2},
])
account_owner = Person.lookup(id=account_data.owner_id)
m.define(Account.new(id=account_data.id, owner=account_owner))
# Create an undirected, unweighted graph.
graph = Graph(m, directed=False, weighted=False)
# Define the edges of the graph.
m.define(graph.Edge.new(src=Person, dst=Person.account))
  • Graph(m, directed=False, weighted=False) creates an undirected, unweighted graph.
  • graph.Edge.new(src=Person, dst=Person.account) derives edges from the Person.account relationship. Each edge connects a Person node to the Account node they own.
  • Defining edges from a relationship like Person.account is a common pattern when your model already has a relationship that maps directly to the connections you want in your graph.
  • For other patterns for edge definition, see Define nodes with node_concept.

To create a directed graph, set directed=True when you construct the Graph object:

from relationalai.semantics import Integer, Model
from relationalai.semantics.reasoners.graph import Graph
m = Model("DirectedGraph")
# Declare the model schema.
Person = m.Concept("Person", identify_by={"id": Integer})
Account = m.Concept("Account", identify_by={"id": Integer})
Account.owner = m.Property(f"{Account} owned by {Person}")
Transaction = m.Concept("Transaction", identify_by={"id": Integer})
Transaction.payer = m.Property(f"{Transaction} received from {Account:payer}")
Transaction.payee = m.Property(f"{Transaction} payed to {Account:payee}")
# Define base facts.
22 collapsed lines
person_data = m.data([
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"},
])
m.define(Person.new(person_data.to_schema()))
account_data = m.data([
{"id": 1, "owner_id": 1},
{"id": 2, "owner_id": 2},
{"id": 3, "owner_id": 2},
])
account_owner = Person.lookup(id=account_data.owner_id)
m.define(Account.new(id=account_data.id, owner=account_owner))
transaction_data = m.data([
{"id": 101, "payer_id": 1, "payee_id": 2, "amount": 100.0},
{"id": 102, "payer_id": 2, "payee_id": 1, "amount": 200.0},
{"id": 103, "payer_id": 2, "payee_id": 1, "amount": 150.0},
])
txn_payer = Account.lookup(id=transaction_data.payer_id)
txn_payee = Account.lookup(id=transaction_data.payee_id)
m.define(Transaction.new(id=transaction_data.id, payer=txn_payer, payee=txn_payee))
# Create a directed, unweighted graph.
graph = Graph(m, directed=True, weighted=False)
# Define the edges of the graph.
m.define(
graph.Edge.new(src=Transaction.payer, dst=Transaction.payee),
graph.Edge.new(src=Account.owner, dst=Account),
)
  • Graph(m, directed=True, weighted=False) creates a directed, unweighted graph.
  • A directed graph makes sense here because payments have a clear direction from payer to payee.
  • graph.Edge.new(src=Transaction.payer, dst=Transaction.payee) derives edges from the Transaction concept, connecting payer accounts to payee accounts.

Use a weighted graph when edge magnitude should influence results, like payments with amount as edge weight:

from relationalai.semantics import Float, Integer, Model
from relationalai.semantics.reasoners.graph import Graph
m = Model("WeightedGraph")
# Declare the model schema.
Person = m.Concept("Person", identify_by={"id": Integer})
Account = m.Concept("Account", identify_by={"id": Integer})
Account.owner = m.Property(f"{Account} owned by {Person}")
Transaction = m.Concept("Transaction", identify_by={"id": Integer})
Transaction.payer = m.Property(f"{Transaction} received from {Account:payer}")
Transaction.payee = m.Property(f"{Transaction} payed to {Account:payee}")
Transaction.amount = m.Property(f"{Transaction} has amount {Float:amount}")
# Define base facts.
person_data = m.data([
22 collapsed lines
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"},
])
m.define(Person.new(person_data.to_schema()))
account_data = m.data([
{"id": 1, "owner_id": 1},
{"id": 2, "owner_id": 2},
{"id": 3, "owner_id": 2},
])
account_owner = Person.lookup(id=account_data.owner_id)
m.define(Account.new(id=account_data.id, owner=account_owner))
transaction_data = m.data([
{"id": 101, "payer_id": 1, "payee_id": 2, "amount": 100.0},
{"id": 102, "payer_id": 2, "payee_id": 1, "amount": 200.0},
{"id": 103, "payer_id": 2, "payee_id": 3, "amount": 150.0},
])
txn_payer = Account.lookup(id=transaction_data.payer_id)
txn_payee = Account.lookup(id=transaction_data.payee_id)
m.define(Transaction.new(id=transaction_data.id, payer=txn_payer, payee=txn_payee, amount=transaction_data.amount))
# Create a directed, weighted graph.
graph = Graph(m, directed=True, weighted=True)
# Define the edges of the graph.
m.define(
graph.Edge.new(
src=Transaction.payer,
dst=Transaction.payee,
weight=Transaction.amount
),
)
  • Graph(m, directed=True, weighted=True) enables weight-aware algorithms.
  • Each Edge.new(...) call includes a weight.
  • In weighted graphs, every edge must have a weight.
  • Weights must be finite, non-negative floating point numbers.
  • Use floats.float() to convert from other numeric types if needed.
  • Graph algorithms assume weights are non-negative and finite.

To include every entity instance of a concept as a node, pass the Concept object to the Graph constructor’s node_concept argument. Then define edges between those nodes as usual.

In the following example, every Account is a node, but you only create edges for transaction with a high enough value:

from relationalai.semantics import Float, Integer, Model
from relationalai.semantics.reasoners.graph import Graph
m = Model("AccountsAsNodes")
# Declare the model schema.
Account = m.Concept("Account", identify_by={"id": Integer})
Transaction = m.Concept("Transaction", identify_by={"id": Integer})
Transaction.payer = m.Property(f"{Transaction} payer is {Account:payer}")
Transaction.payee = m.Property(f"{Transaction} payee is {Account:payee}")
Transaction.amount = m.Property(f"{Transaction} amount is {Float:amount}")
# Define base facts
14 collapsed lines
account_data = m.data([
{"id": 1},
{"id": 2},
{"id": 3},
])
m.define(Account.new(account_data.to_schema()))
transaction_data = m.data([
{"id": 101, "payer_id": 1, "payee_id": 2, "amount": 50.0},
{"id": 102, "payer_id": 2, "payee_id": 1, "amount": 200.0},
])
txn_payer = Account.lookup(id=transaction_data.payer_id)
txn_payee = Account.lookup(id=transaction_data.payee_id)
m.define(Transaction.new(id=transaction_data.id, payer=txn_payer, payee=txn_payee, amount=transaction_data.amount))
# Created a directed graph with nodes from the Account concept.
graph = Graph(m, directed=True, weighted=False, node_concept=Account)
# Define edges for transactions with amount >= 100.0.
(
m.define(graph.Edge.new(src=Transaction.payer, dst=Transaction.payee))
.where(Transaction.amount >= 100.0)
)
assert graph.Node is Account
  • node_concept=Account makes every Account instance a node in the graph.
  • .where(Transaction.amount >= 100.0) filters which Transaction instances become edges.
  • Account entities with no high value transactions still appear in the graph as isolated nodes.
  • graph.Node is the supplied Account concept, not a separate concept. Edge endpoints must be instances of that concept.
  • graph.Node.new(...) defines additional instances of Account using its identifying properties.

Use this approach when your model already represents each interaction as its own entity (for example, each Transaction is an entity). This is especially useful when edges have their own properties (like amount) and you want those properties available for weighting.

To define edges from an existing concept, set edge_concept=<YourEdgeConcept> when you construct the Graph. You also provide relationships that map each edge entity to its endpoints.

In the following example, every Account is a node, and every Transaction is a distinct directed edge whose weight is the transaction amount:

from relationalai.semantics import Float, Integer, Model
from relationalai.semantics.reasoners.graph import Graph
m = Model("TransactionsAsEdges")
# Declare model schema.
Account = m.Concept("Account", identify_by={"id": Integer})
Transaction = m.Concept("Transaction", identify_by={"id": Integer})
Transaction.payer = m.Relationship(f"{Transaction} payer is {Account}")
Transaction.payee = m.Relationship(f"{Transaction} payee is {Account}")
Transaction.amount = m.Property(f"{Transaction} amount is {Float:amount}")
# Define base facts.
14 collapsed lines
account_data = m.data([
{"id": 1},
{"id": 2},
{"id": 3},
])
m.define(Account.new(account_data.to_schema()))
transaction_data = m.data([
{"id": 101, "payer_id": 1, "payee_id": 2, "amount": 50.0},
{"id": 102, "payer_id": 2, "payee_id": 1, "amount": 200.0},
])
txn_payer = Account.lookup(id=transaction_data.payer_id)
txn_payee = Account.lookup(id=transaction_data.payee_id)
m.define(Transaction.new(id=transaction_data.id, payer=txn_payer, payee=txn_payee, amount=transaction_data.amount))
# Create a directed, weighted graph with edges from the `Transaction` concept.
graph = Graph(
m,
directed=True,
weighted=True,
node_concept=Account,
edge_concept=Transaction,
edge_src_relationship=Transaction.payer,
edge_dst_relationship=Transaction.payee,
edge_weight_relationship=Transaction.amount,
)
  • edge_concept=Transaction makes every Transaction instance a distinct graph edge.
  • edge_src_relationship and edge_dst_relationship map each transaction edge to its src and dst properties.
  • edge_weight_relationship=Transaction.amount uses the transaction amount as the edge weight.
  • You must pass all three of edge_concept, edge_src_relationship, and edge_dst_relationship together.
  • edge_weight_relationship is also required when the graph is weighted.
  • With edge_concept, you do not need to define edges separately with graph.Edge.new(...) because the graph automatically treats every instance of that concept as an edge.
  • graph.Edge is the supplied edge concept. To add a connection, define another instance of that concept and its endpoint relationships.
  • You must pass node_concept when you use edge_concept.

You can use Node.new() to explicitly define nodes in a graph that do not come from a concept in your model. This pattern is not common but could be useful for adding special nodes that represent entities outside of your model.

In the following example, three nodes are defined explicitly with Node.new() and connected by two edges:

from relationalai.semantics import Float, Integer, Model, String
from relationalai.semantics.reasoners.graph import Graph
m = Model("ExplicitNodesAndEdges")
# Create an undirected, unweighted graph.
graph = Graph(m, directed=False, weighted=False)
Node, Edge = graph.Node, graph.Edge
m.define(
# Define nodes for the graph
n1 := Node.new(id=Integer(1)),
n2 := Node.new(id=Integer(2)),
n3 := Node.new(id=Integer(3)),
# Define edges for the graph
Edge.new(src=n1, dst=n2),
Edge.new(src=n2, dst=n3),
)
  • Node is a concept, so Node.new() creates instances of type Node.
  • Since Node has no identifying properties, every argument you pass to Node.new() is used to create an identity for that node.

A multi-edge occurs when two distinct edges share the same src and dst nodes but have different identities. The fragments below reuse m, n1, and n2 from the preceding example. There are two main ways this can happen:

  • In a directed graph, multiple edges between the same src and dst nodes are created with additional properties that differentiate them. This is most common in a weighted graph where there are multiple interactions between the same entities with different weights, like multiple transactions between the same payer and payee with different amounts:

    graph = Graph(m, directed=True, weighted=True)
    m.define(
    graph.Edge.new(src=n1, dst=n2, weight=10.0),
    graph.Edge.new(src=n1, dst=n2, weight=2.5)
    )
  • In an undirected graph, multiple edges between the same two nodes can occur when edges are defined for both directions between the same pair of nodes:

    graph = Graph(m, directed=False, weighted=False)
    m.define(
    graph.Edge.new(src=n1, dst=n2),
    graph.Edge.new(src=n2, dst=n1),
    )

Graph algorithms use a simple graph with one connection per node pair. With the default aggregator=None, PyRel records a model validation error when graph methods consume multi-edges. Do not rely on that error to stop computation. If repeated connections are intentional, such as multiple transactions between the same payer and payee, explicitly collapse them into a single edge.

To collapse multi-edges, use the aggregator argument when you construct the Graph object:

from relationalai.semantics import Float, Integer, Model
from relationalai.semantics.reasoners.graph import Graph
m = Model("MultiEdgeGraph")
# Declare the model schema.
Account = m.Concept("Account", identify_by={"id": Integer})
Transaction = m.Concept("Transaction", identify_by={"id": Integer})
Transaction.payer = m.Property(f"{Transaction} payed to {Account:payee}")
Transaction.payee = m.Property(f"{Transaction} received from {Account:payer}")
Transaction.amount = m.Property(f"{Transaction} has amount {Float:amount}")
# Define base facts.
15 collapsed lines
account_data = m.data([
{"id": 1},
{"id": 2},
{"id": 3},
])
m.define(Account.new(account_data.to_schema()))
transaction_data = m.data([
{"id": 101, "payer_id": 1, "payee_id": 2, "amount": 100.0},
{"id": 102, "payer_id": 2, "payee_id": 1, "amount": 200.0},
{"id": 103, "payer_id": 2, "payee_id": 1, "amount": 150.0},
])
txn_payer = Account.lookup(id=transaction_data.payer_id)
txn_payee = Account.lookup(id=transaction_data.payee_id)
m.define(Transaction.new(id=transaction_data.id, payer=txn_payer, payee=txn_payee, amount=transaction_data.amount))
# Create a directed, weighted graph that collapses multi-edges by summing weights.
graph = Graph(m, directed=True, weighted=True, aggregator="sum")
# Define the edges of the graph.
m.define(graph.Edge.new(src=Transaction.payer, dst=Transaction.payee, weight=Transaction.amount))
  • aggregator="sum" collapses multi-edges by summing their weights.
  • Currently, "sum" is the only supported aggregator.
  • You can use aggregator="sum" to collapse multi-edges in an unweighted graph as well.

Use this table when deployed graph results are missing nodes, edges, or weights:

SymptomLikely causeWhat to do
num_edges is 0No edges were defined.Make sure you call define() for Edge.new() declarations. If your edge definition is conditional, ensure the conditions are not filtering out all edges. If you used edge_concept, make sure instances of the concept have been defined.
num_nodes is 0No nodes were defined.If nodes were implicitly defined by edges, ensure your edge definitions do not filter out all nodes. If you used node_concept, make sure instances of the concept have been defined.
Weights are not showing up in edges.You created an unweighted graph.Make sure you set weighted=True when you construct the Graph object.

For a deployed model, apply corrections in the model definition, redeploy, and check that the refresh completes before querying again. See Deploy a model for the deployment workflow.