Interfaces
Declare shared properties once and require multiple node types to include them.
An interface declares a set of properties that one or more node types must include. Interfaces keep schemas consistent when several types share the same fields — for example, a common title + embedding pair for anything that needs to be searchable.
Declaring an interface
interface Searchable {
title: String @index
embedding: Vector(1536) @embed(source: title)
}An interface looks like a node without storage — it has a name and a list of typed, annotated properties.
Implementing an interface
Use the implements keyword after the node name:
node Article implements Searchable {
slug: String @key
title: String @index
embedding: Vector(1536) @embed(source: title)
body: String
}The node must redeclare every property from the interface. The compiler checks that each one matches by name, type, nullability, and annotations. A mismatch is a compile error.
What the compiler checks
| Rule | Example violation |
|---|---|
| Missing property | Interface declares title: String but the node omits it. |
| Type mismatch | Interface declares title: String, node declares title: I32. |
| Nullability mismatch | Interface declares title: String, node declares title: String?. |
| Missing annotation | Interface declares @index on a property, node omits it. |
Multiple interfaces
A node can implement more than one interface by separating them with commas:
interface Slugged {
slug: String @key
}
interface Searchable {
title: String @index
embedding: Vector(1536) @embed(source: title)
}
node Document implements Slugged, Searchable {
slug: String @key
title: String @index
embedding: Vector(1536) @embed(source: title)
body: String
}If two interfaces declare the same property name, the type and annotations must be identical. Conflicting declarations are a compile error.
Interfaces with @key
An interface can require that implementing types use a particular property as their key:
interface Slugged {
slug: String @key
}
node Page implements Slugged {
slug: String @key
title: String
}
node Post implements Slugged {
slug: String @key
content: String
}This guarantees that every type implementing Slugged has a slug key, so queries and load files can address them uniformly.
No runtime concept
Interfaces are strictly compile-time. They do not create storage tables, do not appear in query results, and have no row identity. Their only purpose is to enforce structural consistency across node types during schema compilation.
Full example
A schema using interfaces to standardize searchable, slugged content:
interface Slugged {
slug: String @key
}
interface Searchable {
title: String @index
embedding: Vector(1536) @embed(source: title)
}
node Decision implements Slugged, Searchable {
slug: String @key
title: String @index
summary: String
status: enum(proposed, accepted, rejected, superseded)
embedding: Vector(1536) @embed(source: title)
}
node Signal implements Slugged, Searchable {
slug: String @key
title: String @index
body: String
strength: enum(strong, moderate, weak)
embedding: Vector(1536) @embed(source: title)
}
node Actor implements Slugged {
slug: String @key
name: String
role: String
}All three types share a slug key. Decision and Signal also share the searchable title + embedding pair. The compiler enforces this — if a future contributor adds a new type that implements Searchable but forgets the embedding property, the schema will not compile.