Technical Support: Data Model
This page walks through the graph schema of the technical-support sample, end to end. By the time you're done you'll have working C# (with [Node] / [Property] attributes) and a fluent fallback for the same shape — both verified against the Curiosity.Library SDK.
The full source lives at curiosity-ai/technical-support.
Entity-relationship diagram
| Node | Stable key | Why this key |
|---|---|---|
Device |
Name |
Devices are uniquely identified by their model name. |
Part |
Name |
Parts are normalized by their canonical part number. |
Manufacturer |
Name |
Normalized so multiple parts roll up to one vendor. |
SupportCase |
Id |
The case/ticket ID from the source system. |
SupportCaseMessage |
Id |
Hashed {CaseId}#{Index} so re-runs don't duplicate. |
Status |
Name |
Normalized so multiple cases share a single status node. |
Schema in code — attribute form
The connector under TechnicalSupport.Connector/Schema/Nodes/ defines each entity as a C# class with attributes from Curiosity.Library:
using Curiosity.Library;
namespace TechnicalSupport.Schema.Nodes;
[Node]
public class Device
{
[Key] public string Name { get; set; }
}
[Node]
public class Part
{
[Key] public string Name { get; set; }
[Property] public string ManufacturerHint { get; set; }
}
[Node]
public class Manufacturer
{
[Key] public string Name { get; set; }
}
[Node]
public class Status
{
[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; }
}
[Node]
public class SupportCaseMessage
{
[Key] public string Id { get; set; }
[Property] public string Body { get; set; }
[Timestamp] public DateTimeOffset Time { get; set; }
}
The matching Schema/Edges.cs keeps edge names in one place:
namespace TechnicalSupport.Schema;
public static class Edges
{
public const string HasPart = nameof(HasPart);
public const string PartOf = nameof(PartOf);
public const string MadeBy = nameof(MadeBy);
public const string Makes = nameof(Makes);
public const string ForDevice = nameof(ForDevice);
public const string HasSupportCase = nameof(HasSupportCase);
public const string HasStatus = nameof(HasStatus);
public const string OfStatus = nameof(OfStatus);
public const string HasMessage = nameof(HasMessage);
public const string OfCase = nameof(OfCase);
}
Synchronize everything once at connector startup:
await graph.CreateNodeSchemaAsync<Schema.Nodes.Device>();
await graph.CreateNodeSchemaAsync<Schema.Nodes.Part>();
await graph.CreateNodeSchemaAsync<Schema.Nodes.Manufacturer>();
await graph.CreateNodeSchemaAsync<Schema.Nodes.Status>();
await graph.CreateNodeSchemaAsync<Schema.Nodes.SupportCase>();
await graph.CreateNodeSchemaAsync<Schema.Nodes.SupportCaseMessage>();
await graph.CreateEdgeSchemaAsync(typeof(Schema.Edges));
Edges are declared as bi-directional pairs (HasPart / PartOf). When you Link(...) you give both names so traversal works from either end without a second call.
Schema in code — fluent fallback
If you'd rather not author POCO classes (for example, when the source data is a free-form JSON blob), you can declare the same shape inline:
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"));
await graph.CreateEdgeSchemaAsync(EdgeSchema.New("ForDevice").Reverse("HasSupportCase"));
The fluent form is convenient for hand-written ETL but is harder to refactor — attribute-typed classes get compile-time checks when you rename a property.
Why this model works well
- Entity-centric navigation —
ManufacturerandStatusare nodes, not raw strings onSupportCase, so the workspace can render manufacturer pages and status facets out of the box. - Graph-constrained search —
SearchRequest.TargetUIDslets you scope free-text search to "anything reachable from this Device". - Idempotent ingestion — every key is a real identifier from the source, so re-running the connector never produces duplicates.
- AI-tool friendly — case bodies live on
SupportCaseand conversation messages onSupportCaseMessage, so chunked embeddings stay focused.
Cross-links to product docs
- Modeling guidance: Schema design
- Querying patterns: Graph query language
- Attribute reference: Curiosity.Library SDK
- Permission-aware access: Access control