ObjectStackObjectStack

services.data

CRUD runtime helper API for records (`query`, `get`, `find`, `create`, `update`, `delete`).

services.data

  • Stability: stable
  • Canonical source: packages/client/src/index.ts — the ObjectStackClient.data surface (see Canonical source for why this page names the SDK rather than a contracts/*-service.ts interface)

Who holds this binding — a hook does not

This page documents the services.data contract surface: the signatures, not a binding every runtime surface receives (see the binding note). A data hook never gets one. The engine builds a hook context key by key — object / event / input / session / provenance / user / api / transaction / ql — and sets no services key at any of its construction sites, so services.data.get(…) written beside a ctx.input.… read throws on services at the first call rather than reading anything (#5720).

A hook's own cross-object channel is ctx.api: see Examples §2 for a ctx.api.object('crm_account').findOne(…) read inside a real beforeInsert / beforeUpdate handler. The Example below is written the other way round — as the code that holds the binding calls it, with plain arguments and no ctx in sight.

Methods

services.data.query<T = any>(object: string, query: Partial<QueryAST>): Promise<PaginatedResult<T>>
services.data.find<T = any>(object: string, options?: QueryOptions | QueryOptionsV2): Promise<PaginatedResult<T>>
services.data.get<T = any>(object: string, id: string): Promise<GetDataResult<T>>
services.data.create<T = any>(object: string, data: Partial<T>): Promise<CreateDataResult<T>>
services.data.update<T = any>(object: string, id: string, data: Partial<T>): Promise<UpdateDataResult<T>>
services.data.delete(object: string, id: string): Promise<DeleteDataResult>

Two list entries, one preference

query and find both answer a list read with the same { records, total?, hasMore? } envelope, and the Canonical source declares which of the two to prefer — the block above lists them in its declaration order. data.query is "Advanced Query using ObjectStack Query Protocol"; data.find carries an @deprecated tag in the same file: "Use data.query() with standard QueryAST parameters instead. This method uses legacy parameter names." The tag ships in the package's published type declarations, so an editor strikes find through at every call site and points at query. As with the options vocabulary below, the posture is the SDK's own, not this page's — it is recorded product direction (#986: deprecate the legacy-parameter query entries, promote data.query(AST)).

Deprecated means "prefer query", not "scheduled for removal in this version": find remains fully functional, keeps both of its option vocabularies (see below), and the Canonical source still describes QueryOptionsV2 as "the vocabulary data.find() still accepts" for callers that stay on it. The capability line between the two entries is real, though. find rides GET query parameters, so it has no spelling for a search term or for nested expand detail — it refuses a nested expand with an error whose own text says to use data.query(). query POSTs the full QueryAST as a JSON body (POST /data/:object/query) and carries all of it.

Canonical source

Every sibling page in this chapter names a contract interface (packages/spec/src/contracts/sharing-service.ts, queue-service.ts, …). This one names the client SDK instead, and the difference is real rather than an oversight: the spec declares no IDataService. Its nearest neighbour, IDataEngine (packages/spec/src/contracts/data-engine.ts), is a lower surface with a different shape — find(objectName, query, options): Promise<any[]> straight at the engine — not the object-name-plus-options protocol call documented above.

So the signatures come from ObjectStackClient.data (packages/client/src/index.ts), and the payloads they resolve to are the spec's wire schemas — GetDataResponseSchema, CreateDataResponseSchema, UpdateDataResponseSchema, DeleteDataResponseSchema in packages/spec/src/api/protocol.zod.ts — which the SDK's *DataResult interfaces mirror key for key. A managed runtime binds services.data to this same shape.

Parameters

  • object: short object name (for example task, account)
  • id: record ID for single-record operations
  • data: partial payload for create/update
  • query (query): a Partial<QueryAST> — the spec's query protocol shape (packages/spec/src/data/query.zod.ts): where / fields / orderBy / limit / offset, plus the AST-only clauses (search, expand with nested detail, aggregations, groupBy, having). The AST's own object key is not needed here: the server takes the target from the object argument (the URL path) and overwrites anything the body says
  • options (find): filtering, sorting and pagination — two vocabularies, one behaviour; see the table below

find options: canonical and legacy

The signature above accepts QueryOptions | QueryOptionsV2, and the Canonical source declares which of the two to write. QueryOptionsV2 is "canonical query options using Spec protocol field names … the vocabulary data.find() still accepts", while QueryOptions carries an @deprecated tag in the same file describing "legacy parameter names … that require translation to QueryAST", with the instruction to "prefer QueryAST fields directly". Both interfaces are declared in packages/client/src/index.ts, so the recommendation is the SDK's own, not this page's.

Canonical (QueryOptionsV2)Legacy (QueryOptions)Clause
wherefilterfilter conditions (WHERE)
fieldsselectfield selection (SELECT)
orderBysortsort definition (ORDER BY)
limittopmaximum records (LIMIT)
offsetskiprecords skipped (OFFSET)

Write the left column. Those are the QueryAST and protocol field names, so the same words carry from here down through data.query() into the query layer — one translation step fewer to hold in your head.

The right column still works. Nothing refuses it: find recognises a canonical options object and normalizes it into exactly the transport parameters the legacy names produce, so the two columns are equivalent, not merely similar. Deprecated here means "prefer the other spelling", not "scheduled for removal in this version".

Use one column per call. find reads either the canonical names or the legacy ones for a given options object, never a blend — a key from the other column is dropped silently rather than refused, so migrate an options object as a whole.

Returns

  • get: single record payload
  • query/find: list payload + pagination metadata — the same PaginatedResult envelope for both
  • create/update: mutated record payload
  • delete: { object, id, success } — the spec's DeleteDataResponse. The flag is success, not deleted (#5638)

Typical Errors

  • RECORD_NOT_FOUND
  • VALIDATION_FAILED
  • PERMISSION_DENIED

Example

Call these methods from code that holds the binding — a managed runtime hands it in as services.data — so the record id arrives as an ordinary argument. It is deliberately not a hook body: a hook has no services key to reach through (see above), and reads other objects via ctx.api.

import type { ObjectStackClient } from '@objectstack/client';

/** The `services.data` binding, exactly as this page's Canonical source declares it. */
type DataService = ObjectStackClient['data'];

export async function recentOrdersForContact(data: DataService, contactId: string) {
  // `get` resolves the response envelope `{ object, id, record }` — the row is `record`.
  const { record: contact } = await data.get<{ id: string; name: string }>('contact', contactId);

  // `find` resolves `{ records, total?, hasMore? }`. The options are the canonical
  // `QueryOptionsV2` vocabulary — `where` / `orderBy` / `limit`, not `filter` / `sort` / `top`.
  const { records: orders } = await data.find<{ id: string; amount: number }>('sales_order', {
    where: { contact_id: contact.id },
    orderBy: [{ field: 'created_at', order: 'desc' }],
    limit: 20,
  });

  // `query` — the Canonical source's preferred list entry — POSTs the same
  // words as a Partial<QueryAST> body, and carries the clauses `find` has no
  // GET spelling for. Here: per-relation `expand` detail, which `find`
  // refuses with an error that itself points at `query`.
  const { records: withContact } = await data.query<{ id: string; amount: number }>('sales_order', {
    where: { contact_id: contact.id },
    orderBy: [{ field: 'created_at', order: 'desc' }],
    limit: 20,
    expand: { contact_id: { object: 'contact', fields: ['name'] } },
  });

  return { contact, orders, withContact };
}

The block carries no {/* os:check */} marker, and that is a measurement rather than an omission: check:skill-examples compiles marked blocks against the built @objectstack/spec declarations only — its paths map is derived from that package's own exports, and @objectstack/spec does not depend on @objectstack/client. A marked block here would therefore have to hand-declare DataService instead of importing it, which pins the example to itself and nothing else. The marker becomes worth adding the day this surface has a spec-side contract to import (see the Canonical source note).

On this page