ObjectStackObjectStack

Data API

REST endpoints for CRUD, batch operations, record cloning, and analytics queries.

Data API

Record CRUD, batch operations, and analytics queries over REST. All paths are relative to the base URL (defaults to /api/v1) — see the API Overview for discovery and service availability.

Data Operations

CRUD operations on any object. Always available — provided by the kernel.

GET /data/:object

Query records with filtering, sorting, selection, and pagination.

ParameterLocationDescription
objectpathObject name
selectqueryComma-separated field names
filterqueryFilter expression (JSON). filters also accepted for backward compatibility.
sortquerySort expression (e.g. name asc or -created_at)
topqueryMax records to return. No default — omitting it returns all matching records.
skipqueryOffset
expandqueryComma-separated list of relations to eager-load
searchqueryFull-text search query

Note: OData-style $-prefixed parameters ($filter, $select, $orderby, $top, $skip, $expand, $count, $search) are also accepted directly on this same endpoint as aliases — they're normalized internally to the parameter names above. There is no separate standalone OData endpoint.

Response:

{
  "object": "account",
  "records": [{ "id": "1", "name": "Acme Corp", ... }],
  "total": 42,
  "hasMore": true
}

GET /data/:object/:id

Get a single record by ID. Only select and expand query parameters are allowed; all other parameters are discarded.

ParameterLocationDescription
objectpathObject name
idpathRecord ID
selectqueryComma-separated field names to include
expandqueryComma-separated list of relations to eager-load

Response: { object: "account", id: "1", record: { ... } }

POST /data/:object

Create a new record.

Body: { name: "Acme Corp", industry: "Technology" }
Response: { object: "account", id: "1", record: { ... } }

PATCH /data/:object/:id

Update an existing record (partial update).

Body: { industry: "Healthcare" }
Response: { object: "account", id: "1", record: { ... } }

DELETE /data/:object/:id

Delete a record.

Response: { object: "account", id: "1", success: true }


Batch Operations

Efficient bulk operations. Always available.

POST /data/:object/batch

Execute a batch operation (create / update / upsert / delete) on multiple records.

Body:

{
  "operation": "update",
  "records": [
    { "id": "1", "data": { "status": "active" } },
    { "id": "2", "data": { "status": "active" } }
  ],
  "options": {
    "atomic": true,
    "returnRecords": true,
    "continueOnError": false,
    "validateOnly": false
  }
}

Response: BatchUpdateResponse with succeeded, failed, total, and a per-record results array. Each entry in results has id, success, an optional errors array, and optional data (the full record, present when returnRecords is true).

POST /data/:object/createMany

Batch create multiple records.

Body: a bare array of records — [{ name: "A" }, { name: "B" }]. The REST handler reads the request body directly as the records array, so do not wrap it in { records: [...] }.
Response: { object: "account", records: [...], count: 2 }

POST /data/:object/updateMany

Batch update multiple records.

Body:

{
  "records": [
    { "id": "1", "data": { "status": "active" } },
    { "id": "2", "data": { "status": "closed" } }
  ],
  "options": { "atomic": false }
}

POST /data/:object/deleteMany

Batch delete records by ID list.

Body: { ids: ["1", "2", "3"] }


POST /data/:object/:id/clone

Clone a record. Reads the source, drops engine-owned columns (id, the audit fields, autonumbers, and computed formula/summary values) so they are re-derived, applies any caller overrides, and inserts the copy. Shallow by design — it duplicates the record's own fields, not its child records.

Gated by the object's enable.clone capability (default true); an object with enable.clone: false returns 403 CLONE_DISABLED.

Body (optional): { "overrides": { "name": "Acme (Copy)" } } — applied on top of the copied values (a bare field map is also accepted). The natural place to set a new name or clear a unique field.

Response 201: { object, id, sourceId, record }


Analytics

Semantic BI queries using a cube-style API. Available when the analytics service is enabled.

POST /analytics/query

Execute an analytics query.

Body:

{
  "cube": "account",
  "measures": ["revenue.sum", "count"],
  "dimensions": ["industry"],
  "where": { "status": "active" },
  "limit": 100
}

Filtering uses the canonical Query DSL where object (the same MongoDB-style FilterCondition accepted by find()), not a filters array.

Response: the runtime dispatcher wraps the AnalyticsResult as { success: true, data: { rows, fields, sql?, totals? } }:

{
  "success": true,
  "data": {
    "rows": [
      { "industry": "Technology", "revenue.sum": 150000, "count": 5 },
      { "industry": "Healthcare", "revenue.sum": 80000, "count": 3 }
    ],
    "fields": [
      { "name": "industry", "type": "string", "label": "Industry" },
      { "name": "revenue.sum", "type": "number", "label": "Revenue Sum", "format": "$0,0" },
      { "name": "count", "type": "number", "label": "Count" }
    ],
    "sql": "SELECT ..."
  }
}

GET /analytics/meta

Get metadata for all registered cubes. Cubes are explicitly defined (via defineCube or the analytics service's cubes config) — a cube referenced by a query that isn't yet registered is lazily auto-inferred from that query's shape, but metadata isn't proactively generated for every object.

Response: Array of cube definitions with measures and dimensions (time-based dimensions are dimensions entries with type: "time").

POST /analytics/sql

Generate the SQL for a given analytics query without executing it (dry-run/debug). Accepts the same body shape as /analytics/query; support depends on the underlying driver/strategy.

Response: { success: true, data: { sql: string, params: unknown[] } }


See also

On this page