Skill v1.0.3
currentAutomated scan100/100~3 modified
version: "1.0.3" name: rushdb-query-builder description: Build RushDB queries, searches, filters, and aggregations. Use whenever an agent needs to list or filter records, count or group data, traverse relationships, run semantic/vector search, construct a findRecords call, discover live labels and properties, or recall canonical EPISODE and MEMORY_FACT records with mandatory authorization scope prefilters.
RushDB Query Builder
A discovery-first workflow for safely and correctly querying RushDB.
Never guess label names, property names, or field values. Always discover them first.
Prerequisites
- RushDB MCP server must be connected — it provides
getSchemaMarkdown,findRecords, and all other tools used in this skill. Setup:npx @rushdb/mcp-server(requiresRUSHDB_API_KEYenv var). See https://docs.rushdb.com/mcp-server/quickstart - If the MCP tools are not available in the current session, tell the user the MCP server is not configured and link them to the quickstart above.
Mandatory Workflow (always follow this order)
Step 0 — Schema (every session, first call)
Call getSchemaMarkdown before any other tool. It returns:
- All label names (case-sensitive — use them exactly)
- All property names and types per label
- Array fields shown as
type[]; in structuredgetSchema,isArray: truemarks primitive-array properties - Value ranges for numeric/datetime fields
- The full relationship map between labels
- A Semantic Search column per property: non-
—value means the property is indexed and queryable viavectorSearch
Do not call findLabels, findProperties, or findRecords before this.
getSchemaMarkdown()
If the schema looks stale (e.g. new labels or properties were added recently), pass { force: true } to bypass the 1-hour cache and force a fresh recalculation.
Step 1 — Classify Intent
Before building a query, identify what is being asked:
| Intent | Pattern | Tool | |
|---|---|---|---|
| Aggregation | count / total / sum / avg / breakdown / per X / top N by metric / distribution / grouped | findRecords with select + groupBy | |
| Related count ranking | which/what parent has most/more/least/less/fewer/fewest related child records | findRecords with parent labels, child traversal in where, select count alias, groupBy parent field | |
| Listing | show / list / find / search / get | findRecords with where + limit + orderBy | |
| Single record | get by ID, find one unique | getRecord / findOneRecord / findUniqRecord | |
| Relationships | connected to / linked / related | findRelationships | |
| Mutation | create / update / delete | confirm + preview first |
⚠ Aggregation intent: NEVER fetch raw records and count them manually. ALWAYS use select + groupBy on findRecords.
Step 2 — Load Query Spec (for complex queries)
Before calling findRecords with any of these, call getSearchQuerySpec:
- Date/time filters or date ranges
select+groupBy(metrics, aggregations)- Relationship traversal (
wherekeys that are label names) - Vector/semantic similarity
getSearchQuerySpec returns the complete operator reference, select/groupBy syntax, late-ordering rules, and annotated examples. It is the source of truth — do not guess syntax.
Step 3 — Build and Execute
Use only label and property names from the schema. Labels are case-sensitive.
Root-label rule:
labelscontains only the root record type(s) you want returned as rows.- Related record types belong inside
wheretraversal blocks, not beside the root inlabels. - If a related record is needed in
selectorgroupBy, declare$aliason that related label inwhere.
Top-N rule:
- "Top N records by a scalar field on the same label" is a listing: use
orderBy+limit, noselect. - "Which parent has most/more/least/less/fewer/fewest related children" is an aggregation: root the parent label, traverse the child label with
$alias, count the child alias, group by the parent's display property from the schema. - Related-count direction: most/more/highest/largest/greatest =>
desc; least/less/fewer/fewest/lowest/smallest =>asc. - Condition-verb exception: when the ranking condition ("won", "wrote", "approved", …) is expressed only by a scalar reference property on the related label (
author_id/winner_*/approvedBystyle — confirm via discovery) and no relationship type expresses it, root on the label that owns that property andgroupByit instead.wherecannot correlate two records — no$record.*/$alias.*values and no$ref($refis select-only). - Do not let the related/filter label become the root just because it owns the filtered field. If the requested parent-to-related traversal path is absent, do not silently switch roots; state the path is unavailable or use a closest valid fallback with an explicit assumption.
Named-reference rule:
- When filtering a display property with free text the user typed, resolve the property from schema discovery first (often
nameortitle— never assume one exists), then default to{ $contains: "<user text>" }. This applies to filters on any label, including related labels traversed with$alias. - Use exact equality only when the value is an ID, or the user explicitly asks for an exact match (e.g. "named exactly").
- If you need to confirm a canonical value before filtering, use discovery (list/search a few records) rather than guessing an exact string.
Example:
findRecords({labels: ['DEPARTMENT'],where: { PROJECT: { $alias: '$project' } },select: {department: '$record.name',projects: { $count: '$project' }},groupBy: ['$record.name'],orderBy: { projects: 'desc' }})
Tool Reference
Discovery
| Tool | When to use | |
|---|---|---|
getSchemaMarkdown | Step 0 — always first, once per session | |
getSchema | Same as above but structured JSON; use when you need propertyId values or vectorIndexes per property | |
findLabels | Skip if getSchemaMarkdown already ran | |
findProperties | Discover field names + types for a specific label when not in schema | |
findRelationships | Inspect relationships; where filters edge type/properties, source/target filter endpoint records; no aggregate/groupBy | |
propertyValues | List distinct values for a property (needs propertyId from getSchema or findProperties) |
Semantic search: Properties listed ingetSchemawith a non-emptyvectorIndexesarray are eligible. UsevectorSearchwith the matchingpropertyNameandlabels;semanticSearchis only a deprecated compatibility alias.
Querying
| Tool | When to use | |
|---|---|---|
findRecords | Primary query/read/list/search tool — listing, filtering, aggregation, groupBy, semantic search | |
findOneRecord | Return the first matching record | |
findUniqRecord | Return exactly one record (errors if multiple match) | |
getRecord | Fetch a single record by ID | |
getRecordsByIds | Fetch multiple records by IDs | |
vectorSearch | Direct semantic retrieval over one indexed property; where activates exact prefilter mode | |
exportRecords | Use only when the user explicitly asks for CSV export/download; do not substitute it for findRecords |
Mutations (confirm before use)
| Tool | Notes | |
|---|---|---|
createRecord | Store a new record | |
updateRecord | Patch fields on an existing record | |
setRecord | Replace all fields on an existing record | |
deleteRecord / deleteRecordById | Destructive — preview first | |
bulkCreateRecords | Batch insert via nested JSON | |
bulkDeleteRecords | Destructive batch — always preview with findRecords first | |
attachRelation | Create a relationship between two records | |
detachRelation | Remove a relationship |
Resource-Local where Rule
Do not reuse where blindly across resource types:
| Method | Notes | |
|---|---|---|
findRecords | where applies to Records. Full query: where + select + groupBy + pagination | |
findRelationships | where applies to relationship edges (type + user-defined edge properties). Use source/target for endpoint Records; no select/groupBy | |
findLabels | where applies to Records before label counts are returned | |
findProperties | where applies to Records before property metadata is returned | |
exportRecords | where + labels apply to Records; streams CSV only when explicitly requested | |
bulkDeleteRecords | where + labels apply to Records; destructive — always preview first |
The same Record filter used in findRecords can be passed directly to bulkDeleteRecords, exportRecords, findLabels, or findProperties. For relationship lookup, move Record predicates into source.where or target.where.
Canonical Agent-Memory Recall
Treat canonical memory scope as authorization, not ranking. For EPISODE and MEMORY_FACT, obtain these exact values from trusted host/application metadata:
agentIdprofileIdprivacyScopeparticipantScopeHashsandboxEligible
Pass all five in vectorSearch.where. Never infer them from query text or recalled records, and never remove predicates to increase result count.
Search the two semantic properties separately:
{"labels": ["EPISODE"],"propertyName": "summary","query": "authentication decision","where": {"agentId": "<trusted-agent-id>","profileId": "<trusted-profile-id>","privacyScope": "private","participantScopeHash": "<trusted-hash>","sandboxEligible": false,"externalSessionId": { "$ne": "<current-session-id>" }},"limit": 8}
For facts, switch to labels: ["MEMORY_FACT"], propertyName: "text", and add active: true. Merge results by deterministic eventId/factId in application code. If trusted scope is unavailable, use the runtime's native memory recall rather than issuing a broader query.
Quick Operator Cheat Sheet
// ── Equality ──────────────────────────────────────────────────────────name: "Alice"isActive: trueage: 30// ── String (all comparisons are case-insensitive) ─────────────────────name: { $contains: "ali" }name: { $startsWith: "A" }name: { $endsWith: "e" }name: { $ne: "deleted" }status: { $in: ["active", "pending"] } // matches any value in liststatus: { $nin: ["archived", "deleted"] } // matches none of these values// ── Number ────────────────────────────────────────────────────────────score: { $gt: 90 }score: { $gte: 70, $lte: 100 } // between (inclusive)score: { $in: [85, 90, 95] }score: { $nin: [0, -1] }// ── Datetime ──────────────────────────────────────────────────────────// ISO 8601 strings work for equality, $in, and range operators:created: "2024-01-01T00:00:00Z"created: { $gte: "2024-04-23T00:00:00Z" } // "after a timestamp" / relative rangescreated: { $in: ["2024-01-01T00:00:00Z", "2024-06-01T00:00:00Z"] }// Use component objects for calendar-semantic ranges (year / month / day boundaries):created: { $gte: { $year: 2024, $month: 1 }, $lt: { $year: 2024, $month: 2 } } // January 2024created: { $gte: { $year: 1990 }, $lt: { $year: 2000 } } // the 1990s// Available components: $year $month $day $hour $minute $second $millisecond $microsecond $nanosecond// ── Existence / type ──────────────────────────────────────────────────email: { $exists: true } // field is present and not nullphone: { $exists: false } // field absentage: { $type: "number" } // "string"|"number"|"boolean"|"datetime"// ── Logical ───────────────────────────────────────────────────────────// Implicit AND — multiple keys at the same level:where: { status: "active", score: { $gte: 70 } }// Explicit operators:$and: [ { status: "active" }, { score: { $gte: 70 } } ]$or: [ { plan: "pro" }, { plan: "enterprise" } ]$not: { status: "deleted" }$nor: [ { status: "deleted" }, { status: "archived" } ] // none must match$xor: [ { isPremium: true }, { hasTrial: true } ] // exactly one must match// ── Relationship traversal — key IS the label, exactly as spelled in the schema ──where: {DEPARTMENT: {name: "Engineering",headcount: { $gte: 10 }}}// Named traversal ($alias for use in select/groupBy):where: { DEPARTMENT: { $alias: "$dept", budget: { $gte: 50000 } } }// Constrain relationship type or direction:where: { POST: { $relation: { type: "AUTHORED", direction: "in" } } }// Multihop over one pattern (hierarchies, "within N degrees") — hops inside $relation:where: { EMPLOYEE: { $relation: { type: "REPORTS_TO", direction: "out", hops: { max: 4 } } } }// Cycle/ring detection (fraud rings, circular ownership) — the $cycle operator's value IS the traversal spec:where: { $cycle: { type: "TRANSFERRED_TO", direction: "out", hops: { min: 2, max: 6 } } }
Each Relationships row in the schema is a directed pattern rooted at that label: (SELF)-[:TYPE]->(OTHER) is outgoing, (SELF)<-[:TYPE]-(OTHER) is incoming. To pin the edge, set $relation: { type: "TYPE", direction } — "out" for ->, "in" for <-. Only patterns shown in the schema are traversable; a scalar *_id property is a plain value, not an edge — never nest a label to "join" on it (root on the owning label and groupBy it instead).
Common mistakes to avoid:
$label,$direction,$as,$of,$through,$hops— these do not exist (multihop depth is$relation.hops;$relation.hopsand$cycleARE valid)- Inventing a semantic
$relation.typewhen the schema shows only__RUSHDB__RELATION__DEFAULT__edges — WRONG: copy the type verbatim from a schema Relationships row, or omittype(untyped traversal is valid) - Changing a label's case (e.g. uppercasing
departmentstoDEPARTMENTS) — WRONG: labels are case-sensitive; copy them exactly as the schema spells them { employee: { $label: "EMPLOYEE" } }— WRONG: key must be the label name{ EMPLOYEE: { $alias: "$emp" } }— CORRECTlabels: ["PARENT", "CHILD"]for a parent-child metric — WRONG: keeplabels: ["PARENT"], putCHILDinwherelabels: ["CHILD"]for "which parent has most/fewest children" — WRONG when a parent-child path exists: keep the requested parent as root and count the child aliasgroupBy: ["$record"]or["$child"]— WRONG: dimensionalgroupBymust include a property, e.g."$record.name"; self-group uses select key names only{ fieldA: "$record.id" }or{ fieldA: { $eq: "$alias.id" } }— WRONG:wherevalues must be literals.$record.*/$alias.*references work only inselect/groupBy/aggregate; as awherevalue they are matched as a literal string and return nothing (RushDB has no correlated where predicate)- Simulating a join on a scalar field that is not a relationship in the schema — WRONG: root on the label that owns the field and
groupByit, e.g.{ labels: ["LABEL"], select: { count: { $count: "*" } }, groupBy: ["$record.someScalarField"] } name: "partial user text"for an incomplete named reference — RISKY: prefername: { $contains: "partial user text" }- For calendar-semantic ranges (year / month / day boundaries) use component objects, not ISO strings
- Do NOT include
limitwhen usingselect(produces mathematically wrong results)
Key Limits
limitmax = 1000- Never use `limit` with `select` — it restricts the scan and gives wrong totals
- For
"how many"simple counts: readtotalfrom thefindRecordsresponse — do NOT usefn:'count' - For groupBy self-group and dimensional groupBy: omit
limitunless you want "top N"
When to Load the Full Reference
Call getSearchQuerySpec (or read references/search-query-spec.md) for:
- Datetime filters — ISO 8601 strings work with all operators; use component objects for calendar-semantic ranges (year / month / day)
- Select expressions —
$count,$sum,$avg,$min,$max,$collect,$timeBucket, math ops,$ref - groupBy modes — dimensional (
$alias.property) vs self-group (select key names) - Late-ordering rule — critical for correct totals in self-group queries
- Relationship traversal —
$alias,$relation, multi-hop BFS algorithm - Vector similarity inside `findRecords` — use legacy
aggregate(notselect); prefervectorSearchfor direct semantic retrieval - Nested collect — building tree-shaped results with label-based
$collect - Validation checklist — before submitting any
findRecordscall