#
Graph Query Language
#
Graph Query Language
Curiosity Workspace provides a graph query interface used by connectors, endpoints, and (in limited form) front-ends to traverse and filter the knowledge graph.
This page describes the concepts and common patterns you will see in Curiosity queries. The exact API surface can vary by environment/version, but the building blocks remain consistent.
If you are new to graph query languages, think of the Curiosity query API as a guided, step-by-step traversal. You start with a set of nodes, move along edges, filter the results, and then emit the output. Each method in the chain performs one clear action.
#
Core concepts
StartAt(...): defines the starting set of nodes (by type, key, or UID).Out(...)/In(...): traverses edges to neighboring nodes.Where(...): filters nodes by predicate (properties, timestamp, type).SortByTimestamp(...): orders results by time when a timestamp is present.Take(...)/Skip(...)/TakeAll(): pagination and bounded reads.Emit(...): returns results (nodes, aggregates, summaries).
#
Choosing a starting set
Start sets are the most important performance lever. The narrower the start set, the cheaper the traversal.
- By type:
StartAt("Device")returns all nodes of that type. - By key:
StartAt("Manufacturer", "Apple")returns the node with a known key. - By UID:
StartAt(uid)is the most specific start (when you already have a UID).
#
Output shapes
Most queries end with Emit(...), but there are a few common variations:
- Nodes:
Emit("N")returns nodes. - Scores:
EmitWithScores()returns nodes and relevance scores (for similarity queries). - UIDs:
AsUIDEnumerable()is useful when a query is feeding another API.
#
Choosing a starting set
Start sets are the most important performance lever. The narrower the start set, the cheaper the traversal.
- By type:
StartAt("Device")returns all nodes of that type. - By key:
StartAt("Manufacturer", "Apple")returns the node with a known key. - By UID:
StartAt(uid)is the most specific start (when you already have a UID).
#
Output shapes
Most queries end with Emit(...), but there are a few common variations:
- Nodes:
Emit("N")returns nodes. - Scores:
EmitWithScores()returns nodes and relevance scores (for similarity queries). - UIDs:
AsUIDEnumerable()is useful when a query is feeding another API.
#
Query anatomy
Most queries follow:
- Choose a starting set
- Traverse edges
- Filter
- Paginate
- Emit output
Think of each line in a query as a pipeline stage. The output of one stage is the input to the next.
#
Mental model for beginners
You can read most Curiosity queries as a sentence:
Start at Manufacturer = Apple, go out to Device, take the first 50, emit nodes.
That sentence corresponds to the chain below:
return Q().StartAt("Manufacturer", "Apple")
.Out("Device")
.Take(50)
.Emit("N");
#
Common query patterns
The examples below focus on common tasks: browsing a type, traversing relationships, scoping searches, and summarizing graph neighborhoods.
In code, node and edge names may appear as strings (for quick prototypes) or as schema constants (for example, N.Device.Type). Both represent the same graph objects.
#
Example: list nodes by type
Use this to get a quick slice of a type while validating your schema.
// Return 10 nodes of a given type
return Q().StartAt("Device").Take(10).Emit("N");
#
Example: paginate through a result set
Pagination keeps responses bounded and is safer for UI and API calls.
// Skip the first 20 results and take the next 20
return Q().StartAt("Device")
.Skip(20)
.Take(20)
.Emit("N");
#
Example: traverse a relationship
Use Out(...) and In(...) to move along edges and gather neighbors.
// Starting from a manufacturer node (by key), return related devices
return Q().StartAt("Manufacturer", "Apple")
.Out("Device")
.Take(50)
.Emit("N");
#
Example: filter by a property
Property filters run on the current working set. Start narrow to keep them fast.
// Find devices whose name contains a manufacturer label
return Q().StartAt(N.Device.Type)
.Where(n => n.GetString(N.Device.Name).Contains("Apple"))
.Take(50)
.Emit("N");
#
Example: multi-hop traversal (type -> related -> related)
Each hop builds on the previous result set. Keep multi-hop paths short.
// Find support cases connected to devices for a manufacturer
return Q().StartAt("Manufacturer", "Apple")
.Out("Device")
.Out("SupportCase")
.Take(50)
.Emit("N");
#
Example: filter by time (event-like nodes)
Use timestamp sorting or filters to surface recent activity.
// Return recent cases (sorted by timestamp if available)
return Q().StartAt("SupportCase")
.SortByTimestamp(oldestFirst: false)
.Take(10)
.Emit("N");
#
Example: use a graph query to scope search
This pattern powers “search within context” experiences.
Graph queries are often used to produce a target set for search (for example, search only within a manufacturer or account). This pattern combines graph traversal with the Search DSL.
var request = SearchRequest.For("screen issue");
request.BeforeTypesFacet = new([] { "SupportCase" });
request.TargetUIDs = Q().StartAt("Manufacturer", "Apple")
.Out()
.AsUIDEnumerable()
.ToArray();
var query = await Graph.CreateSearchAsync(request);
return query.Emit();
#
Example: semantic similarity (embeddings)
Use embeddings when keyword search is not sufficient.
// Return similar cases with scores when embeddings are enabled
return Q().StartAtSimilarText("laptop overheating", nodeTypes: new[] { "SupportCase" })
.EmitWithScores();
#
Example: graph summaries (high leverage during debugging)
Summaries are a fast way to understand the structure of your graph.
// Summarize the graph or neighborhood
return Q().EmitSummary();
// Summarize neighbors for a type
return Q().StartAt("Part").EmitNeighborsSummary();
#
Best practices
- Bound traversal: always use
Take(...)/pagination unless you truly needTakeAll(). - Start narrow: pick the most specific
StartAt(...)possible to keep traversals efficient. - Move in small steps: add a single traversal or filter at a time and validate the output.
- Model for queries: if queries are complex, the schema likely needs another edge or entity.
- Prefer deterministic computation in queries/endpoints: use LLMs for narration, not graph computation.
#
If you are coming from Cypher
Curiosity queries are imperative and chain-based instead of declarative pattern matching. A few common differences:
- No
MATCH/RETURNblock: each method call is a step in the query pipeline, andEmit(...)produces the final output. - Traversal is explicit: use
Out(...)orIn(...)to move across edges rather than pattern arrows. - Start set first: you always begin with a
StartAt(...)set instead of binding variables in a match clause. - No graph-wide scans by default:
Take(...)and scoping are expected to keep queries bounded.
Mapping example (conceptual):
Cypher: MATCH (m:Manufacturer {name: "Apple"})-[:Device]->(d) RETURN d LIMIT 50
Curiosity: Q().StartAt("Manufacturer", "Apple").Out("Device").Take(50).Emit("N")
#
Related pages
- Search queries: Search DSL
- Schema concepts: Schema Reference