
Dm Graph Traversal
- 20 installs
- 5 repo stars
- Updated August 4, 2026
- cognitedata/builder-skills
Traverse graph structures in data models
About
Navigates complex graph structures in Cognite data models and databases. Traverses relationships.
- Graph navigation
- Data traversal
Dm Graph Traversal by the numbers
- 20 all-time installs (skills.sh)
- Ranked #3,455 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/cognitedata/builder-skills --skill dm-graph-traversalAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 20 |
|---|---|
| repo stars | ★ 5 |
| Last updated | August 4, 2026 |
| Repository | cognitedata/builder-skills ↗ |
What it does
Traverse graph structures in data models
Files
DM Graph Traversal
Outcome
Ship correct, maintainable, graph-native CDF Data Model reads.
This skill turns query/list ambiguity into a deterministic workflow:
1. Decide whether the read is graph-native. 2. Build safe instances/query payloads. 3. Validate traversal semantics and cursor behavior. 4. Encode merge/dedupe policy explicitly. 5. Lock behavior with payload-shape tests.
---
Decision Tree: instances/query vs instances/list
Use instances/query when any is true:
- You start from one node type and need related nodes/edges.
- You need reverse relation traversal (
direction,through,from). - You would otherwise do multiple list calls and stitch results client-side.
- You need step-specific pagination and relationship-aware filtering.
Use instances/list when all are true:
- Single node/edge type lookup.
- No traversal intent.
- Flat filtering is sufficient.
Heuristic:
- "If this read needs graph context, it is a query."
---
Search-First Entry Pattern
When user intent is discovery/ranking (for example free-text name matching), prefer:
1. instances.search to find/rank candidate anchors. 2. instances.query to hydrate graph-related details for those anchors.
Why:
- Search APIs are better for ranking and fuzzy discovery.
- Query APIs are better for explicit graph traversal and constrained joins.
- This avoids broad traversal scans when the first step is actually discovery.
---
Operating Mode (Hard Rule)
- Default to Node.js/TypeScript workflows for parity checks, examples, and validator tooling.
- Use
code/validate-query-parity.cjsfor payload validation in all normal cases. - Do not introduce Python parity scripts by default in TypeScript repositories.
- Use Python-based validation only if the user explicitly requests Python or no viable Node.js path exists.
---
Related Skill Boundary
This skill is the source of truth for graph query correctness (traversal shape, refs, relation direction, and payload guardrails).
For runtime limits and throughput controls, see dm-limits-and-best-practices (concurrency budgets, semaphore/task-runner usage, retry policy details, and write batching limits).
Practical rule:
- Build the correct graph query with this skill first.
- Then harden runtime behavior at scale with
dm-limits-and-best-practices.
---
Canonical Payload Guardrails
1) Step-level limit placement
- Valid:
with.<step>.limit - Invalid:
with.<step>.nodes.limit,with.<step>.edges.limit
2) select.sources requires properties
If a step uses select.<step>.sources, each source entry includes explicit properties.
3) Start-step constraints
For node start steps, include:
- space filter (
['node','space']) hasDatafor expected view where relevant
4) Versioned traversal refs
In traversal-step filters, use:
[space, 'ViewExternalId/version', property]
5) Step cursor loops
Paginate per step with:
nextCursor.<step>->cursors: { <step>: ... }
6) Deterministic merge semantics
When combining multi-step outputs:
- dedupe by stable IDs (or business keys)
- define tie-breaks (
explicit > fallback,max,latest, etc.)
7) Strict-to-broad fallback
Start with strict server-side constraints (space, exact filters, scoped predicates). Only broaden filters when needed, and keep fallback stages explicit and ordered.
---
Two-Phase Latest Datapoint Rule
When a use case asks for graph relationships plus the latest numeric value, use a two-phase read:
1. Use instances.query to traverse and collect the relevant time-series node IDs. 2. Use datapoints.retrieveLatest on those IDs in batches. 3. Merge latest values back by stable business key.
Why:
instances.queryis best for relationship traversal and filtering.retrieveLatestis the efficient API for last-value reads.- Keeping these responsibilities separate avoids over-fetching and expensive fan-out logic.
Efficiency guardrails:
- Query only IDs/properties needed for downstream latest reads.
- Dedupe node IDs before latest retrieval.
- Batch latest calls (for example, max 100 IDs per request).
- Use
ignoreUnknownIds: trueto tolerate stale references. - Keep aggregation logic deterministic when many series map to one entity.
Reference shape:
const nodeIds = queryResult.items.ptts.map((n) => ({ instanceId: { space: n.space, externalId: n.externalId } }));
const latest = await client.datapoints.retrieveLatest(nodeIds, { ignoreUnknownIds: true });---
Edge-With-Properties Query Pattern
Use this pattern when relationship edges carry business data (for example risk, confidence, allocation, ownership, status, weight).
Mental model:
1. Start from the primary node set (the page/entity context). 2. Traverse to relationship edges as first-class records. 3. Read edge properties explicitly from select.<edgeStep>.sources. 4. Join edge rows to endpoint nodes for labels/details. 5. Aggregate with deterministic dedupe/tie-break rules.
Why this matters:
- Edge properties are business facts; treating edges as transport-only loses critical data.
- Querying nodes first and stitching ad-hoc often creates N+1 calls and double counting.
- Explicit edge-step design keeps lineage and semantics clear.
Generic example:
const result = await client.instances.query({
with: {
start: {
nodes: {
filter: {
and: [
{ equals: { property: ["node", "space"], value: "my_space" } },
{ hasData: [{ type: "view", space: "my_space", externalId: "PrimaryEntity", version: "v1" }] }
]
}
},
limit: 1000
},
links: {
edges: {
from: "start",
direction: "outwards",
filter: {
equals: {
property: ["edge", "type"],
value: { space: "my_space", externalId: "PrimaryToSecondaryLink" }
}
}
},
limit: 1000
},
secondary: {
nodes: {
from: "links",
direction: "outwards"
},
limit: 1000
}
},
select: {
start: { sources: [{ source: { type: "view", space: "my_space", externalId: "PrimaryEntity", version: "v1" }, properties: ["name"] }] },
links: { sources: [{ source: { type: "view", space: "my_space", externalId: "PrimaryToSecondaryLink", version: "v1" }, properties: ["weight", "status"] }] },
secondary: { sources: [{ source: { type: "view", space: "my_space", externalId: "SecondaryEntity", version: "v1" }, properties: ["name"] }] }
}
});Edge aggregation guidance:
- Dedupe by endpoint business key (or edge ID when edge uniqueness matters).
- Define tie-break policy up front (
max(weight), latest timestamp, explicit-over-derived, etc.). - Keep aggregation deterministic and test it directly.
---
Failure Signature Playbook
| Error / Symptom | Likely Cause | Fix |
|---|---|---|
Unexpected field - nodes.limit | limit nested under nodes | move to with.<step>.limit |
Unexpected field - edges.limit | limit nested under edges | move to with.<step>.limit |
properties must not be null | sources without properties | add explicit properties: [...] |
| Unexpectedly slow latest-value endpoint | trying to read latest values via traversal-only flow | split into instances.query + batched retrieveLatest |
| Query path intermittently fails with 429/5xx/timeout | missing transient failure handling | add bounded retries with exponential backoff + jitter |
| Edge properties missing in output | traversed edges but did not project edge properties | add explicit select.<edgeStep>.sources[*].properties for edge view |
| Aggregates inflated after edge traversal | multiple edges per endpoint without dedupe policy | dedupe by stable key and apply explicit tie-break rule |
| Traversal step returns empty, no error | non-versioned traversal ref | use View/version in property refs |
Cannot traverse lists of direct relations inwards. | inwards traversal through list direct relation | traverse from owning node with outwards, or remodel as edge |
| Traversal step empty despite data | missing hasData or wrong direction/identifier | add hasData; verify direction + through.identifier |
| First page works, later missing | cursor loop not step-scoped | iterate nextCursor.<step> |
| Inflated totals | dedupe policy missing | dedupe and apply explicit tie-break |
---
Implementation Workflow
1. Model graph intent (start entity, edge, target entity). 2. Name steps semantically (initiatives, featureLinks, commitments, customerArr). 3. Apply payload guardrails (limit placement, properties, refs, hasData). 4. Implement step cursor loop(s). 5. Map only required fields. 6. Add deterministic join/dedupe logic. 7. Add payload-shape tests. 8. Add bounded retries for transient failures (408/425/429/5xx). 9. (Optional) Cross-check in TypeScript SDK.
---
Testing Requirements (Required For Merge)
For every critical helper, tests must assert payload shape (not only mapped output):
with.<step>.limitexists- nested
nodes.limit/edges.limitabsent - each
select.<step>.sources[*].propertiespresent - traversal refs are versioned where needed
hasDatapresent on constrained start steps- unintended fallback to
instances/listis absent (if query-only design) - retry behavior exists for transient failures in production paths
Example assertion style:
const call = (client.instances.query as ReturnType<typeof vi.fn>).mock.calls[0]?.[0];
expect(call?.with?.initiatives?.limit).toBe(1000);
expect(call?.with?.initiatives?.nodes?.limit).toBeUndefined();
expect(call?.select?.initiatives?.sources?.[0]?.properties).toContain('title');---
TypeScript SDK Parity Check (Recommended)
Use the TypeScript SDK to validate query shape and traversal semantics directly in frontend/backend JavaScript tooling:
const query = {
with: {
cycles: {
nodes: {
filter: {
and: [
{ equals: { property: ["node", "space"], value: "product_portfolio" } },
{
hasData: [{ type: "view", space: "product_portfolio", externalId: "PortfolioReviewCycle", version: "v1" }]
}
]
}
},
limit: 200
}
},
select: {
cycles: {
sources: [
{
source: { type: "view", space: "product_portfolio", externalId: "PortfolioReviewCycle", version: "v1" },
properties: ["key", "displayName", "periodStart", "periodEnd", "status"]
}
]
}
}
};
await client.dataModeling.instances.query(query);Parity checks:
- Step names and cursor keys match TS.
- Limit placement and
propertiesshape are valid. - Traversal filters behave as expected.
- Validate any project payload with:
node skills/dm-graph-traversal/code/validate-query-parity.cjs --query <path-to-query.json> --check all --expect pass- Validate expected failures (negative tests) with:
node skills/dm-graph-traversal/code/validate-query-parity.cjs --query <path-to-query.json> --check all --expect fail- Add
--schema-hints <path-to-schema-hints.json>when running schema-aware relation checks. - Check modes:
sources-properties,limit-placement,start-step-hasdata,versioned-traversal-refs,cursor-shape,inwards-list-direct-relations,all- For latest-value scenarios, apply the two-phase rule (
instances.queryIDs -> batchedretrieveLatest) instead of forcing latest reads into traversal payloads. - Prefer server-side filtering and explicit property lists; avoid wildcard projection (
"*").
---
Anti-patterns To Avoid
1. Broad cross-space retrieval without explicit scope when not required. 2. Wildcard property projection (properties: ["*"]) in production query paths. 3. Client-side filtering when equivalent server-side filters exist. 4. Raw HTTP query payload posting when SDK methods provide equivalent behavior and retries. 5. N+1 relation fetch loops when one traversal query can hydrate the same graph.
---
Done Criteria
- Correct query/list decision documented in PR/code comments.
- Query payload follows all guardrails.
- Merge/dedupe semantics explicit and tested.
- Regression tests cover known failure signatures.
- (High-risk changes) TypeScript SDK parity sanity check completed.
---
References
references/query-vs-list.mdcode/validate-query-parity.cjs
{
"with": {
"initiatives": {
"nodes": {
"filter": {
"and": [
{
"equals": {
"property": ["node", "space"],
"value": "product_portfolio"
}
},
{
"hasData": [
{
"type": "view",
"space": "product_portfolio",
"externalId": "RoadmapInitiative",
"version": "v1"
}
]
}
]
}
},
"limit": 100
}
},
"cursors": {
"badStep": "abc123",
"initiatives": ""
},
"nextCursor": {
"initiatives": "should-not-be-here"
},
"select": {
"initiatives": {
"sources": [
{
"source": {
"type": "view",
"space": "product_portfolio",
"externalId": "RoadmapInitiative",
"version": "v1"
},
"properties": ["title"]
}
]
}
}
}
{
"with": {
"initiatives": {
"nodes": {
"filter": {
"and": [
{
"equals": {
"property": ["node", "space"],
"value": "product_portfolio"
}
},
{
"hasData": [
{
"type": "view",
"space": "product_portfolio",
"externalId": "RoadmapInitiative",
"version": "v1"
}
]
}
]
}
},
"limit": 100
}
},
"cursors": {
"initiatives": "abc123"
},
"select": {
"initiatives": {
"sources": [
{
"source": {
"type": "view",
"space": "product_portfolio",
"externalId": "RoadmapInitiative",
"version": "v1"
},
"properties": ["title"]
}
]
}
}
}
{
"with": {
"equipment": {
"nodes": {
"filter": {
"and": [
{
"hasData": [
{
"type": "view",
"space": "sp_asset_maintenance",
"externalId": "FunctionalAsset",
"version": "v1.0.0"
}
]
},
{
"equals": {
"property": ["node", "space"],
"value": "sp_supply_chain_instances"
}
}
]
}
},
"limit": 1000
},
"productionSchedules": {
"nodes": {
"from": "equipment",
"through": {
"view": {
"space": "ssp_manufacturing",
"externalId": "ProductionSchedule",
"version": "v1.0.0",
"type": "view"
},
"identifier": "equipment"
},
"direction": "inwards"
},
"limit": 1000
}
},
"select": {
"equipment": {
"sources": [
{
"source": {
"space": "sp_asset_maintenance",
"externalId": "FunctionalAsset",
"version": "v1.0.0",
"type": "view"
},
"properties": ["name"]
}
]
},
"productionSchedules": {
"sources": [
{
"source": {
"space": "ssp_manufacturing",
"externalId": "ProductionSchedule",
"version": "v1.0.0",
"type": "view"
},
"properties": ["equipment", "customerOrderRef"]
}
]
}
}
}
{
"with": {
"productionSchedules": {
"nodes": {
"filter": {
"hasData": [
{
"type": "view",
"space": "ssp_manufacturing",
"externalId": "ProductionSchedule",
"version": "v1.0.0"
}
]
}
},
"limit": 1000
},
"equipment": {
"nodes": {
"from": "productionSchedules",
"through": {
"view": {
"space": "ssp_manufacturing",
"externalId": "ProductionSchedule",
"version": "v1.0.0",
"type": "view"
},
"identifier": "equipment"
},
"direction": "outwards"
},
"limit": 1000
}
},
"select": {
"productionSchedules": {
"sources": [
{
"source": {
"space": "ssp_manufacturing",
"externalId": "ProductionSchedule",
"version": "v1.0.0",
"type": "view"
},
"properties": ["equipment", "customerOrderRef"]
}
]
},
"equipment": {
"sources": [
{
"source": {
"space": "sp_asset_maintenance",
"externalId": "FunctionalAsset",
"version": "v1.0.0",
"type": "view"
},
"properties": ["name"]
}
]
}
}
}
{
"with": {
"cycles": {
"nodes": {
"filter": {
"and": [
{
"equals": {
"property": ["node", "space"],
"value": "product_portfolio"
}
},
{
"hasData": [
{
"type": "view",
"space": "product_portfolio",
"externalId": "PortfolioReviewCycle",
"version": "v1"
}
]
}
]
}
},
"limit": 200
}
},
"select": {
"cycles": {
"sources": [
{
"source": {
"type": "view",
"space": "product_portfolio",
"externalId": "PortfolioReviewCycle",
"version": "v1"
}
}
]
}
}
}
{
"with": {
"cycles": {
"nodes": {
"filter": {
"and": [
{
"equals": {
"property": ["node", "space"],
"value": "product_portfolio"
}
},
{
"hasData": [
{
"type": "view",
"space": "product_portfolio",
"externalId": "PortfolioReviewCycle",
"version": "v1"
}
]
}
]
}
},
"limit": 200
}
},
"select": {
"cycles": {
"sources": [
{
"source": {
"type": "view",
"space": "product_portfolio",
"externalId": "PortfolioReviewCycle",
"version": "v1"
},
"properties": ["key", "displayName", "periodStart", "periodEnd", "status"]
}
]
}
}
}
{
"with": {
"initiatives": {
"nodes": {
"filter": {
"and": [
{
"equals": {
"property": ["node", "space"],
"value": "product_portfolio"
}
},
{
"hasData": [
{
"type": "view",
"space": "product_portfolio",
"externalId": "RoadmapInitiative",
"version": "v1"
}
]
}
]
}
},
"limit": 1000
},
"featureLinks": {
"nodes": {
"from": "initiatives",
"through": {
"view": {
"type": "view",
"space": "product_portfolio",
"externalId": "InitiativeFeatureLink",
"version": "v1"
},
"identifier": "initiative"
},
"direction": "inwards"
},
"limit": 1000
},
"commitments": {
"edges": {
"from": "initiatives",
"direction": "outwards",
"filter": {
"equals": {
"property": ["edge", "type"],
"value": {
"space": "product_portfolio",
"externalId": "CustomerInitiativeCommitment"
}
}
}
},
"limit": 1000
}
},
"select": {
"initiatives": {
"sources": [
{
"source": {
"type": "view",
"space": "product_portfolio",
"externalId": "RoadmapInitiative",
"version": "v1"
}
}
]
},
"featureLinks": {
"sources": [
{
"source": {
"type": "view",
"space": "product_portfolio",
"externalId": "InitiativeFeatureLink",
"version": "v1"
},
"properties": ["initiative", "portfolioFeature", "impactStrength"]
}
]
},
"commitments": {
"sources": [
{
"source": {
"type": "view",
"space": "product_portfolio",
"externalId": "CustomerInitiativeCommitment",
"version": "v1"
},
"properties": ["arrAtRisk"]
}
]
}
}
}
{
"with": {
"initiatives": {
"nodes": {
"filter": {
"and": [
{
"equals": {
"property": ["node", "space"],
"value": "product_portfolio"
}
},
{
"hasData": [
{
"type": "view",
"space": "product_portfolio",
"externalId": "RoadmapInitiative",
"version": "v1"
}
]
}
]
}
},
"limit": 1000
},
"featureLinks": {
"nodes": {
"from": "initiatives",
"through": {
"view": {
"type": "view",
"space": "product_portfolio",
"externalId": "InitiativeFeatureLink",
"version": "v1"
},
"identifier": "initiative"
},
"direction": "inwards"
},
"limit": 1000
},
"commitments": {
"edges": {
"from": "initiatives",
"direction": "outwards",
"filter": {
"equals": {
"property": ["edge", "type"],
"value": {
"space": "product_portfolio",
"externalId": "CustomerInitiativeCommitment"
}
}
}
},
"limit": 1000
}
},
"select": {
"initiatives": {
"sources": [
{
"source": {
"type": "view",
"space": "product_portfolio",
"externalId": "RoadmapInitiative",
"version": "v1"
},
"properties": ["title", "status", "horizon", "priority"]
}
]
},
"featureLinks": {
"sources": [
{
"source": {
"type": "view",
"space": "product_portfolio",
"externalId": "InitiativeFeatureLink",
"version": "v1"
},
"properties": ["initiative", "portfolioFeature", "impactStrength"]
}
]
},
"commitments": {
"sources": [
{
"source": {
"type": "view",
"space": "product_portfolio",
"externalId": "CustomerInitiativeCommitment",
"version": "v1"
},
"properties": ["arrAtRisk"]
}
]
}
}
}
{
"directRelationLists": [
{
"space": "ssp_manufacturing",
"externalId": "ProductionSchedule",
"version": "v1.0.0",
"identifier": "equipment"
}
]
}
{
"with": {
"cycles": {
"nodes": {
"filter": {
"equals": {
"property": ["node", "space"],
"value": "product_portfolio"
}
}
},
"limit": 50
}
},
"select": {
"cycles": {
"sources": [
{
"source": {
"type": "view",
"space": "product_portfolio",
"externalId": "PortfolioReviewCycle",
"version": "v1"
},
"properties": ["key"]
}
]
}
}
}
{
"with": {
"cycles": {
"nodes": {
"filter": {
"and": [
{
"equals": {
"property": ["node", "space"],
"value": "product_portfolio"
}
},
{
"hasData": [
{
"type": "view",
"space": "product_portfolio",
"externalId": "PortfolioReviewCycle",
"version": "v1"
}
]
}
]
}
},
"limit": 50
}
},
"select": {
"cycles": {
"sources": [
{
"source": {
"type": "view",
"space": "product_portfolio",
"externalId": "PortfolioReviewCycle",
"version": "v1"
},
"properties": ["key"]
}
]
}
}
}
{
"with": {
"initiatives": {
"nodes": {
"filter": {
"and": [
{
"equals": {
"property": ["node", "space"],
"value": "product_portfolio"
}
},
{
"hasData": [
{
"type": "view",
"space": "product_portfolio",
"externalId": "RoadmapInitiative",
"version": "v1"
}
]
}
]
}
},
"limit": 100
},
"commitments": {
"edges": {
"from": "initiatives",
"direction": "outwards",
"filter": {
"equals": {
"property": ["product_portfolio", "CustomerInitiativeCommitment", "arrAtRisk"],
"value": 0
}
}
},
"limit": 100
}
},
"select": {
"initiatives": {
"sources": [
{
"source": {
"type": "view",
"space": "product_portfolio",
"externalId": "RoadmapInitiative",
"version": "v1"
},
"properties": ["title"]
}
]
},
"commitments": {
"sources": [
{
"source": {
"type": "view",
"space": "product_portfolio",
"externalId": "CustomerInitiativeCommitment",
"version": "v1"
},
"properties": ["arrAtRisk"]
}
]
}
}
}
{
"with": {
"initiatives": {
"nodes": {
"filter": {
"and": [
{
"equals": {
"property": ["node", "space"],
"value": "product_portfolio"
}
},
{
"hasData": [
{
"type": "view",
"space": "product_portfolio",
"externalId": "RoadmapInitiative",
"version": "v1"
}
]
}
]
}
},
"limit": 100
},
"commitments": {
"edges": {
"from": "initiatives",
"direction": "outwards",
"filter": {
"equals": {
"property": ["product_portfolio", "CustomerInitiativeCommitment/v1", "arrAtRisk"],
"value": 0
}
}
},
"limit": 100
}
},
"select": {
"initiatives": {
"sources": [
{
"source": {
"type": "view",
"space": "product_portfolio",
"externalId": "RoadmapInitiative",
"version": "v1"
},
"properties": ["title"]
}
]
},
"commitments": {
"sources": [
{
"source": {
"type": "view",
"space": "product_portfolio",
"externalId": "CustomerInitiativeCommitment",
"version": "v1"
},
"properties": ["arrAtRisk"]
}
]
}
}
}
"use strict";
const fs = require("node:fs");
const path = require("node:path");
function parseArgs(argv) {
const args = {
queries: [],
check: "all",
expect: "pass",
json: false,
schemaHints: null,
};
for (let i = 2; i < argv.length; i += 1) {
const token = argv[i];
if (token === "--query" || token === "-q") {
const value = argv[++i];
if (!value) throw new Error("Missing value for --query");
args.queries.push(value);
} else if (token === "--check") {
const value = argv[++i];
if (!value) throw new Error("Missing value for --check");
args.check = value;
} else if (token === "--expect") {
const value = argv[++i];
if (!value) throw new Error("Missing value for --expect");
args.expect = value;
} else if (token === "--json") {
args.json = true;
} else if (token === "--schema-hints") {
const value = argv[++i];
if (!value) throw new Error("Missing value for --schema-hints");
args.schemaHints = value;
} else if (token === "--help" || token === "-h") {
args.help = true;
} else {
throw new Error(`Unknown argument: ${token}`);
}
}
return args;
}
function usage() {
return [
"Reusable CDF query payload validator",
"",
"Usage:",
" node validate-query-parity.cjs --query <path> [--query <path> ...] [--check all|sources-properties|limit-placement|start-step-hasdata|versioned-traversal-refs|cursor-shape|inwards-list-direct-relations] [--expect pass|fail] [--schema-hints <path>] [--json]",
"",
"Examples:",
" node validate-query-parity.cjs --query ./query.json",
" node validate-query-parity.cjs --query ./query-fail.json --expect fail",
" node validate-query-parity.cjs --query ./query1.json --query ./query2.json --check all",
" node validate-query-parity.cjs --query ./query.json --check inwards-list-direct-relations --schema-hints ./schema-hints.json",
].join("\n");
}
function readJson(filePath) {
const abs = path.resolve(filePath);
let raw = fs.readFileSync(abs, "utf8");
if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1);
try {
return { abs, data: JSON.parse(raw) };
} catch (err) {
const e = new Error(`Invalid JSON in ${abs}: ${err.message}`);
e.code = "INVALID_JSON";
throw e;
}
}
function validateSourcesProperties(query) {
const issues = [];
const select = query?.select;
if (!select || typeof select !== "object") {
return issues;
}
for (const [step, stepSelect] of Object.entries(select)) {
const sources = stepSelect?.sources;
if (!Array.isArray(sources)) continue;
for (let i = 0; i < sources.length; i += 1) {
const src = sources[i];
if (!Array.isArray(src?.properties) || src.properties.length === 0) {
issues.push({
rule: "sources-properties",
path: `select.${step}.sources[${i}].properties`,
message: "properties must not be null",
});
}
}
}
return issues;
}
function validateLimitPlacement(query) {
const issues = [];
const withBlock = query?.with;
if (!withBlock || typeof withBlock !== "object") {
return issues;
}
for (const [step, stepDef] of Object.entries(withBlock)) {
if (stepDef?.nodes && Object.prototype.hasOwnProperty.call(stepDef.nodes, "limit")) {
issues.push({
rule: "limit-placement",
path: `with.${step}.nodes.limit`,
message: "nodes.limit is invalid; use with.<step>.limit",
});
}
if (stepDef?.edges && Object.prototype.hasOwnProperty.call(stepDef.edges, "limit")) {
issues.push({
rule: "limit-placement",
path: `with.${step}.edges.limit`,
message: "edges.limit is invalid; use with.<step>.limit",
});
}
}
return issues;
}
function hasHasDataFilter(node) {
if (!node || typeof node !== "object") return false;
if (Object.prototype.hasOwnProperty.call(node, "hasData")) return true;
if (Array.isArray(node)) return node.some((item) => hasHasDataFilter(item));
return Object.values(node).some((value) => hasHasDataFilter(value));
}
function validateStartStepHasData(query) {
const issues = [];
const withBlock = query?.with;
if (!withBlock || typeof withBlock !== "object") return issues;
for (const [step, stepDef] of Object.entries(withBlock)) {
const nodes = stepDef?.nodes;
if (!nodes || typeof nodes !== "object") continue;
const isStartStep = !Object.prototype.hasOwnProperty.call(nodes, "from");
if (!isStartStep) continue;
const filter = nodes?.filter;
if (!hasHasDataFilter(filter)) {
issues.push({
rule: "start-step-hasdata",
path: `with.${step}.nodes.filter`,
message: "start node step should include hasData constraint",
});
}
}
return issues;
}
function collectPropertyRefs(node, refs = []) {
if (!node || typeof node !== "object") return refs;
if (Array.isArray(node)) {
node.forEach((item) => collectPropertyRefs(item, refs));
return refs;
}
if (Object.prototype.hasOwnProperty.call(node, "property") && Array.isArray(node.property)) {
refs.push(node.property);
}
Object.values(node).forEach((value) => collectPropertyRefs(value, refs));
return refs;
}
function validateVersionedTraversalRefs(query) {
const issues = [];
const withBlock = query?.with;
if (!withBlock || typeof withBlock !== "object") return issues;
for (const [step, stepDef] of Object.entries(withBlock)) {
const nodes = stepDef?.nodes;
const edges = stepDef?.edges;
const traversalDef = nodes ?? edges;
if (!traversalDef || typeof traversalDef !== "object") continue;
const isTraversal = Object.prototype.hasOwnProperty.call(traversalDef, "from");
if (!isTraversal) continue;
const refs = collectPropertyRefs(traversalDef?.filter, []);
for (const ref of refs) {
if (!Array.isArray(ref) || ref.length < 3) continue;
const scope = String(ref[0]);
const viewRef = String(ref[1]);
if (scope === "node" || scope === "edge") continue;
if (!viewRef.includes("/")) {
issues.push({
rule: "versioned-traversal-refs",
path: `with.${step}.${nodes ? "nodes" : "edges"}.filter.property`,
message: "traversal property references should use View/version",
});
}
}
}
return issues;
}
function validateCursorShape(query) {
const issues = [];
const withBlock = query?.with;
const cursors = query?.cursors;
if (Object.prototype.hasOwnProperty.call(query ?? {}, "nextCursor")) {
issues.push({
rule: "cursor-shape",
path: "nextCursor",
message: "nextCursor is a response field and should not be sent in request payloads",
});
}
if (cursors === undefined) return issues;
if (!cursors || typeof cursors !== "object" || Array.isArray(cursors)) {
issues.push({
rule: "cursor-shape",
path: "cursors",
message: "cursors must be an object keyed by step name",
});
return issues;
}
const validSteps = new Set(withBlock && typeof withBlock === "object" ? Object.keys(withBlock) : []);
for (const [step, value] of Object.entries(cursors)) {
if (!validSteps.has(step)) {
issues.push({
rule: "cursor-shape",
path: `cursors.${step}`,
message: "cursor key should match a declared with-step",
});
}
if (typeof value !== "string" || value.trim() === "") {
issues.push({
rule: "cursor-shape",
path: `cursors.${step}`,
message: "cursor value must be a non-empty string",
});
}
}
return issues;
}
function normalizeListRelationHints(schemaHints) {
const list = schemaHints?.directRelationLists;
if (!Array.isArray(list)) return new Set();
const set = new Set();
for (const entry of list) {
const key = [
entry?.space ?? "",
entry?.externalId ?? "",
entry?.version ?? "",
entry?.identifier ?? "",
].join("|");
if (key !== "|||") set.add(key);
}
return set;
}
function validateInwardsListDirectRelations(query, schemaHints) {
const issues = [];
const withBlock = query?.with;
if (!withBlock || typeof withBlock !== "object") return issues;
const hintSet = normalizeListRelationHints(schemaHints);
if (hintSet.size === 0) return issues;
for (const [step, stepDef] of Object.entries(withBlock)) {
const nodes = stepDef?.nodes;
if (!nodes || typeof nodes !== "object") continue;
if (nodes?.direction !== "inwards") continue;
if (!Object.prototype.hasOwnProperty.call(nodes, "from")) continue;
const through = nodes?.through;
const view = through?.view;
const identifier = through?.identifier;
if (!view || typeof view !== "object" || !identifier) continue;
const key = [view.space ?? "", view.externalId ?? "", view.version ?? "", identifier].join("|");
if (hintSet.has(key)) {
issues.push({
rule: "inwards-list-direct-relations",
path: `with.${step}.nodes`,
message:
"Cannot traverse lists of direct relations inwards. Use outwards traversal from the owning node or remodel as an edge.",
});
}
}
return issues;
}
function runChecks(query, checkMode, schemaHints) {
const modes =
checkMode === "all"
? [
"sources-properties",
"limit-placement",
"start-step-hasdata",
"versioned-traversal-refs",
"cursor-shape",
"inwards-list-direct-relations",
]
: [checkMode];
let issues = [];
if (modes.includes("sources-properties")) {
issues = issues.concat(validateSourcesProperties(query));
}
if (modes.includes("limit-placement")) {
issues = issues.concat(validateLimitPlacement(query));
}
if (modes.includes("start-step-hasdata")) {
issues = issues.concat(validateStartStepHasData(query));
}
if (modes.includes("versioned-traversal-refs")) {
issues = issues.concat(validateVersionedTraversalRefs(query));
}
if (modes.includes("cursor-shape")) {
issues = issues.concat(validateCursorShape(query));
}
if (modes.includes("inwards-list-direct-relations")) {
issues = issues.concat(validateInwardsListDirectRelations(query, schemaHints));
}
return issues;
}
function formatFailureLikeApi(issues) {
const first = issues[0];
const tail = first?.message || "constraint violation";
return {
error: {
code: 400,
message: `Request had ${issues.length} constraint violations. Please fix the request and try again. [${tail}]`,
},
issues,
};
}
function main() {
const args = parseArgs(process.argv);
if (args.help || args.queries.length === 0) {
console.log(usage());
process.exit(args.help ? 0 : 1);
}
const validChecks = new Set([
"all",
"sources-properties",
"limit-placement",
"start-step-hasdata",
"versioned-traversal-refs",
"cursor-shape",
"inwards-list-direct-relations",
]);
if (!validChecks.has(args.check)) {
throw new Error(
`Invalid --check '${args.check}'. Use all|sources-properties|limit-placement|start-step-hasdata|versioned-traversal-refs|cursor-shape|inwards-list-direct-relations`
);
}
const validExpect = new Set(["pass", "fail"]);
if (!validExpect.has(args.expect)) {
throw new Error(`Invalid --expect '${args.expect}'. Use pass|fail`);
}
let schemaHints = null;
if (args.schemaHints) {
schemaHints = readJson(args.schemaHints).data;
}
const results = [];
let hasError = false;
for (const queryPath of args.queries) {
const { abs, data } = readJson(queryPath);
const issues = runChecks(data, args.check, schemaHints);
const passed = issues.length === 0;
const expectPass = args.expect === "pass";
const expectationMet = expectPass ? passed : !passed;
const result = {
file: abs,
check: args.check,
expect: args.expect,
passed,
expectationMet,
issues,
};
results.push(result);
if (!expectationMet) {
hasError = true;
}
}
if (args.json) {
const output = {
ok: !hasError,
results,
apiLikeError: hasError ? formatFailureLikeApi(results.flatMap((r) => r.issues)) : null,
};
console.log(JSON.stringify(output, null, 2));
} else {
for (const r of results) {
const status = r.expectationMet ? "PASS" : "FAIL";
console.log(`${status}: ${path.basename(r.file)} (check=${r.check}, expect=${r.expect})`);
if (!r.passed) {
for (const issue of r.issues) {
console.log(` - ${issue.path}: ${issue.message}`);
}
}
}
if (hasError) {
const allIssues = results.flatMap((r) => r.issues);
if (allIssues.length > 0) {
const err = formatFailureLikeApi(allIssues);
console.log(JSON.stringify(err, null, 2));
}
}
}
process.exit(hasError ? 1 : 0);
}
try {
main();
} catch (err) {
console.error(`ERROR: ${err.message}`);
process.exit(1);
}
{
"skill_name": "dm-graph-traversal",
"evals": [
{
"id": 1,
"prompt": "I need initiatives plus related features and commitment edges for one page. Should I use instances.list or instances.query?",
"expected_output": "Select instances.query and explain that relationship-aware retrieval should be graph-native rather than stitched from multiple flat calls.",
"files": [],
"assertions": [
{
"text": "Response explicitly recommends instances.query.",
"type": "content"
},
{
"text": "Response explains this is relationship-aware/graph-context retrieval.",
"type": "content"
},
{
"text": "Response warns against client-side stitching or N+1 list calls.",
"type": "content"
}
]
},
{
"id": 2,
"prompt": "We only need all initiative nodes with a simple status filter and no edge traversal. Which API is better?",
"expected_output": "Select instances.list and explain that flat, non-traversal reads should stay on list.",
"files": [],
"assertions": [
{
"text": "Response explicitly recommends instances.list.",
"type": "content"
},
{
"text": "Response describes this as flat retrieval with no traversal.",
"type": "content"
}
]
},
{
"id": 3,
"prompt": "Query fails with Unexpected field - nodes.limit in with.initiatives. How do I fix it?",
"expected_output": "Move limit to with.initiatives.limit and state nodes.limit is invalid.",
"files": [],
"assertions": [
{
"text": "Response includes step-level limit placement (with.<step>.limit).",
"type": "content"
},
{
"text": "Response states nodes.limit is invalid.",
"type": "content"
}
]
},
{
"id": 4,
"prompt": "I put edges.limit under a traversal step and now query validation fails. What is the right shape?",
"expected_output": "Explain that edges.limit is invalid and limit must be set at the result-step level.",
"files": [],
"assertions": [
{
"text": "Response states edges.limit is invalid.",
"type": "content"
},
{
"text": "Response gives correct pattern with.<step>.limit.",
"type": "content"
}
]
},
{
"id": 5,
"prompt": "instances.query returns properties must not be null. We do have select.sources configured. What is missing?",
"expected_output": "Call out missing select.<step>.sources[*].properties and require explicit property lists.",
"files": [],
"assertions": [
{
"text": "Response references select.<step>.sources[*].properties.",
"type": "content"
},
{
"text": "Response requires explicit properties array.",
"type": "content"
}
]
},
{
"id": 6,
"prompt": "Traversal step returns empty results without error after refactor. Which versioning mistake should I check first?",
"expected_output": "Check traversal property references are versioned (View/version) and not unversioned refs.",
"files": [],
"assertions": [
{
"text": "Response mentions versioned refs using View/version.",
"type": "content"
},
{
"text": "Response ties unversioned refs to silent empty traversal results.",
"type": "content"
}
]
},
{
"id": 7,
"prompt": "How can I prevent unrelated nodes from entering the start step when querying cycles?",
"expected_output": "Recommend start-step filters with node space and hasData constraints to restrict the source set.",
"files": [],
"assertions": [
{
"text": "Response recommends hasData on start step.",
"type": "content"
},
{
"text": "Response includes an additional start-step scope constraint (for example node space).",
"type": "content"
}
]
},
{
"id": 8,
"prompt": "Reverse traversal over an edge returns no rows. Which parts of the edge expression should we verify?",
"expected_output": "Verify direction, through.identifier, and from/source step linkage.",
"files": [],
"assertions": [
{
"text": "Response includes direction verification.",
"type": "content"
},
{
"text": "Response includes through.identifier verification.",
"type": "content"
},
{
"text": "Response includes from/source-step linkage verification.",
"type": "content"
}
]
},
{
"id": 9,
"prompt": "First page is correct but later pages lose feature links. How should cursor pagination be implemented?",
"expected_output": "Use step-specific cursor loops (nextCursor.<step>) and continue until the step cursor is absent.",
"files": [],
"assertions": [
{
"text": "Response mentions step-specific cursor keys such as nextCursor.<step>.",
"type": "content"
},
{
"text": "Response describes looping until cursor exhaustion.",
"type": "content"
}
]
},
{
"id": 10,
"prompt": "What unit test should prevent accidental fallback from query to list in a graph-native helper?",
"expected_output": "Recommend payload-shape assertions on instances.query and an explicit assertion that instances.list was not called.",
"files": [],
"assertions": [
{
"text": "Response includes query payload-shape assertions.",
"type": "content"
},
{
"text": "Response includes negative assertion for instances.list calls.",
"type": "content"
}
]
},
{
"id": 11,
"prompt": "Give me the most important payload-shape assertions for limits in query tests.",
"expected_output": "Assert with.<step>.limit exists and nested nodes.limit/edges.limit are absent.",
"files": [],
"assertions": [
{
"text": "Response asserts with.<step>.limit presence.",
"type": "content"
},
{
"text": "Response asserts nodes.limit absence.",
"type": "content"
},
{
"text": "Response asserts edges.limit absence.",
"type": "content"
}
]
},
{
"id": 12,
"prompt": "What should we assert for select.sources in tests to avoid runtime validation errors?",
"expected_output": "Assert sources exist and each source has explicit properties including expected field names.",
"files": [],
"assertions": [
{
"text": "Response asserts sources are present.",
"type": "content"
},
{
"text": "Response asserts properties are present per source.",
"type": "content"
},
{
"text": "Response suggests checking expected property names.",
"type": "content"
}
]
},
{
"id": 13,
"prompt": "We merge commitment ARR with fallback ARR facts. How should tie-break semantics be defined?",
"expected_output": "Define deterministic merge rules, with explicit commitment ARR preferred over fallback ARR facts.",
"files": [],
"assertions": [
{
"text": "Response defines deterministic merge policy.",
"type": "content"
},
{
"text": "Response states explicit values win over fallback values.",
"type": "content"
},
{
"text": "Response recommends documenting/testing the rule.",
"type": "content"
}
]
},
{
"id": 14,
"prompt": "ARR at risk is inflated because one customer has multiple commitment edges. What merge rule prevents double counting?",
"expected_output": "Deduplicate by business key (customer) so each customer contributes once using a clear tie-break strategy.",
"files": [],
"assertions": [
{
"text": "Response calls for dedupe by customer/business key.",
"type": "content"
},
{
"text": "Response enforces single contribution per dedupe key.",
"type": "content"
},
{
"text": "Response defines tie-break policy for competing values.",
"type": "content"
}
]
},
{
"id": 15,
"prompt": "How can we validate a new TypeScript query design quickly before wiring everything in frontend code?",
"expected_output": "Use Node.js/TypeScript parity checks to validate query payload shape and traversal semantics before wiring app integration.",
"files": [],
"assertions": [
{
"text": "Response recommends a Node.js/TypeScript parity workflow.",
"type": "content"
},
{
"text": "Response references the reusable Node validator or equivalent TypeScript-first validation flow.",
"type": "content"
},
{
"text": "Response validates both payload shape and semantics.",
"type": "content"
},
{
"text": "Response avoids defaulting to Python unless explicitly requested.",
"type": "content"
}
]
},
{
"id": 16,
"prompt": "We currently do one list call per initiative to fetch related links. What is the recommended migration approach?",
"expected_output": "Replace N+1 list calls with one multi-step instances.query and join results by stable identifiers.",
"files": [],
"assertions": [
{
"text": "Response proposes single multi-step instances.query.",
"type": "content"
},
{
"text": "Response explicitly removes/eliminates N+1 list pattern.",
"type": "content"
},
{
"text": "Response describes joining by identifiers/keys.",
"type": "content"
}
]
},
{
"id": 17,
"prompt": "Give me a concise done-checklist before merging a graph traversal helper.",
"expected_output": "Provide checklist with API-choice correctness, payload guardrail compliance, and regression tests.",
"files": [],
"assertions": [
{
"text": "Response includes query-vs-list choice verification.",
"type": "content"
},
{
"text": "Response includes payload guardrails.",
"type": "content"
},
{
"text": "Response includes regression/payload-shape testing.",
"type": "content"
}
]
},
{
"id": 18,
"prompt": "Create a rapid troubleshooting playbook for query errors we keep seeing.",
"expected_output": "Provide a failure-signature table mapping error symptoms to likely causes and concrete fixes.",
"files": [],
"assertions": [
{
"text": "Response provides error/signature oriented troubleshooting.",
"type": "content"
},
{
"text": "Response maps likely causes.",
"type": "content"
},
{
"text": "Response gives concrete corrective actions.",
"type": "content"
}
]
},
{
"id": 19,
"prompt": "This page uses 7 separate instances.list calls and merges results in the UI layer. We can ship it as-is, right?",
"expected_output": "Treat this as a strong signal to evaluate a consolidated instances.query traversal approach, while keeping fallback and pagination explicit.",
"files": [],
"assertions": [
{
"text": "Response identifies many related list calls on one page as a trigger to explore instances.query.",
"type": "content"
},
{
"text": "Response recommends consolidation strategy instead of defaulting to list fan-out.",
"type": "content"
},
{
"text": "Response preserves explicit fallback/pagination guidance.",
"type": "content"
}
]
},
{
"id": 20,
"prompt": "My query fails with 'Cannot traverse lists of direct relations inwards.' when traversing from equipment to production schedules. What should I change?",
"expected_output": "Explain that inwards traversal over list direct relations is invalid and recommend traversing from the owning side with outwards direction or remodeling as an edge relation.",
"files": [],
"assertions": [
{
"text": "Response states the failure is caused by inwards traversal on list direct relations.",
"type": "content"
},
{
"text": "Response recommends traversing from the owning node with outwards direction.",
"type": "content"
},
{
"text": "Response includes edge remodeling as an alternative fix.",
"type": "content"
}
]
},
{
"id": 21,
"prompt": "I need latest usage values per feature, but also need graph traversal to map customer -> PTTS nodes -> feature. What is the recommended query pattern?",
"expected_output": "Use a two-phase pattern: traverse with instances.query to collect timeseries instance IDs, then fetch latest values with batched datapoints.retrieveLatest and merge by stable entity key.",
"files": [],
"assertions": [
{
"text": "Response recommends instances.query for graph traversal and ID collection.",
"type": "content"
},
{
"text": "Response recommends datapoints.retrieveLatest for latest-value retrieval.",
"type": "content"
},
{
"text": "Response includes batching guidance and ignoreUnknownIds handling.",
"type": "content"
},
{
"text": "Response includes deterministic merge-by-entity guidance.",
"type": "content"
}
]
},
{
"id": 22,
"prompt": "User search starts with fuzzy equipment name matching, then needs related work orders and latest metrics. Should we start with query traversal or search?",
"expected_output": "Recommend search-first then traverse: use instances.search to rank anchors, then instances.query for graph hydration, and apply retrieveLatest for last-value metrics.",
"files": [],
"assertions": [
{
"text": "Response recommends instances.search first for fuzzy/discovery stage.",
"type": "content"
},
{
"text": "Response recommends instances.query second for relation traversal.",
"type": "content"
},
{
"text": "Response includes two-phase latest-value retrieval via datapoints.retrieveLatest when needed.",
"type": "content"
}
]
},
{
"id": 23,
"prompt": "Our DMS query endpoint occasionally returns 429 and 5xx during peak usage. What production rule should be enforced?",
"expected_output": "Require bounded transient retry policy with exponential backoff and jitter, while keeping payload/page sizes moderate and preserving deterministic behavior.",
"files": [],
"assertions": [
{
"text": "Response explicitly lists transient retry status classes (for example 408/425/429/5xx).",
"type": "content"
},
{
"text": "Response requires exponential backoff with jitter and bounded retries.",
"type": "content"
},
{
"text": "Response includes payload/page-size moderation guidance.",
"type": "content"
}
]
},
{
"id": 24,
"prompt": "Can we just use properties ['*'] and broad list/query filters first, then narrow results client-side?",
"expected_output": "Reject this as an anti-pattern: prefer explicit server-side filtering and explicit property lists, and only broaden constraints through staged strict-to-broad fallback.",
"files": [],
"assertions": [
{
"text": "Response rejects wildcard projection in production paths.",
"type": "content"
},
{
"text": "Response recommends server-side filtering over client-side filtering.",
"type": "content"
},
{
"text": "Response describes strict-to-broad fallback instead of immediately broad retrieval.",
"type": "content"
}
]
},
{
"id": 25,
"prompt": "We need Initiative -> Commitment edge properties (arrAtRisk, confidence) and then Customer details. How should we structure the query?",
"expected_output": "Recommend an edge-first traversal design: start nodes, traverse to edges as first-class records, project edge properties explicitly, then traverse/join endpoint nodes and apply deterministic dedupe/tie-break aggregation.",
"files": [],
"assertions": [
{
"text": "Response treats edge rows as first-class data (not transport-only).",
"type": "content"
},
{
"text": "Response requires explicit edge property projection via select.<edgeStep>.sources.properties.",
"type": "content"
},
{
"text": "Response includes endpoint join/hydration after edge traversal.",
"type": "content"
},
{
"text": "Response includes deterministic dedupe/tie-break guidance for multi-edge aggregation.",
"type": "content"
}
]
}
]
}
dm-graph-traversal
This folder contains the canonical dm-graph-traversal skill.
Authoritative files
SKILL.md- skill frontmatter and instruction body.evals/evals.json- canonical eval set for skill-creator workflows.code/validate-query-parity.cjs- reusable Node validator for CDFinstances.querypayload checks.
Contributor notes
- Treat
evals/evals.jsonas the single source of truth for eval prompts and expectations. - Keep
SKILL.mdaligned with skill-creator frontmatter requirements. - Keep
references/for deeper supporting material and leaveSKILL.mdfocused on operational guidance. - To validate any payload:
node skills/dm-graph-traversal/code/validate-query-parity.cjs --query <path-to-query.json> --check all --expect pass - To validate an expected failure:
node skills/dm-graph-traversal/code/validate-query-parity.cjs --query <path-to-query.json> --check all --expect fail - For schema-aware relation checks, pass hints:
--schema-hints <path-to-schema-hints.json>. - Available checks:
sources-properties,limit-placement,start-step-hasdata,versioned-traversal-refs,cursor-shape,inwards-list-direct-relations,all. - Example fixtures are in
code/examples/(including fail/pass variants for commonproperties must not be null, missinghasData, non-versioned traversal refs, cursor request-shape issues, and inwards list direct relation traversal errors). - For latest datapoint use cases, follow the skill's two-phase rule:
instances.queryfor traversal/ID collection, then batcheddatapoints.retrieveLatestfor last-value reads. - For concurrency limits, semaphore usage, and write batching policies, pair this skill with
dm-limits-and-best-practices.
Rollout mindset (recommended)
Treat the validator as a normal engineering guardrail, not an optional manual check:
- Add a repository test that runs
code/validate-query-parity.cjsagainst generated query payloads used by app code. - Include that test in the standard
npm test/ Vitest suite so it runs locally and in CI by default. - Keep fixtures close to query-builder tests so shape regressions fail fast during development.
- Prefer failing in tests over discovering query-shape errors at runtime.
Query vs List: Operational Guide
Principle
instances/list is a flat retrieval API. instances/query is a graph traversal API.
If the question is relationship-aware, default to query.
Good Query Triggers
- “For each initiative, load related features and commitments.”
- “For this customer, show connected initiatives with edge attributes.”
- “Walk relation X in reverse to parent entities.”
- “Paginate multiple related result sets reliably.”
Good List Triggers
- “Get all nodes of one type that match static filters.”
- “No edge traversal or join semantics needed.”
Why teams regress to list
- Query payloads look verbose.
- Engineers underestimate stitching complexity.
- Subtle query errors (
limit,properties, refs) can look intimidating.
Avoiding silent regressions
- Add tests that inspect
client.instances.querypayload shape. - Add one negative assertion that
instances.listis not called in query-only paths. - Keep step names explicit and stable for cursor loops and test readability.
Common anti-patterns
1. Query intent implemented as N+1 list calls. 2. Step limit nested under nodes or edges. 3. Select sources without explicit properties. 4. Non-versioned traversal property refs. 5. Aggregation without dedupe/tie-break policy.
Practical migration checklist
- Identify relation path in natural language.
- Encode path as
withsteps. - Add explicit
select.sources.properties. - Add step cursor handling.
- Add payload-shape test assertions.
- Remove list fallback if query path is canonical.