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
/repository/createJWT/repository/createJWTBoth 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
/repository/create-extendedJWTTakes { 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.
/repository/cloneJWTTakes { datatype, uid } and duplicates an existing record.
/repository/bulk-createJWTMany records in one call. See Bulk and migration.
Read
/repository/get/:datatype/:idJWT:id is the sk.
/repository/find-any-id/:datatype/:idJWTLooks the record up by any identifier it recognizes, not only sk — useful when you hold a slug or an external id.
/repository/find-by-attribute/:datatype/:attribute?/:attrValue?JWT/repository/get/:datatype/:attribute?/:value?JWTFetch by an arbitrary field — how you resolve a page by slug or a customer by email.
/repository/find-related/:datatype/:anyIdJWTRecords related to the given one.
/repository/find-timed-data/:startDate?/:endDate?JWTRecords within a date window.
/repository/isunique/:datatype/:attribute/:value/:scopeValue?JWTUniqueness check — what a form calls while the user is still typing.
/repository/lookup-codeJWTResolves a lookup code to its record.
/repository/link/:datatype/:idJWTA 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
/repository/update/:idJWTFull 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.
/repository/update-partial/:datatype/:idJWTPatch 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
/repository/delete/:datatype/:idJWT/repository/delete/:datatypeJWT/repository/truncate/:datatypeJWTSingle, 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:
/repository/trash-restoreJWTDELETE /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.
Search
/repository/search/:datatypeJWT{ "keyword": "smith", "query": { "data.status": "active" }, "options": { "page": 0, "pageSize": 25 } }/repository/search/:datatype?JWTThe 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 plainfind(query, options). Full-text is skipped entirely. - No
datatype— defaults tosite_index, the cross-content search index. - Missing text index — a
text index required for $text queryerror triggersfixCollectionfor 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
400carrying the underlying message.
Aggregate
/repository/aggregate/:datatypeJWTRuns an aggregation pipeline against the datatype — grouping, counting and summing without pulling records to the client.
Categories and tags
/repository/category/:datatype?JWT/repository/tagJWT/repository/tagJWTCategories are a tree (the category datatype); tags are flat (tag, grouped by tag_group). Both are referenced from BaseModel.post.
Settings
/repository/setting/:settingType/:settingName?/:subName?JWT/repository/setting/:settingType/:settingName?/:subName?JWT/repository/setting/:settingType/:settingName?/:subName?JWTOrg-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
/repository/org/createJWT/repository/org/:orgidJWT/repository/orgJWT/repository/org/update/:orgidJWT/repository/org/delete/:orgidJWT/repository/org-by-hostname/:hostnameJWT/repository/org/user/:emailJWT/repository/org/query/:datatypeJWT/repository/org/user/:email:/:orgidJWTorg/query/:datatype runs a query in the org context rather than the caller's tenant.
Preview
/repository/preview/:orgId/:site/:pageJWTRenders 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.