Curiosity

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.

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:

  1. Stop writing it from the connector.
  2. Wait one ingestion cycle.
  3. 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.

© 2026 Curiosity. All rights reserved.