<< All versions

Skill v1.0.3

currentLLM-judged scan100/100
rush-db/rushdb/rushdb-data-modeling

~3 modified

──Details
PublishedSeptember 7, 2026 at 10:26 PM
Content Hashsha256:9331047a033717c5...
Git SHA67214ab66769
Bump Typepatch
Compare with v1.0.2
──Files
Files (1 file, 11.9 KB)
SKILL.md11.9 KBactive
SKILL.md · 279 lines · 11.9 KB

version: "1.0.3" name: rushdb-data-modeling description: Model data in RushDB's property-centric LMPG graph. Use when designing or evolving schemas, choosing labels and properties, defining relationships, importing nested JSON, structuring a new use case, or modeling agent memory with EPISODE, MEMORY_FACT, domain records, scope fields, semantic properties, and fact supersession.


RushDB Data Modeling

RushDB uses the LMPG (Labels, Multi-Properties, Graph) model — a property-centric graph where every node (record) has a label, a set of typed properties, and can be connected to other records via named relationships.

Core insight: you don't define a schema upfront. You push JSON, and RushDB infers structure. Labels and properties emerge from your data. Schema discovery happens via getSchemaMarkdown.


The LMPG Model

ConceptRushDB termDescription
NodeRecordA single data entity — like a row, but with flexible properties
Node typeLabelThe record's category/type (USER, ORDER, PROJECT)
AttributesPropertiesKey-value pairs with typed values attached to a record
EdgeRelationshipA named, directed connection between two records

Records are schema-free: two USER records can have different property sets. Properties carry one of four types: string, number, boolean, datetime. A null value leaves the field unset — query whether a field is set with $exists. Embeddings are number arrays with an embedding index for semantic search.


Labels

Naming Rules

  • Prefer UPPER_CASE as the RushDB convention. Labels are case-sensitive, so User and USER are distinct; copy existing labels exactly.
  • Singular noun: USER not USERS, ORDER not ORDERS
  • No spaces or special characters — use underscores: BLOG_POST

Choosing Labels

Labels should reflect the entity type, not the action or state:

GoodAvoid
USERREGISTERED_USER, ACTIVE_USER
ORDERPLACED_ORDER, ORDER_ENTITY
BLOG_POSTPUBLISHED_CONTENT, POST_NODE

Use a single label per record type. Model state with a status property, not separate labels.

Multi-Label Queries

findRecords with labels: ["ARTICLE", "BLOG_POST"] returns both types (OR). Useful for shared abstract types (e.g., all content records).


Properties

Supported Types

TypeExample valuesNotes
string"hello", "active"Case-sensitive equality; case-insensitive for $contains etc.
number42, 3.14, -100Supports full range operators
booleantrue, false
datetime"2026-04-10T09:00:00Z"ISO 8601 UTC format. Use component objects for range queries.
Writing null (or an all-null array) leaves the field unset — it is not stored and does not appear in the schema. Match set/unset fields with { field: { $exists: true } } / { field: { $exists: false } }.

Naming Conventions

  • camelCase for all property names: createdAt, isActive, userId
  • Consistent types per property across all records of the same label
  • Avoid reusing a property name with different types across records of the same label
  • ISO 8601 for all timestamps: "2026-04-10T09:00:00Z" (always UTC)
  • Booleans as isX or hasX: isActive, hasAccess, isPremium

Property Discovery

getSchemaMarkdown() → all labels + all properties + value ranges
findProperties({ labels: ["USER"] }) → detailed property list for a label
getSchema() → same as above but structured JSON with propertyId values
propertyValues(propertyId) → distinct values / min-max for a property

Relationships

How Auto-Linking Works

When you pass nested objects to bulkCreateRecords, RushDB creates:

  • One record per nested label key
  • One relationship per parent→child nesting level
  • Relationship type defaults to the uppercase label name

Example — this single import:

json
{
"label": "COMPANY",
"data": {
"name": "Acme Corp",
"DEPARTMENT": [
{
"name": "Engineering",
"EMPLOYEE": [
{ "name": "Alice", "role": "Engineer" },
{ "name": "Bob", "role": "Manager" }
]
}
]
}
}

Produces:

  • 1 COMPANY record
  • 1 DEPARTMENT record → linked to COMPANY
  • 2 EMPLOYEE records → each linked to DEPARTMENT
  • Relationships: COMPANY -[HAS_DEPARTMENT]→ DEPARTMENT, DEPARTMENT -[HAS_EMPLOYEE]→ EMPLOYEE

No schema declaration. No migration. Just push the JSON.

Manual Relationship Management

attachRelation({ sourceId, targetId, type: "AUTHORED" })
detachRelation({ sourceId, targetId, type: "AUTHORED" })
findRelationships({ source: { where: { $id: "record-id" } } })
findRelationships({ target: { where: { $id: "record-id" } } })

Relationship Naming Conventions

  • UPPER_SNAKE_CASE: AUTHORED_BY, BELONGS_TO, DEPENDS_ON, HAS_TASK
  • Directional and readable: AUTHOR -[WROTE]→ POST, SESSION -[HAS_DECISION]→ DECISION
  • Verb-based: describes the action/association from source to target

Relationship Query Patterns

Relationships are traversed in where by using the target label as the key:

json
{
"labels": ["COMPANY"],
"where": {
"DEPARTMENT": {
"name": "Engineering",
"$relation": { "type": "HAS_DEPARTMENT", "direction": "out" }
}
}
}

Omit $relation when direction and type are not important — RushDB traverses any edge.

For self-referencing hierarchies (org charts, category trees, dependency graphs), one nested block traverses multiple levels via hops: "$relation": { "type": "REPORTS_TO", "direction": "out", "hops": { "max": 4 } }. Cycles in such graphs are queryable with the $cycle operator — see the query-builder skill for both.


Data Import Patterns

Flat Records (simple entities)

Use createRecord for individual records with no nesting:

json
{
"label": "PRODUCT",
"data": {
"name": "Widget Pro",
"sku": "WGT-PRO-001",
"price": 49.99,
"category": "hardware",
"inStock": true,
"createdAt": "2026-04-10T00:00:00Z"
}
}

Hierarchical Data (nested structures)

Use bulkCreateRecords with nested label keys for structures like org charts, project trees, or content hierarchies. The nesting depth can be arbitrary.

CSV / Flat Files

Use exportRecords to export existing records to CSV. For import, use bulkCreateRecords by transforming flat rows into records with a consistent label.

When source rows have blank cells, pass options: { skipEmptyValues: true } so empty strings ("") and empty arrays ([]) are treated as unset (no property is created) instead of being stored as empty values. 0 and false are real values and are always kept. Defaults to false.

Enum Fields

Define a consistent set of values upfront and document them as a status or type field comment. Enforce consistency via propertyValues queries during writes if needed.


Schema Discovery Workflow

When working with an existing database:

  1. `getSchemaMarkdown()` — see all labels, their property names/types, and relationships
  2. `findProperties({ labels: ["LABEL"] })` — deep-dive a specific label
  3. `propertyValues(propertyId)` — enumerate distinct string/boolean values or get number/date ranges
  4. `findRelationships({ source: { where: { $id: "sample-id" } } })` and `findRelationships({ target: { where: { $id: "sample-id" } } })` — explore outgoing and incoming connections from a sample record

Never guess label names or property names — always discover them first.


Schema Evolution Patterns

Add a property

Just include it in new records. Existing records that lack it are unmatched by filters on that field — they're not broken.

Change a property name

  1. findRecords to get all affected records
  2. updateRecord each with the new property name added and old removed
  3. There is no schema migration — just update the data

Add a label

Start creating records with the new label — it appears in getSchemaMarkdown once at least one record exists.

Retire a label

  1. Add deprecated: true to all existing records of that label
  2. Stop creating new ones
  3. Filter with { deprecated: { $ne: true } } in queries until fully cleaned up
  4. Delete with bulkDeleteRecords (always preview first)

Split a label into two

  1. findRecords on old label, classify each record by new label
  2. bulkCreateRecords into new labels
  3. Re-attach relationships with attachRelation
  4. Mark old records as deprecated

Agent Memory Model

Separate lifecycle projections from domain knowledge:

LabelPurposeSemantic property
EPISODEOne bounded completed turn or lifecycle observationsummary
MEMORY_FACTCurated, versioned fact, preference, or ruletext
SESSIONOptional graph-visible work/conversation boundaryOptional
DECISION, TASK, ENTITY, ARTIFACTExplicit domain knowledgeDomain-specific

Every canonical episode and fact must carry agentId, profileId, privacyScope, participantScopeHash, and sandboxEligible. These properties form an authorization scope and must have consistent types across both labels.

Use deterministic eventId for episodes and factId for facts. Configure managed embedding indexes for EPISODE.summary and MEMORY_FACT.text; do not index raw complete transcripts.

Model fact changes as history:

  1. Create the replacement MEMORY_FACT with a new deterministic factId, active: true, and supersedesFactId referencing the prior fact.
  2. Mark the prior fact active: false.
  3. Recall with active: true; query both versions only for audit/history.

Keep explicit graph labels separate from automatically captured episodes. A native runtime provider owns turn capture; MCP/application logic may add structured decisions or tasks without duplicating the turn.


Common Modeling Mistakes

MistakeCorrect approach
Changing existing label casingCopy the exact case returned by getSchemaMarkdown
Storing state in the label: ACTIVE_USERStore state as status: "active" on USER
One giant label with all fieldsSplit into focused labels linked by relationships
Guessing property namesCall getSchemaMarkdown or findProperties first
Hardcoding enum valuesProbe with propertyValues before filtering
Plain date strings in range queriesUse component objects: { $gte: { $year: 2024 } }
Inventing $label/$direction operatorsUse the label as the key, $alias for naming, $relation for edge type
← v1.0.2All versions