Defining Schemas
A graph schema is the set of node types, properties, edge types, and indexes the workspace knows about. For connectors, you describe the schema in one of two ways: attributed C# classes (idiomatic, refactor-safe) or fluent builders (handy when the source is dynamic).
For a step-by-step walkthrough see Custom connector from scratch and the technical-support data model.
Attributed classes (recommended)
The connector defines POCOs decorated with attributes from Curiosity.Library:
using Curiosity.Library;
[Node]
public class Device
{
[Key] public string Name { get; set; }
}
[Node]
public class SupportCase
{
[Key] public string Id { get; set; }
[Property] public string Summary { get; set; }
[Property] public string Content { get; set; }
[Timestamp] public DateTimeOffset Time { get; set; }
}
Attribute reference:
| Attribute | Applies to | Meaning |
|---|---|---|
[Node] |
class | Marks the class as a graph node type. Class name is the node type name. |
[Key] |
property | The stable identifier for this node. Exactly one per type. |
[Property] |
property | A searchable property on the node. |
[Timestamp] |
property | A timestamp property. Sortable via SortByTimestamp. |
[Ignore] |
property | Skip this property when ingesting. |
The class name becomes the node type name (Device, SupportCase). To get a compile-time-safe string for use in queries, use nameof(Device) or — better — the auto-generated N.Device.Type helper available in endpoints and shells (see Auto-generated helpers).
Edges as constants
Edges don't have classes — they're string-named, declared as constants for type safety. Curiosity uses bi-directional edges: declare both ends and Link takes both names.
public static class Edges
{
public const string HasPart = nameof(HasPart);
public const string PartOf = nameof(PartOf);
public const string ForDevice = nameof(ForDevice);
public const string HasSupportCase = nameof(HasSupportCase);
}
Synchronizing the schema
Call once at startup. Both methods are idempotent — they no-op when the schema already matches:
await graph.CreateNodeSchemaAsync<Device>();
await graph.CreateNodeSchemaAsync<SupportCase>();
await graph.CreateEdgeSchemaAsync(typeof(Edges));
CreateNodeSchemaAsync<T>(overwrite: bool) defaults to overwrite: false — if the workspace has additional properties on the schema that aren't on T, they're left in place. Use overwrite: true only when you're certain the C# class is the source of truth.
Fluent fallback
When the source is a free-form JSON blob and authoring a POCO per type would be wasted code, use the builder API:
await graph.CreateNodeSchemaAsync(NodeSchema.New("Device")
.WithKey("Name"));
await graph.CreateNodeSchemaAsync(NodeSchema.New("SupportCase")
.WithKey("Id")
.WithProperty("Summary")
.WithProperty("Content")
.WithTimestamp("Time"));
await graph.CreateEdgeSchemaAsync(EdgeSchema.New("HasPart").Reverse("PartOf"));
The trade-off: the fluent form is dynamic but loses compile-time safety. Refactoring a property name in your source can break the connector silently — you'll only notice when the search index goes stale.
Schema evolution
When a property is added in code but not yet in the workspace, the connector won't fail — AddOrUpdate will write the new property and the workspace picks it up automatically. To remove a property:
- Stop writing it from the connector.
- Wait one ingestion cycle.
- Remove it from the workspace's schema configuration (under Settings → Schema).
Renaming is the same as remove + add; old data keeps the old property name until you migrate it explicitly.
Cross-links
- Ingestion — applying the schema with
AddOrUpdate/TryAdd/Link. - Idempotency —
[Key]is what makes re-runs safe. - Access control — restricting access on nodes you've defined.
- Schema design (concepts) — modeling guidance, separate from the SDK syntax.