Request flags
data.svc parses 22 flags off the query string on its REST routes: 17 that travel with the
request through the engine and hook chain, 4 that only change the shape of the response
envelope, and download, which switches a get-by-id into file streaming. On a read, every
query-string key that is not one of these (and not a query-AST key such as select or
limit) becomes an equality filter predicate instead.
Where flags apply
Section titled “Where flags apply”| Route (public path) | Engine/routing flags | Response-shape flags |
|---|---|---|
GET /data/rest/{entity} | all | all |
GET /data/rest/{entity}/id/{id} | all | all |
POST /data/rest/{entity} (JSON or multipart) | all | all |
PUT/PATCH /data/rest/{entity}[/id/{id}] | all | all |
DELETE /data/rest/{entity}/id/{id} | all | all |
POST /data/rest/{entity}/id/{id}/restore | all | all |
POST /data/rest/{entity}/search | all | all |
POST/PUT/PATCH/DELETE /data/rest (bulk envelope) | parsed; datastore and purge take effect, delta and expression do not | stats only |
GET/POST/… /data/anon/rest/* | all | all |
The anonymous mirror parses exactly the same flags as the authenticated routes; it has no
restore path. GraphQL, /data/actions/{name}, the scripted /data/x/* endpoints and every
admin route read ?datastore= only, none of the other flags reach them. Route shapes,
headers and status codes are in the Data API HTTP reference.
How values are parsed
Section titled “How values are parsed”- Keys are case-insensitive.
?sendStats=true,?sendstats=trueand?SendStats=truereach the same field. The one exception is the file-download path, which readsdownloadandfieldwith an exact, lower-case key lookup. - Truthy values are
true,t,yes,y,on,1(any case). Everything else, including?deletedwith no value, is false. - Unknown keys are never rejected. A request carrying a flag the service does not know gets no 400; on a read it becomes a filter predicate.
- A repeated key uses its first value.
- A malformed
asofis refused with400 invalid_asofnaming the accepted spellings. It used to be dropped, and the read answered with current rows and a200.
Engine and routing flags
Section titled “Engine and routing flags”| Flag | Spellings | Value | Default | Effect |
|---|---|---|---|---|
deleted | deleted | bool | false | Drops the auto-injected deletedon IS NULL predicate so soft-deleted rows come back. Reads only; no effect on entities without soft delete. |
unmask | unmask | bool | false | Asks for unmasked values on a read and stacks the entity’s unmask access rule. Does not change the body in the shipping service: see below. |
untokenize | untokenize | bool | false | Asks for detokenised values on a read and stacks the entity’s decrypt access rule (not an untokenize rule). Does not change the body in the shipping service. |
purge | purge | bool | false | Turns a delete into a hard DELETE instead of a soft-delete UPDATE, and bypasses version bookkeeping on versioned entities. Stacks the entity’s purge access rule. |
delta | delta | bool | false | On create and update, trims every returned row to the literal key id plus the keys present in the request body. Reads, deletes and restores pass through untrimmed. |
datastore | datastore | datastore name | schema default | Selects which of the tenant’s datastores serves the request. |
expression | expression | jq program | none | Runs jq over the composed envelope and replaces the response body with the result. |
version | version | N, first, latest, latest-N, -N | current row | Pins the version read on entities with a versioned history strategy. |
asof | asof | timestamp | current row | Point-in-time read on versioned entities: the row as it stood at that moment, on a get-by-id, a list and a search. |
field | field | field name | none | Names the file field on ?download=true. Inert on every other path. |
download | download | true | false | Routing switch, not an engine flag: on a get-by-id path it streams the named file field instead of returning the row, with Content-Type, Content-Length and a Content-Disposition filename. Matches only the literal value true (any case), not the truthy set above. On a list path without an id it returns 400 download_needs_id; a field the entity does not declare is 404 field_not_found; a deployment with files.enabled: false is 501 file_downloader_unconfigured. |
scd | scd, cdc | bool | unset | Records an explicit enable/disable of history bookkeeping for the mutation. No hook consumes it; nothing changes. |
sendhash | sendhash | bool | false | No effect. No hook consumes it. |
noversion | noversion | bool | false | No effect. No hook consumes it. |
skipstateflow | skipstateflow | bool | false | No effect on mutations; breaks reads: see the caution below. |
customstateflow | customstateflow | field:flow,field2:flow2 | none | Parsed into a map. No hook consumes it; stateflows are not swapped. |
event | event | event name | none | No effect, the stateflow machine derives the event from the target state in the payload. |
alias | alias | entity alias | none | No effect. |
Seven of these change behaviour today: deleted, purge, delta, datastore, expression,
version, and the access-guard half of unmask/untokenize. deletechildren is gone: what
happens to child rows on a delete is declared on the reference
(onDelete), not chosen per request, and the flag is no longer parsed. field never reaches the file path, see the caution below; asof reaches the engine and
answers historically, the rest parse cleanly, travel on the request context, and are mirrored into the hook chain’s metadata map,
but no hook in the deployable service reads them. They are a stable contract for a host that
installs its own hooks, not behaviour you get by sending them.
?version= accepted forms:
| Form | Meaning |
|---|---|
N (positive integer) | exact _version_num = N |
first | version 1 |
latest | the current row: same as omitting the flag |
latest-N, -N | N versions back from the row’s current _version_num |
latest-N needs a primary-key predicate in the request; without one, or when the token is
malformed, the read falls back to the current view and says so: the envelope gains a top-level
warnings[] entry with code: "version_fallback" and the reason as its message. Non-versioned
entities ignore the flag.
Response-shape flags
Section titled “Response-shape flags”Every response-shape flag adds its block under meta, next to the entity and count that are
always there. Nothing is added at the top level of the envelope.
| Flag | Spellings | Adds to meta |
|---|---|---|
stats | stats, sendstats | meta.stats: duration_ms, total_count, rows_affected; sql and include_sql[] for an admin only |
meta | meta, sendmeta | meta.schema: entity, primary_key, fields[] with name, type, required, nullable |
states | states, sendstates | meta.states[]: one object per row, keyed by stateflow name, each {field, current, events[]} |
allowedactions | allowedactions, sendallowedactions | meta.allowed_actions[]: the action names declared on the entity |
meta.states is omitted entirely when the entity declares no stateflows, so a client cannot tell
“no stateflows” from “flag not sent” without checking the schema.
allowed_actions lists the actions that have a rule declared on the entity, create,
read, update, delete, purge, restore, unmask, decrypt, not the actions this
caller is permitted to perform, an entity with no access.actions block returns the fallback
list ["create","read","update","delete"]. Render buttons from it if you like, but the 403
still arrives from the access gate.
On the bulk envelope only stats is honoured, and it lands under meta.batch with the batch’s
own shape: {operations_run, ok, fail, duration_ms}.
Point-in-time reads work on GET /data/rest/{entity}, GET /data/rest/{entity}/id/{id} and
POST /data/rest/{entity}/search. Until 2026-09-17 asof and skipstateflow were missing from
the reserved-key set, so they fell through to the auto-filter branch and became equality
predicates on columns of the same name: the compile failed with field "asof" not found on entity "<name>" and the request answered 404. Mutation routes build no query from the URL and are
unaffected; the versioning filter runs on reads, so asof would do nothing there anyway.
Unmasked and detokenised reads
Section titled “Unmasked and detokenised reads”?unmask=true and ?untokenize=true do two separate things, and only the first of them is
live in the deployable service.
The access guard runs. On a read, unmask stacks the entity’s unmask rule and
untokenize stacks its decrypt rule on top of the base read rule, with AND logic, a
caller who passes read but fails unmask is denied. The extended guard is evaluated only
when the entity declares that rule, an entity with a read rule and no unmask rule admits
?unmask=true on the strength of the read rule alone. Declare the rule if you want the flag
gated; see Access rules.
The reveal follows the guard. The read-side compliance hook applies mask, redact and
hidden on every read; ?unmask=true lifts them for a caller who passes the entity’s
unmask rule, so an admin reads the cleartext where an anonymous caller reads
*******7777. Encrypted and tokenized columns are decrypted and detokenised transparently on
read, encryption is an at-rest control, and the per-caller control is masking, so
?untokenize=true changes nothing beyond stacking the decrypt rule.
Declare the unmask rule on every entity that carries a masked field: without it, the flag is
admitted on the strength of the read rule alone.
Deleted-row visibility
Section titled “Deleted-row visibility”?deleted=true has no permission of its own. Any caller allowed to read the entity can
retrieve its soft-deleted rows by adding the flag, including over the anonymous mirror at
/data/anon/rest/{entity}. If deleted rows are sensitive, constrain them with a row-level
security predicate rather than assuming the default filter hides them.
Hard deletes
Section titled “Hard deletes”?purge=true marks the delete as a hard delete, which makes the compiler emit a real DELETE
rather than the soft-delete UPDATE, and routes versioned entities around version bookkeeping,
no closing row is written, so the history gap is not recorded, the purge access rule is
stacked only when the entity declares one; otherwise delete permission is enough.
The bulk envelope carries its own per-row purge key inside data. The query-string flag is
read from the request context by the engine, so ?purge=true on the bulk URL applies to
every delete operation in the batch.
Statistics and expression cost
Section titled “Statistics and expression cost”?stats=true returns stats.duration_ms and stats.total_count to every caller. The compiled
statement, stats.sql, and one entry per eager-load query in stats.include_sql, are returned
only to a caller with the admin or superadmin role: the statement carries the physical schema
and table names, every column it touched, and the tenancy and row-level-security predicates the
compiler injected, which is the row rule itself. An admin can read the same statement from the
statement log.
stats.total_count and the envelope’s total are not populated on reads: the engine never
sets a total count. On a mutation with no returned rows, total carries the affected-row
count instead.
?expression= runs a jq program in-process over the fully composed envelope, so .data,
.count and .stats are addressable and the result replaces the body wholesale.
| Behaviour | Value |
|---|---|
| Evaluation ceiling | 2 seconds, hard |
| Compiled-program cache | 256 entries, FIFO eviction |
| Zero results | null |
| More than one result | JSON array of the results |
| Compile error | 400, code bad_expression |
| Runtime error | 500, code expression_runtime |
It runs on every REST route that emits the standard envelope, including create and update responses. It does not run on the bulk envelope.
Worked example
Section titled “Worked example”Read soft-deleted rows of one entity, project them down to two fields with jq, and ask for the compiled SQL:
curl -G https://api.example.com/data/rest/invoice \ -H "Authorization: Bearer $TOKEN" \ -H "X-Customer: acme" -H "X-Product: erp" -H "X-Env: prod" -H "X-Tenant: main" \ --data-urlencode "deleted=true" \ --data-urlencode "limit=50" \ --data-urlencode "stats=true" \ --data-urlencode "expression={sql: .stats.sql, rows: (.data | map({id, total}))}"The jq program runs over the whole envelope, so it has to re-project anything it wants to
keep, .data | map(...) on its own would discard the stats block you just asked for.
Create a row and get back only what you sent:
curl -X POST "https://api.example.com/data/rest/invoice?delta=true" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Customer: acme" -H "X-Product: erp" -H "X-Env: prod" -H "X-Tenant: main" \ -H "Content-Type: application/json" \ -d '{"customer_id":"01H...","total":420}'{"data":[{"id":"01J...","customer_id":"01H...","total":420}],"meta":{"count":1}}