Querying the Graph
The graph query interface is a fluent C# API for traversing and querying the knowledge graph. The full interface is available inside Custom Endpoints and the Shell via the Q() / Query() global helpers. A subset is available in Data Connectors via graph.QueryAsync().
For the complete method reference, see Graph Query Language. This page covers the most common patterns and how to read node properties.
Using the Query Interface
In Endpoints and the Shell
The Q() helper creates a new query. Chain methods to build the pipeline, then either return the result directly or materialize it to C# objects for further logic.
// Return results directly from an endpoint
return Q().StartAt(N.SupportCase.Type)
.SortByTimestamp(oldestFirst: false)
.Take(50)
.Emit("N");
// Iterate over results in code
foreach (var node in Q().StartAt(N.SupportCase.Type).AsEnumerable())
{
var summary = node.GetString(N.SupportCase.Summary);
}
// Count matching nodes
int total = Q().StartAt(N.SupportCase.Type)
.WhereString(N.SupportCase.Type, N.SupportCase.Status, "Open")
.Count();
In Data Connectors
Use graph.QueryAsync(), which accepts a lambda over the library IQuery (a subset) and returns a QueryResults object.
var response = await graph.QueryAsync(q =>
q.StartAt(nameof(Nodes.Device)).Take(10).Emit("N"));
var nodes = response.GetEmitted("N");
See Querying in Data Connectors for the full data connector query reference.
Returning Results: Shell vs. Endpoints
A query is built the same way in both contexts — the difference is how you turn the result set into output.
- In the Shell, you explore interactively. Reach for the
Emitfamily (Emit,EmitCount,EmitWithEdges,EmitSummary, …): it sends the matched nodes straight to the result panel, with no types to define and no mapping to write. - In Endpoints, you usually want a stable, typed JSON shape for the caller. Materialize the query with
AsEnumerable(),ToList(),First(), and friends, then map eachReadOnlyNodeinto arecordand return that.
EmitThe Emit family is the quickest way to inspect data while you work in the Query or Shell tab. The matched nodes are serialized to the result panel as-is.
// Most recent open cases, dumped to the result panel
Q().StartAt(N.SupportCase.Type)
.WhereString(N.SupportCase.Type, N.SupportCase.Status, "Open")
.SortByTimestamp(oldestFirst: false)
.Take(50)
.Emit("cases");
In an endpoint, materialize the same query and project each node into a record, so callers get a predictable JSON contract.
public record OpenCase(string Id, string Summary, DateTimeOffset Opened);
var cases = Q().StartAt(N.SupportCase.Type)
.WhereString(N.SupportCase.Type, N.SupportCase.Status, "Open")
.SortByTimestamp(oldestFirst: false)
.Take(50)
.AsEnumerable()
.Select(n => new OpenCase(
n.GetString(N.SupportCase.Id),
n.GetString(N.SupportCase.Summary),
n.GetTime(N.SupportCase.Time).ToDateTimeOffset()))
.ToList();
return Ok(cases);
Why records in endpoints
Emit serializes Curiosity's internal node shape, which changes as you add fields and is awkward to consume from a typed client. Mapping into a record keeps the HTTP response decoupled from the graph schema and gives front-end and connector code a stable type to deserialize into.
Emit family — Shell and direct responses
The Emit methods send results to the output without materializing them into C# objects. They are the default choice in the Shell, and can also be returned directly from an endpoint when the raw node shape is acceptable.
| Method | Description |
|---|---|
Emit(string name, params string[] fields) |
Return matched nodes, optionally restricting fields. |
EmitWithEdges(string name, params string[] fields) |
Return nodes with their edge data. |
EmitWithScores(string name, params string[] fields) |
Return nodes with their relevance scores. |
EmitCount(string name) |
Return only the count of matched nodes. |
EmitUIDs(string name) |
Return only node UIDs. |
EmitSummary(string name) |
Return a structural graph summary (useful in the shell). |
EmitNeighborsSummary(string name) |
Return a summary of edges from the current node set. |
Materialize to C# objects — endpoint logic
Use these when you need to work with results in code — filtering, projecting into a record, or computing a response. This is the usual path when writing an endpoint.
| Method | Return Type | Description |
|---|---|---|
AsEnumerable() |
IEnumerable<ReadOnlyNode> |
Lazy iteration without loading all nodes at once. |
AsEnumerableAsync(...) |
IAsyncEnumerable<ReadOnlyNode> |
Async lazy iteration; parallelizes reads. Accepts a CancellationToken. See Async Operations. |
AsUIDEnumerable() |
IEnumerable<UID128> |
UIDs only — no node content loaded. |
AsTypedUIDEnumerable() |
IEnumerable<TypedUID128> |
Typed UID + node type pairs. |
AsScoredUIDEnumerable() |
IEnumerable<ScoredUID> |
UIDs with relevance scores. |
First() / FirstOrDefault() |
ReadOnlyNode |
First node. First() throws when empty; FirstOrDefault() returns null. |
Single() / SingleOrDefault() |
ReadOnlyNode |
The one node. Both throw when more than one element; Single() also throws when empty. |
ToList() |
List<ReadOnlyNode> |
Materialize all nodes into a list. |
ToUIDList() |
List<UID128> |
All UIDs as a list. |
ToUIDSet() |
HashSet<UID128> |
All UIDs as a set. |
ToScoredUIDList() |
List<ScoredUID> |
All scored UIDs as a list. |
ToDictionary(keySelector, …) |
Dictionary<TKey, …> |
Index nodes by a key selector (optionally with a value selector). |
ToJsonAsync() |
Task<string> |
Serialize results to a JSON string. |
Count() |
int |
Total count. |
Any() |
bool |
True if any results exist. |
// Lazy iteration (efficient for large sets)
foreach (var node in Q().StartAt(N.SupportCase.Type).AsEnumerable())
{
// process each node
}
// Materialize to a list
var recentCases = Q().StartAt(N.SupportCase.Type)
.SortByTimestamp(oldestFirst: false)
.Take(20)
.ToList();
// Check existence
bool hasOpen = Q().StartWhereString(N.SupportCase.Type, N.SupportCase.Status, "Open").Any();
Accessing Node Properties
Nodes returned from AsEnumerable(), ToList(), or inside a Where() predicate expose typed accessor methods.
| Data Type | Accessor | List Accessor |
|---|---|---|
| String | GetString(key) |
GetStringList(key) |
| Integer | GetInt(key) |
GetIntList(key) |
| Boolean | GetBool(key) |
GetBoolList(key) |
| Float | GetFloat(key) |
GetFloatList(key) |
| Double | GetDouble(key) |
GetDoubleList(key) |
| Long | GetLong(key) |
GetLongList(key) |
| DateTime | GetTime(key) |
GetTimeList(key) |
| UID | GetUID128(key) |
GetUID128List(key) |
| Dynamic | node[key] |
— |
Use auto-generated constants (e.g. N.SupportCase.Content) instead of raw strings to avoid typos:
foreach (var node in Q().StartAt(N.SupportCase.Type).AsEnumerable())
{
var id = node.GetString(N.SupportCase.Id);
var summary = node.GetString(N.SupportCase.Summary);
var time = node.GetTime(N.SupportCase.Time);
}
Common Patterns
Pagination
class PageRequest { public int Page { get; set; } }
const int pageSize = 50;
var req = Body.FromJson<PageRequest>();
return Q().StartAt(N.Part.Type)
.Page(req.Page, pageSize) // shorthand for Skip(req.Page * pageSize).Take(pageSize)
.Emit();
Filter by related entity
var deviceUID = Node.GetUID(N.Device.Type, "MacBook Air");
return Q().StartAt(N.SupportCase.Type)
.IsRelatedTo(deviceUID)
.SortByTimestamp(oldestFirst: false)
.Take(50)
.Emit("N");
Semantic similarity with graph filter
class Request { public string Query { get; set; } public string Manufacturer { get; set; } }
var req = Body.FromJson<Request>();
return (await Q().StartAtSimilarTextAsync(req.Query, nodeTypes: new[] { N.SupportCase.Type }, count: 500))
.IsRelatedTo(Node.GetUID(N.Manufacturer.Type, req.Manufacturer))
.EmitWithScores();
Set operations
// Union: combine two queries
var open = Q().StartWhereString(N.SupportCase.Type, N.SupportCase.Status, "Open");
var high = Q().StartWhereString(N.SupportCase.Type, N.SupportCase.Priority, "High");
return open.Union(high).Distinct().SortByTimestamp(oldestFirst: false).Emit("N");
Shell exploration
// Graph structure overview
return Q().EmitSummary();
// What edges does SupportCase have?
return Q().StartAt(N.SupportCase.Type).EmitNeighborsSummary();
// Analytics: count cases per device
return Q().StartAt(N.Device.Type).AsEnumerable()
.ToDictionary(
n => n.GetString(N.Device.Name),
n => Q().StartAt(n.UID).Out(N.SupportCase.Type).Count()
);
Next Steps
- Full method reference: Graph Query Language
- Search integration: Search Configuration
- Data connector queries: Querying in Data Connectors