Nodes
Node types define the vertices of your graph with typed properties, an optional key, and constraints.
A node declaration creates a vertex type in the graph. Each node type gets its own storage table with an auto-generated id column plus one column per property.
Basic syntax
node Person {
name: String @key
age: I32?
}The identifier after node is the type name. Inside the braces you declare properties, each with a name, a type, and zero or more annotations.
Properties
Every property has a name, a colon, and a type. Supported scalar types include String, Bool, I32, I64, U32, U64, F32, F64, Date, and DateTime. You can also use enums, lists, vectors, and blobs.
node Paper {
doi: String @key
title: String @index
year: I32
abstract: String?
keywords: [String]
embedding: Vector(1536) @embed
status: enum(draft, review, published)
}Required vs optional
By default every property is required — a row cannot be inserted without it. Append ? to make a property nullable:
node Person {
name: String # required
age: I32? # optional, may be null
}Property annotations
Annotations appear after the type on the same line.
| Annotation | Purpose |
|---|---|
@key | Stable external identity. At most one per type. Immutable after creation. Implies @index. |
@unique | No two rows may share the same value in this column. |
@index | Build a BTree index for fast lookups and range scans. |
@embed | Mark a vector property for automatic embedding. Optionally specify a source: @embed(source: title). |
@description("...") | Human-readable description, surfaced in tooling and documentation. |
@instruction("...") | Prompt-level guidance for agent systems that read the schema. |
The @key annotation
A key is the stable, external identity of a node. It is how edges reference nodes in load files, how queries bind variables, and how agents address entities.
node Company {
name: String @key
}Rules:
- At most one
@keyper node type. - The key property must be required (not nullable).
- Key values are immutable — once a node is created, its key cannot change.
@keyimplies@index; you do not need to add both.
The @embed annotation
Used on Vector(N) properties to indicate that the vector should be populated from a text source property:
node Document {
title: String @index
body: String
embedding: Vector(1536) @embed(source: body)
}Body constraints
Constraints declared inside the node body (but outside any property line) apply to the type as a whole.
node Measurement {
sensor_id: String
timestamp: DateTime
value: F64
@unique(sensor_id, timestamp)
@index(sensor_id, timestamp)
@range(value, 0.0..1000.0)
}| Constraint | Purpose |
|---|---|
@range(prop, min..max) | Restrict a numeric property to a closed range. Checked at load and mutation time. |
@check(prop, regex) | Validate a string property against a regular expression. |
@unique(p1, p2, ...) | Composite uniqueness constraint across multiple properties. |
@index(p1, p2, ...) | Composite BTree index across multiple properties. |
Type-level annotations
Annotations can also appear directly after the opening brace to describe the type itself:
node Actor {
@description("A person or system that performs actions within the workflow.")
@instruction("When creating Actor nodes, always provide a role.")
name: String @key
role: String
}Implementing interfaces
A node type can implement one or more interfaces to inherit a required set of properties:
interface Searchable {
title: String @index
embedding: Vector(1536) @embed(source: title)
}
node Article implements Searchable {
slug: String @key
title: String @index
embedding: Vector(1536) @embed(source: title)
body: String
}The compiler checks that every property declared in the interface is present in the node with a matching name, type, and nullability.
Full example
A schema from a decision-tracking graph with multiple node types:
interface Searchable {
title: String @index
embedding: Vector(1536) @embed(source: title)
}
node Actor {
name: String @key
role: String
bio: String?
@description("A person or system involved in decisions.")
}
node Decision implements Searchable {
slug: String @key
title: String @index
summary: String
status: enum(proposed, accepted, rejected, superseded)
embedding: Vector(1536) @embed(source: title)
@description("A recorded decision with its current lifecycle status.")
}
node Signal implements Searchable {
slug: String @key
title: String @index
body: String
strength: enum(strong, moderate, weak)
embedding: Vector(1536) @embed(source: title)
@description("An observation or data point that informs decisions.")
}