Schema Reference
This page is the authoritative reference for declaring node and edge schemas with the Curiosity.Library SDK. Use it as a lookup when you're writing a connector, ingestion pipeline, or any code that talks to a Curiosity Workspace.
For the conceptual "why" — when to use a key vs a property, how schemas evolve, what to index — see Graph model and Schema design.
Two ways to declare a schema
You can declare a node schema either declaratively (with attributes on a C# class) or fluently (by calling builder methods). Edge schemas are always fluent — they're just a name.
Declarative (attributes on a class)
using Curiosity.Library;
[Node]
public class SupportCase
{
[Key] public string Id { get; set; }
[Property] public string Summary { get; set; }
[Property] public string Status { get; set; }
[Property] public int Priority { get; set; }
[Timestamp] public DateTimeOffset OpenedAt { get; set; }
}
var schema = Schema.NodeFromType<SupportCase>();
The class name (or [Node(Name = "…")]) becomes the node type. The [Key] property is the identity field; every node type has exactly one and it must be a string. The [Timestamp] property — if present — is what time filters and recency sort use.
Fluent (builder methods)
var supportCase = Schema
.NewNode("SupportCase", key: "Id")
.Field("Summary", FieldType.String)
.Field("Status", FieldType.String)
.Field("Priority", FieldType.Int32)
.Field("OpenedAt", FieldType.Time);
var hasDevice = Schema.NewEdge("HasDevice");
Use the fluent form when the shape isn't known at compile time — for example when a connector reads a remote source's metadata and maps it into a schema at startup.
Attribute reference
The C# SDK ships four attributes. Anything implied by other audits ([VectorIndex], [Encrypted], etc.) is not a Library attribute — those concerns are configured at the workspace level, not in code.
| Attribute | Applies to | Required | Notes |
|---|---|---|---|
[Node] |
class | yes | Marks a class as a node type. Name overrides the class name. |
[Key] |
property | yes | Exactly one per node type. Must be string. Inherits from [Property]. |
[Property] |
property | no | Maps a CLR property to a schema field. Name overrides the property name. |
[Timestamp] |
property | no | At most one per node type. Property type must be DateTimeOffset or Time. |
[Key] and [Timestamp] both derive from [Property], so on a single property only one of the three applies.
Schema-level constraints
These are enforced at registration time. Violating them throws when the schema is built.
- Node: exactly one
[Key]property of typestring. - Node: at most one
[Timestamp]property. - Node: field names
Type,UID,Key,Timestamp,Edges,EdgeCountare reserved and cannot be used as properties. - Edge: must not declare a key (edges are not addressable by key).
- Edge: field names
Type,To,ToType,UID,Timestampare reserved. - Field names must be unique within a schema (case-sensitive). Re-adding the same field throws
Duplicated field {name}.
Field types
FieldType is the enum used by the fluent builder. The declarative form maps CLR types onto these automatically.
FieldType |
C# type | JSON wire shape | Notes |
|---|---|---|---|
String |
string |
string | Default for text. |
Boolean |
bool |
true / false |
|
Byte |
byte |
number (0–255) | |
SByte |
sbyte |
number (−128–127) | Signed byte. |
Char |
char |
string (1 char) | |
Int32 |
int |
number | |
Int64 |
long |
number / string | Large values may serialize as string to avoid loss. |
UInt32 |
uint |
number | |
UInt64 |
ulong |
number / string | |
Float |
float |
number | |
Double |
double |
number | |
Decimal |
decimal |
string | Encoded as a string to preserve precision. |
Time |
DateTime, DateTimeOffset, Time |
ISO-8601 string | Use for [Timestamp] properties. |
GeoPoint |
GeoPoint |
{ lat, lon } |
Resume indexing after enabling. |
Language |
Language |
ISO 639 code | |
UID128 |
UID128 |
base64 string | The workspace's internal node identifier. |
Collections
The same set of FieldType values is supported through three collection variants:
| Builder method | C# property type | Wire shape |
|---|---|---|
Field(name, type) |
scalar | scalar |
ListField(name, type) |
T[] or List<T> |
JSON array |
DictionaryField(name, type) |
Dictionary<string, T> |
JSON object |
TableField(name, type) |
(fluent only — tabular data) | array of arrays |
In the declarative form Curiosity infers ListField for arrays and List<T>, and DictionaryField for Dictionary<string, T>. Dictionaries must have string keys; any other key type throws at schema-build time. Nullable scalars (int?, DateTime?) collapse to their non-nullable form.
Edge schemas
An edge schema is just a name. Edges have no properties of their own at the schema level — the meaning of the edge lives in the type name and the two endpoints.
var hasDevice = Schema.NewEdge("HasDevice");
var assignedTo = Schema.NewEdge("AssignedTo");
Conventionally, name edges as a verb phrase that reads naturally from source to target: Case -> HasDevice -> Device. Add a reverse edge if you traverse both ways often: Device -> AppearsInCase -> Case. Both edges are independent — committing the forward one does not create the reverse.
Keys and identity
Keys define identity. Two nodes of the same type with the same key are the same node — re-ingesting will update, not duplicate.
Constraints:
- exactly one
[Key]per node type - type must be
string - non-empty; the SDK rejects empty keys at commit time
- stable over time; changing a key changes the node's identity
If your source doesn't expose a natural key, build one deterministically from fields that do uniquely identify the record (e.g. "jira:" + projectKey + "-" + issueNumber). Never use a generated UUID for the key — re-runs will create duplicates.
The Key property
Every compiled node schema exposes a read-only Key property that aliases whatever field the schema declared as its key. It is there so code handed a node of a type it does not know can still read the one field that identifies it, without having to look up what that type called its key.
- Read-only. A write through
Keyis refused the same way a write to the key field itself is — the key is identity, and changing it would make a different node. - A computed alias, not a field. It takes no slot on the wire; the key field's own value is still what is serialized, so nothing about the stored or transmitted shape changes.
- Answered by the field indexer.
node["Key"]returns the key. The strongly-typedGet*accessors stay a view over the fields the schema actually declares. Keyis a reserved schema field name. A new schema cannot declare a field calledKey.- Compatibility: a schema that already declared a field spelled
Keybefore the reservation keeps compiling. There, that field's own property answers to the name and the alias stands down.
Indexing
What gets indexed for text or vector search is not a schema-level decision in code. You declare schemas, then configure indexes against them in the workspace (UI or admin API):
- text-search fields: see Text search
- vector-embedding fields and chunking: see Vector search
- entity extraction and NLP pipelines: see NLP pipelines
This keeps the schema layer small and lets you change retrieval behaviour without re-deploying connectors.
Aliases (conceptual)
Aliases are alternate strings that resolve to the same node — useful for spelling variants, abbreviations, and cross-source normalization. They're managed at the entity-extraction layer, not declared in the schema. See Entity capture.
Reserved / system node types
The workspace stores some configuration (pipelines, search indexes, scheduled tasks) as nodes in a hidden system namespace. You won't see them in user-facing search but they're inspectable for debugging through the admin UI.
Related pages
- Schema design — how to think about a schema before you write it.
- Graph query language — how to traverse the graph you just defined.
- Curiosity.Library SDK (C#) — the full client surface.
- Reindexing and re-embedding — what to do after a schema change.