Edges
Edge types define directed relationships between node types, with optional properties and cardinality constraints.
An edge declaration creates a directed relationship between two node types. Each edge type gets its own storage table with three built-in String columns, id, src, and dst. Plus one column per declared property. The endpoints reference the @key value of the source and destination nodes.
Basic syntax
edge WorksAt: Person -> CompanyThe identifier after edge is the type name. The source and destination are existing node type names separated by ->.
Edge properties
Edges can carry properties just like nodes. Declare them inside braces:
edge Knows: Person -> Person {
since: Date?
confidence: F64?
}Property rules are the same as for nodes: each property has a name, a type, and optional annotations. Nullable properties use the ? suffix.
Self-referencing edges
An edge can connect a node type to itself:
edge Knows: Person -> Person
edge DependsOn: Task -> TaskThis is how you model recursive structures like social graphs, dependency chains, or org hierarchies.
Cardinality constraints
Use @card(...) on the edge header to limit how many edges of a given type can
originate from a single source node.
edge ManagedBy: Employee -> Manager @card(1..1)The range syntax:
| Pattern | Meaning |
|---|---|
@card(1..1) | Exactly one edge per source node. |
@card(0..1) | At most one edge per source node. |
@card(1..) | At least one edge per source node. |
@card(0..) | No constraint (default). |
Cardinality is checked when edges are inserted or deleted. A mutation that would violate the constraint is rejected.
Edge-level annotations
Annotations that describe the edge type itself appear on the edge header, after
any @card(...) annotation and before the optional property block:
edge Authored: Actor -> Decision @description("Records who authored a decision and when.") {
date: Date
}Edge types can use @description("..."), @instruction("..."), and
@rename_from("OldEdgeName") on the header.
Edge body constraints
Edge bodies only allow @unique and @index. @key, @range, and @check
are rejected by the parser on edges. Those are node-only constraints. Inside
@unique(...) and @index(...) you can also reference the reserved endpoint
identifiers src and dst (in addition to declared property names).
edge Follows: Person -> Person {
@unique(src, dst)
}Note that @unique(src, dst) is currently parsed as two separate per-column
uniqueness constraints, one on src and one on dst, not a composite "no
duplicate (src, dst) pair" guarantee. The engine will accept multiple
identical edges with the same src and dst values. Pair-uniqueness has to
be enforced in the loader or reviewer until the engine grows true composite
edge uniqueness.
Edge storage
Every edge row contains:
| Column | Type | Description |
|---|---|---|
id | String | Auto-generated unique identifier. |
src | String | Source-node key (the @key value of the from node). |
dst | String | Destination-node key (the @key value of the to node). |
Plus one column per declared property. The src and dst columns are indexed automatically for efficient traversal in both directions.
Full example
A decision-tracking graph with multiple edge types:
node Actor {
name: String @key
role: String
}
node Decision {
slug: String @key
title: String @index
status: enum(proposed, accepted, rejected, superseded)
}
node Signal {
slug: String @key
title: String @index
strength: enum(strong, moderate, weak)
}
edge Authored: Actor -> Decision @description("Records who authored a decision and when.") {
date: Date
}
edge Informed: Signal -> Decision @description("Links a signal to the decision it informed.") {
weight: F64?
}
edge Supersedes: Decision -> Decision @card(0..1) @description("A newer decision that replaces an older one.")
edge Supports: Signal -> Signal @description("One signal corroborating another.") {
@unique(src, dst)
}