Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
objectstack-ai avatar

Objectstack Query

  • 133 installs
  • 18 repo stars
  • Updated August 5, 2026
  • objectstack-ai/framework

Helps with ai & agent building tasks.

About

objectstack-query is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.

  • objectstack-query
  • AI & Agent Building
  • AI-coding skill

Objectstack Query by the numbers

  • 133 all-time installs (skills.sh)
  • +4 installs in the week ending Aug 4, 2026 (Skillselion tracking)
  • Ranked #3,627 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
  • Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/objectstack-ai/framework --skill objectstack-query

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs133
repo stars18
Last updatedAugust 5, 2026
Repositoryobjectstack-ai/framework

What it does

Helps with ai & agent building tasks.

Files

SKILL.mdMarkdownGitHub ↗

Query Design — ObjectStack Query DSL

Expert instructions for constructing data queries using the ObjectStack Query DSL. This skill covers filter expressions, sorting, pagination, aggregation, joins, window functions, full-text search, and the expand system for related records.

---

Skill Boundaries

NeedUse instead
Define objects, fields, or relationshipsobjectstack-data
Define REST API endpoints or authobjectstack-api
Build views, dashboards, or appsobjectstack-ui
Create a plugin or register servicesobjectstack-platform

---

When to Use This Skill

  • You are constructing a filter expression for record retrieval
  • You need to sort or paginate query results
  • You are writing aggregation queries (count, sum, avg, group by)
  • You need to expand related records through lookups
  • You are implementing full-text search across fields
  • You need window functions for analytical queries
  • You are choosing between offset vs cursor pagination

---

Core Concepts

Query Structure (QueryAST)

Every ObjectStack query follows the QuerySchema structure:

{
  object: 'account',           // Target object (required)
  fields: ['name', 'email'],   // SELECT — fields to retrieve
  where: { status: 'active' }, // WHERE — filter conditions
  orderBy: [{ field: 'created_at', order: 'desc' }],  // ORDER BY
  limit: 20,                   // LIMIT — max records
  offset: 0,                   // OFFSET — skip records
}

Key rule: object is the only required property. Everything else is optional.

---

Quick Reference — Detailed Rules

For comprehensive documentation with incorrect/correct examples:

  • [Filters](./rules/filters.md) — All operators, logical combinations, nested relations
  • [Aggregation](./rules/aggregation.md) — GroupBy, aggregation functions, HAVING, window functions
  • [Pagination](./rules/pagination.md) — Offset vs cursor, best practices, performance

---

Filter Operators

ObjectStack uses a declarative, database-agnostic filter DSL inspired by Prisma, Strapi, and MongoDB.

Implicit Equality (Shorthand)

The simplest filter — field equals value:

{ where: { status: 'active' } }
// SQL: WHERE status = 'active'

Comparison Operators

OperatorPurposeSQL EquivalentTypes
$eqEqual=Any
$neNot equal<>Any
$gtGreater than>Number, Date
$gteGreater than or equal>=Number, Date
$ltLess than<Number, Date
$lteLess than or equal<=Number, Date
{ where: { age: { $gte: 18 } } }
// SQL: WHERE age >= 18

{ where: { created_at: { $gt: '2025-01-01' } } }
// SQL: WHERE created_at > '2025-01-01'

Set & Range Operators

OperatorPurposeSQL Equivalent
$inIn listIN (...)
$ninNot in listNOT IN (...)
$betweenInclusive rangeBETWEEN ? AND ?
{ where: { status: { $in: ['active', 'pending'] } } }
// SQL: WHERE status IN ('active', 'pending')

{ where: { amount: { $between: [100, 500] } } }
// SQL: WHERE amount BETWEEN 100 AND 500

String Operators

OperatorPurposeSQL Equivalent
$containsContains substringLIKE '%?%'
$notContainsDoes not containNOT LIKE '%?%'
$startsWithStarts with prefixLIKE '?%'
$endsWithEnds with suffixLIKE '%?'
{ where: { email: { $contains: '@company.com' } } }
// SQL: WHERE email LIKE '%@company.com%'

Null & Existence Operators

OperatorPurposeSQL / NoSQL
$nullIs null checkIS NULL / IS NOT NULL
$existsField exists (NoSQL)MongoDB $exists
{ where: { deleted_at: { $null: true } } }
// SQL: WHERE deleted_at IS NULL

Logical Operators

Combine conditions with $and, $or, and $not:

// OR: active accounts OR accounts with high revenue
{
  where: {
    $or: [
      { status: 'active' },
      { revenue: { $gt: 1000000 } }
    ]
  }
}

// AND + OR combined
{
  where: {
    $and: [
      { type: 'enterprise' },
      { $or: [
        { region: 'us' },
        { region: 'eu' }
      ]}
    ]
  }
}

// NOT: exclude closed accounts
{
  where: {
    $not: { status: 'closed' }
  }
}

Nested Relation Filters

Filter through relationships without an explicit join:

// Filter accounts where the related contact has a verified profile
{
  object: 'account',
  where: {
    contact: {                    // Relation field name
      profile: {                  // Nested relation
        verified: true
      }
    }
  }
}

Field References (Cross-Field Comparisons)

Compare two fields using $field:

// Where actual_revenue > estimated_revenue
{
  where: {
    actual_revenue: { $gt: { $field: 'estimated_revenue' } }
  }
}

---

Sorting

Sort with orderBy — an array of sort nodes:

{
  object: 'account',
  orderBy: [
    { field: 'priority', order: 'desc' },
    { field: 'name', order: 'asc' },      // Secondary sort
  ]
}

Rules:

  • Order of array elements defines sort priority
  • Default order is 'asc' — you can omit it for ascending sorts
  • Sort fields should be indexed for performance (see objectstack-data indexing rules)

---

Pagination

Offset Pagination (Simple)

{
  object: 'account',
  limit: 20,
  offset: 40,   // Skip first 40 records (page 3)
}

When to use: UI pages, small datasets (<100K records), when you need "jump to page N".

Pitfall: Offset pagination degrades on large offsets — the database still scans skipped rows.

Cursor Pagination (Performant)

{
  object: 'account',
  limit: 20,
  cursor: { id: 'last-seen-id' },
  orderBy: [{ field: 'id', order: 'asc' }],
}

When to use: Infinite scroll, APIs, large datasets, real-time feeds.

Rule: The cursor fields must match orderBy fields. The engine uses them to generate WHERE id > ? instead of OFFSET.

OData Compatibility

top is an alias for limit (for OData-style APIs):

{ object: 'account', top: 50 }
// Equivalent to: { object: 'account', limit: 50 }

---

Aggregation

Basic Aggregation Functions

FunctionPurposeSQL
countCount rowsCOUNT(*) or COUNT(field)
sumSum valuesSUM(field)
avgAverageAVG(field)
minMinimumMIN(field)
maxMaximumMAX(field)
count_distinctUnique countCOUNT(DISTINCT field)
array_aggCollect into arrayARRAY_AGG(field)
string_aggConcatenate stringsSTRING_AGG(field, ',')

GroupBy + Aggregation

// Total revenue per region
{
  object: 'deal',
  fields: ['region'],
  aggregations: [
    { function: 'sum', field: 'amount', alias: 'total_revenue' },
    { function: 'count', alias: 'deal_count' },
  ],
  groupBy: ['region'],
  orderBy: [{ field: 'total_revenue', order: 'desc' }],
}
// SQL: SELECT region, SUM(amount) AS total_revenue, COUNT(*) AS deal_count
//      FROM deal GROUP BY region ORDER BY total_revenue DESC

HAVING Clause

Filter groups after aggregation:

{
  object: 'deal',
  fields: ['region'],
  aggregations: [
    { function: 'sum', field: 'amount', alias: 'total_revenue' },
  ],
  groupBy: ['region'],
  having: { total_revenue: { $gt: 100000 } },
}
// SQL: ... HAVING SUM(amount) > 100000

Filtered Aggregation

Apply a filter to a specific aggregation only:

{
  object: 'order',
  aggregations: [
    { function: 'count', alias: 'total_orders' },
    { function: 'count', alias: 'high_value_orders',
      filter: { amount: { $gt: 1000 } } },
  ],
}
// SQL: COUNT(*) AS total_orders,
//      COUNT(*) FILTER (WHERE amount > 1000) AS high_value_orders

---

Expand (Related Records)

Load related records through lookup/master_detail fields:

{
  object: 'task',
  fields: ['title', 'status'],
  expand: {
    assignee: {
      object: 'user',
      fields: ['name', 'email'],
    },
    project: {
      object: 'project',
      fields: ['name'],
      expand: {
        org: { object: 'org', fields: ['name'] }  // Nested expand
      }
    }
  }
}

Rules:

  • Max expand depth is 3 by default
  • The engine resolves expands via batch $in queries (not N+1)
  • Keys in expand must be lookup or master_detail field names
  • Each expand value is a full QueryAST — you can filter, sort, and paginate within it

---

Joins (Advanced)

For cross-object queries beyond what expand provides:

{
  object: 'order',
  fields: ['id', 'amount'],
  joins: [
    {
      type: 'inner',       // 'inner' | 'left' | 'right' | 'full'
      object: 'customer',
      alias: 'c',
      on: { 'order.customer_id': { $eq: { $field: 'c.id' } } },
    }
  ],
}

Join Strategy Hints

StrategyWhen to use
autoDefault — engine decides
databaseBoth objects on same datasource
hashCross-datasource, moderate data
loopSmall right-side lookup table

---

Full-Text Search

{
  object: 'article',
  search: {
    query: 'machine learning',
    fields: ['title', 'content'],
    fuzzy: true,
    boost: { title: 2.0 },
    highlight: true,
  },
  limit: 10,
}

Options:

  • fuzzy: true — tolerates typos
  • boost — field-specific relevance weighting
  • operator: 'and' | 'or' — match all terms or any term
  • minScore — minimum relevance threshold
  • language — text analysis language

---

Window Functions (Analytics)

Window functions compute values across row sets without collapsing results:

// Rank products by sales within each category
{
  object: 'product',
  fields: ['name', 'category', 'sales'],
  windowFunctions: [
    {
      function: 'row_number',
      alias: 'category_rank',
      over: {
        partitionBy: ['category'],
        orderBy: [{ field: 'sales', order: 'desc' }],
      }
    }
  ],
}

Available Window Functions

FunctionPurpose
row_numberSequential number within partition
rankRank with gaps for ties
dense_rankRank without gaps
lag / leadAccess previous/next row value
first_value / last_valueFirst/last value in window
sum / avg / count / min / maxRunning aggregates

---

Common Patterns

Expand vs Join: Which to Use?

ScenarioUse
Load lookup fields for displayexpand
Filter parent by child conditionsNested relation filter
Cross-datasource joinsjoins with strategy: 'hash'
Analytical queries across tablesjoins
Simple parent→child navigationexpand

Pagination Pattern for APIs

// Page-based API response
{
  object: 'account',
  where: { status: 'active' },
  fields: ['id', 'name', 'email'],
  orderBy: [{ field: 'name', order: 'asc' }],
  limit: 20,
  offset: (page - 1) * 20,
}

Dashboard Aggregation Pattern

// KPI dashboard: multiple aggregations on same object
{
  object: 'deal',
  aggregations: [
    { function: 'count', alias: 'total_deals' },
    { function: 'sum', field: 'amount', alias: 'pipeline_value' },
    { function: 'avg', field: 'amount', alias: 'avg_deal_size' },
    { function: 'count', alias: 'won_deals',
      filter: { stage: 'closed_won' } },
  ],
}

---

CRM Analytics Query Blueprint

Use dashboards/reports metadata as the practical query pattern source:

Query NeedCRM ReferencePattern
KPI widgetsdashboards/sales.dashboard.tsFiltered aggregates (sum, count, avg) over opportunity. Add `compareTo: 'previousPeriod' \
Time-series chartdashboards/sales.dashboard.tsDate filters + `categoryGranularity: 'day' \
Matrix reportreports/opportunity.report.tsgroupingsDown + groupingsAcross + dateGranularity: 'quarter'
Funnel summaryreports/opportunity.report.tsMulti-level grouping (owner -> stage) + aggregated measures
Operational filterdashboard/report filtersPrefer declarative operators ($ne, $nin, $gte) over hardcoded SQL

For metadata app development, model analytics in report/dashboard metadata first; only fall back to custom query code when schema limits require it.

---

Verify your work

Most queries run at runtime (smoke-test them with os data query or a vitest test), but query metadata — list-view filter specs and report/dashboard datasets — is validated statically. After editing those, run:

os validate     # schema + CEL predicates + widget/dataset bindings (no artifact)
# or: os build  # the same gates, plus emits dist/

A dashboard widget whose dataset / dimensions / values don't resolve fails here instead of rendering an empty chart (ADR-0021). In a scaffolded project the gate is npm run validate. See objectstack-platform → Verify your work.

---

References

See references/_index.md for the full list of Zod schemas (with one-line descriptions) — pointers into node_modules/@objectstack/spec/src/. Always Read the source for exact field shapes; do not rely on memory of property names.

Related skills

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.