standards
Objects and recordsConcepts

Objects and records

Read, filter, write and search records with the envelope, the sixteen operators and the lifecycle every object shares.


An object is a table you declare: a name, a label expression, and a list of attributes. A record is one row of that object. Every object gets the same REST surface under /records/:object, the same envelope, the same filters and the same lifecycle, so what you learn on one object applies to all of them.

Objects

An attribute has one of sixteen types. Each is declared with a builder from @stndrds/client; see SDK / Attributes for options and inferred types.

TypeHolds
textA short string.
richtextFormatted text. Not filterable, except for emptiness.
numberA number, optionally with a unit and precision.
checkboxtrue or false.
dateA calendar date or a timestamp.
phoneA phone number, written as { countryCode, phoneNumber }, filtered as digits.
currencyAn amount and a currency code.
statusOne value from a declared list, with a colour.
selectOne value from a declared list.
multiselectSeveral values from a declared list.
locationA postal address; filter on city, postalCode or country.
userA workspace user.
relationA link to records of another object, single or .many().
formulaA value computed from the record.
rollupA value aggregated across a relation.
documentA pack of files attached to the record.

Relations link records across objects, and a bilateral relation keeps both sides in step. Documents hold files under a record. Views describe how the app lists and opens records. SDK / Relations and documents and SDK / Views cover them.

Records

Every record carries these fields beside its attribute values:

FieldMeaning
idUUID of the record.
objectNameName of the owning object.
labelDisplay name computed from the object's label expression.
valuesAttribute values, keyed by attribute name. The SDK flattens values onto the record.
visibilityworkspace or private.
ownedByThe owning actor of a private record, null otherwise.
metadataFree-form object you can write, for UI state or your own flags.
createdAt, updatedAtTimestamps.
createdBy, lastUpdatedByActor ids.
deletedAt, deletedBySet while the record sits in the trash.
writeVersionOptimistic-lock token, incremented on every write.
schemaVersionSchema version the record was last migrated to.

These names are reserved. An attribute cannot be called id, label, values or metadata.

Reading

Three routes read a collection. All three answer the same envelope.

RouteUse it for
GET /records/:objectQuick reads with limit, offset, countMode and deleted=true in the query string.
POST /records/:object/listFilters, sorts, a visibility scope, fields and a timezone in the body.
POST /records/:object/searchFull-text search. q is required; the body takes every list option on top.
curl https://api.standards.new/v1/records/contacts/list \
  -H "Authorization: Bearer $STANDARDS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "limit": 20, "sorts": [{ "attribute": "lastName", "direction": "asc" }] }'
{
  "data": [{ "id": "…", "objectName": "contacts", "label": "Ada Lovelace", "values": { "lastName": "Lovelace" } }],
  "page": { "hasMore": true, "total": null, "countMode": "none", "nextOffset": 20 }
}

GET /records/:object/:id reads one record. Add ?includeDeleted=true to read one from the trash.

Pagination and counting

  • limit defaults to 20. Anything above 100 is clamped to 100.
  • limit: 0 returns no rows. With a countMode, it is the cheapest way to count.
  • offset defaults to 0. It must stay below 2147483647 - limit, or the request is a 400.
  • countMode is none (default), estimated or exact. With none, total is null and the database skips the count. Send exact only when a number is displayed.
  • hasMore tells you whether a next page exists. Follow page.nextOffset when present rather than computing it from data.length: search can return an empty page that still advances the cursor.

Filtering

A filter is one combinator, and or or, over a flat list of rules. There is no nesting.

{
  "combinator": "and",
  "rules": [
    { "attribute": "status", "operator": "any_of", "value": ["lead", "active"] },
    { "attribute": "createdAt", "operator": "greater_than", "value": "2026-01-01" }
  ]
}

Sixteen operators exist. Which ones apply depends on the attribute type.

OperatorTypesMeaning
istext, phone, number, currency, checkbox, status, select, date, user, relation, formulaEquals. On a checkbox, value is a boolean.
is_nottext, phone, number, currency, status, select, date, user, relation, formulaDoes not equal.
containstext, phone, formulaCase-insensitive substring.
not_containstext, phone, formulaSubstring absent.
starts_withtext, phoneCase-insensitive prefix.
ends_withtextCase-insensitive suffix.
greater_thannumber, currency, date, formulaStrictly after or above. Dates compare by calendar day.
greater_or_equalnumber, currency, date, formulaOn or after, at or above.
less_thannumber, currency, date, formulaStrictly before or below.
less_or_equalnumber, currency, date, formulaOn or before, at or below.
any_ofstatus, select, multiselect, user, relationAt least one stored value is in the list.
none_ofstatus, select, multiselect, user, relationNo stored value is in the list.
is_emptyevery type but checkbox and rollupNo value. value is null.
is_not_emptyevery type but checkbox and rollupHas a value. value is null.
is_withindateRelative range: { "amount": 7, "unit": "days", "direction": "past" }.
on_day_monthdateAnniversary, any year: { "day": 14, "month": 7 }.

System fields filter too: id, label, createdAt, updatedAt, createdBy, lastUpdatedBy and the others take the operators of their kind.

Aliases. On input, the API also accepts eq, neq, lt, gt, lte, gte, in, notIn, not_in, and the camelCase forms (startsWith, isEmpty, doesNotContain …). They are normalized to the canonical name above before validation and never stored. The former before, after, on_or_before, on_or_after, is_checked, is_not_checked and day_month_eq are rejected with a message naming the replacement.

Dynamic values. Three values resolve at query time:

{ "attribute": "owner", "operator": "is", "value": { "dynamic": "actor", "ref": "current" } }
{ "attribute": "dueDate", "operator": "is", "value": { "dynamic": "date", "anchor": "today" } }
{ "attribute": "updatedAt", "operator": "less_than", "value": { "dynamic": "date", "anchor": "now" } }

The actor token (@me) matches the caller, on a user attribute as on createdBy. today resolves to a calendar day in the request's timezone, which defaults to UTC; send timezone in the body or an x-timezone header (Europe/Paris) so today means the caller's today. now is the current UTC instant. When @me cannot resolve, the rule is dropped, not rejected.

Relation and document properties. When a relation carries qualified properties, add property to filter on the property and quantifier (any or none) to say how many linked records must match:

{ "attribute": "members", "property": "role", "operator": "is", "value": "admin", "quantifier": "any" }

A location filters the same way, on property: "city", "postalCode" or "country" (an ISO3 code), with the text operators and no quantifier.

Sorting

sorts is an ordered list of { attribute, direction }, asc or desc. Sorts apply in the order given.

Reference hydration

By default every relation, document and user attribute comes back as the linked records. fields narrows that:

  • omit fields: every reference attribute is hydrated;
  • "fields": ["company"]: only those attributes are hydrated;
  • "fields": []: nothing is hydrated and the keys are absent.

Skipping hydration is the cheapest read for exports.

Writing

Create

POST /records/:object takes the values under data and options beside them:

curl https://api.standards.new/v1/records/contacts \
  -H "Authorization: Bearer $STANDARDS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "data": { "firstName": "Ada", "lastName": "Lovelace" }, "options": { "visibility": "private" } }'
  • options.allowDraft skips required-attribute validation. The record is created and nothing marks it incomplete.
  • options.visibility sets workspace (default) or private.
  • options.linkTo ({ objectName, recordId, attribute, expectedWriteVersion }) creates the record and links it to an existing one in one transaction. The owner's new writeVersion comes back in a response header.

The answer is 201 with the record.

Update

PUT /records/:object/:id is a partial update: send only the attributes that change. The body is the values object itself, with no data wrapper. An empty body is a 400.

curl -X PUT https://api.standards.new/v1/records/contacts/$RECORD_ID \
  -H "Authorization: Bearer $STANDARDS_API_KEY" \
  -H "If-Match: 4" \
  -H "Content-Type: application/json" \
  -d '{ "email": "[email protected]" }'

If-Match carries the writeVersion you last read. A stale version rejects the write; omit the header to write unconditionally. Sending null, "" or [] clears an attribute, required or not: required() gates creation, not later writes.

Delete, restore, purge

ActionRoutePermissionEffect
DeleteDELETE /records/:object/:iddeleteMoves the record to the trash. 204.
RestorePOST /records/:object/:id/restoredeleteBrings it back.
PurgePOST /records/:object/:id/purgemanageRemoves it for good. 409 unless it is already in the trash.

GET /records/:object?deleted=true lists the trash.

Bulk

RouteBodyMax itemsResult
POST /records/:object/bulk-create{ "records": [ … ], "options"? }10 000{ created, errors: [{ index, error }] }
POST /records/:object/bulk-update{ "updates": [{ "id", "data" }] }100{ updated, errors: [{ id, error }] }
POST /records/:object/bulk-delete{ "ids": [ … ] }100{ deleted, errors: [{ id, error }] }

Each item is validated before the batch runs. A failed item lands in errors while its siblings proceed; the batch is not transactional, so a bulk update that reports one error has still written the others.

Visibility

A record is workspace or private. A private record is returned only to its owner; anyone else gets the same 404 as for a missing id. List and search accept a visibility scope to read one side only. Hydration and totals never leak private records.

PATCH /records/:object/:id/visibility with { "visibility": "private" } flips a record. Only the creator can make a workspace record private, and only the owner can open it back up. ownedBy is set by the server and cannot be assigned.

One object. POST /records/:object/search runs a full-text query with every list option on top. q is required; an empty string means "filters only". Totals are lower bounds (totalIsExact: false) because permission checks run after ranking.

curl https://api.standards.new/v1/records/contacts/search \
  -H "Authorization: Bearer $STANDARDS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "q": "lovelace", "limit": 10 }'

The whole workspace. GET /search?q= looks across every object you can read and returns hits, not records.

Query paramEffect
qThe text to search. Required.
objectsComma-separated object names to restrict to.
grouped=trueOne group per object instead of a flat list.
deletedarchived for the trash only, all for both. Default: live records.
countMode=noneSkip the exact count. Any other value keeps it.
limit, offsetFlat mode only; default 20.

A hit carries recordId, objectId, objectName, objectLabel, label, createdAt, updatedAt, and deletedAt when the record is in the trash. Flat responses are { results, total, totalIsExact, hasMore, nextOffset? }; grouped ones replace results with groups: [{ objectName, objectLabel, results, count }]. Fetch the record by id when you need its values.

Next steps