Curiosity

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

erDiagram Device { string Name PK } Part { string Name PK string ManufacturerHint } Manufacturer { string Name PK } SupportCase { string Id PK string Summary string Content datetime Time } SupportCaseMessage { string Id PK string Body datetime Time } Status { string Name PK } Device ||--o{ Part : HasPart Part }o--|| Manufacturer : MadeBy SupportCase}o--|| Device : ForDevice SupportCase}o--|| Status : HasStatus SupportCase||--o{ SupportCaseMessage : HasMessage
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 navigationManufacturer and Status are nodes, not raw strings on SupportCase, so the workspace can render manufacturer pages and status facets out of the box.
  • Graph-constrained searchSearchRequest.TargetUIDs lets 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 SupportCase and conversation messages on SupportCaseMessage, so chunked embeddings stay focused.

Referenced by

© 2026 Curiosity. All rights reserved.
Powered by Neko