docs
/
AppEngine API

The repository

One generic controller that serves create, read, update and delete for all 261 datatypes.

RepositoryController is the single largest surface in AppEngine — 92 handlers under /repository — and it is generic. There is no POST /products and no POST /employees; there is PUT /repository/create with a datatype in the body.

Learn this controller and you can manipulate every datatype in the platform, including collections you define yourself.

Create

PUT/repository/createJWT
POST/repository/createJWT

Both verbs hit the same handler. Requires the create content permission.

{
  "datatype": "sf_product",
  "isNew": true,
  "name": "Blue Widget",
  "data": { "sku": "BW-1", "price": 19.99 }
}

The author is stamped for you: the handler passes the caller's email, and for PUT …/create CurrentUserMiddleware sets body.author from the caller's username, email or sk when the body does not already carry one. When a System user acts through x-client-authorization, the customer's sk is written as author instead.

Returns the created BaseModel<T>.

Variants

PUT/repository/create-extendedJWT

Takes { data, action, options }. The implemented action is create-page-from-template, which clones a page out of SHARED_ORG into the caller's org, giving the copy a unique name and slug.

PUT/repository/cloneJWT

Takes { datatype, uid } and duplicates an existing record.

POST/repository/bulk-createJWT

Many records in one call. See Bulk and migration.

Read

GET/repository/get/:datatype/:idJWT

:id is the sk.

GET/repository/find-any-id/:datatype/:idJWT

Looks the record up by any identifier it recognizes, not only sk — useful when you hold a slug or an external id.

GET/repository/find-by-attribute/:datatype/:attribute?/:attrValue?JWT
GET/repository/get/:datatype/:attribute?/:value?JWT

Fetch by an arbitrary field — how you resolve a page by slug or a customer by email.

GET/repository/find-related/:datatype/:anyIdJWT

Records related to the given one.

GET/repository/find-timed-data/:startDate?/:endDate?JWT

Records within a date window.

GET/repository/isunique/:datatype/:attribute/:value/:scopeValue?JWT

Uniqueness check — what a form calls while the user is still typing.

POST/repository/lookup-codeJWT

Resolves a lookup code to its record.

GET/repository/link/:datatype/:idJWT

A Studio link to a record, for handing to a person instead of an id: { url, title, datatype }. The url is <STUDIO_URL>/app/collection/<datatype>/<sk>, which the Studio opens in the record's own app when it has one, else in the Database app. title is the person's name, else the record's title, subject, name or email. Staff only — a customer token gets 403 — and 404 when the record does not exist. This replaces the older /links/record/… route.

Update

POST/repository/update/:idJWT

Full update. Requires the update content permission. The body carries datatype and sk; when both are present, CurrentUserMiddleware pre-loads the existing record into request.currentData so the guard can run its ownership and requiredRole checks before the handler is reached.

POST/repository/update-partial/:datatype/:idJWT

Patch specific fields, using dotted paths:

{ "data.audit.lastLogin": "2026-08-28T10:00:00.000Z", "data.status": "active" }

This is what the platform itself uses for narrow writes — the sign-in path patches data.audit this way rather than rewriting the user.

Delete

DELETE/repository/delete/:datatype/:idJWT
POST/repository/delete/:datatypeJWT
DELETE/repository/truncate/:datatypeJWT

Single, bulk, and empty-the-collection respectively.

Before a DELETE …/repository/*, the middleware parses the datatype and sk back out of the URL and loads the record into currentData — so ownership is checked on the actual record, not just on the URL.

Deletes are recoverable:

POST/repository/trash-restoreJWT
truncate empties a collection

DELETE /repository/truncate/:datatype removes every record of that datatype in the tenant. There is no datatype-level confirmation step. It is API-only and limited to RootAdmin (with the delete content permission); the Studio's Collections screen does not offer it. Like the other deletes, it refuses customer and user with 400 USE_DOMAIN_API — those are removed through their own endpoints.

POST/repository/search/:datatypeJWT
{ "keyword": "smith", "query": { "data.status": "active" }, "options": { "page": 0, "pageSize": 25 } }
GET/repository/search/:datatype?JWT

The GET form takes ?keyword=, ?query= (JSON), ?p= (page, default 1) and ?ps= (page size, default 50).

Behavior worth knowing:

  • No keyword — the call degrades to a plain find(query, options). Full-text is skipped entirely.
  • No datatype — defaults to site_index, the cross-content search index.
  • Missing text index — a text index required for $text query error triggers fixCollection for that datatype and one automatic retry. The first search on a new collection can therefore be slow but still succeed.
  • Any other search error becomes a 400 carrying the underlying message.

Aggregate

POST/repository/aggregate/:datatypeJWT

Runs an aggregation pipeline against the datatype — grouping, counting and summing without pulling records to the client.

Categories and tags

GET/repository/category/:datatype?JWT
GET/repository/tagJWT
POST/repository/tagJWT

Categories are a tree (the category datatype); tags are flat (tag, grouped by tag_group). Both are referenced from BaseModel.post.

Settings

GET/repository/setting/:settingType/:settingName?/:subName?JWT
POST/repository/setting/:settingType/:settingName?/:subName?JWT
DELETE/repository/setting/:settingType/:settingName?/:subName?JWT

Org-level configuration, keyed by BaseSettingKeys — securitySettings, passcodeLoginSettings and friends. settingName and subName drill into named and nested values. Admin only.

These are the switches that change platform behavior: enableTwoFactorForUsers, enableNewDeviceAuthentication, alertOnNewDeviceLogin all live under securitySettings.

Organizations

POST/repository/org/createJWT
GET/repository/org/:orgidJWT
GET/repository/orgJWT
POST/repository/org/update/:orgidJWT
DELETE/repository/org/delete/:orgidJWT
GET/repository/org-by-hostname/:hostnameJWT
GET/repository/org/user/:emailJWT
POST/repository/org/query/:datatypeJWT
DELETE/repository/org/user/:email:/:orgidJWT

org/query/:datatype runs a query in the org context rather than the caller's tenant.

Preview

GET/repository/preview/:orgId/:site/:pageJWT

Renders a page as it would appear published — what Build Studio's preview pane loads. On the site itself, staff preview a page at /__preview/page/<site>/<page name, slug or id> and a post at /__preview/post/<slug or id> (Content Studio).

AI-callable methods

Repository service methods are annotated with @AiCallable, carrying a description, per-parameter docs and a return description:

@AiCallable({
  description: 'Insert a record bypassing all collection/workflow/schedule pre/post processing…',
  params: { orgid: 'Org id.', data: 'BaseModel to insert.', … },
  returns: 'Inserted record (when returnObject=true) or provider-shaped result.',
})
async createNoChecks(...)

This is the metadata the MCP server (POST /mcp, see MCP) exposes, so an AI agent can introspect what the platform can do at runtime. Add a repository method that agents should be able to reach and it needs this decorator.