Request flags
data.svc parses 23 flags off the query string on its REST routes: 18 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 dropped silently and the read falls back to current-row semantics.
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. Unusable on REST reads — see the caution below. |
field | field | field name | none | Names the file field on ?download=true. Inert on every other path, and inert there too — see download. |
download | download | true | false | Routing switch, not an engine flag: on a get-by-id path it streams a file field instead of returning the row. Matches only the literal value true (any case), not the truthy set above. Returns 501 v2.file_downloader_unconfigured in the shipping service — no downloader is registered at boot. On a list path without an id it returns 400 v2.download_needs_id. |
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. |
deletechildren | deletechildren | bool | false | No effect — no cascade-delete hook is registered. |
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. asof never reaches the engine
over REST and field never reaches the file path — see the cautions below. 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. Non-versioned entities ignore the flag.
Response-shape flags
Section titled “Response-shape flags”| Flag | Spellings | Adds to the envelope |
|---|---|---|
stats | stats, sendstats | stats: sql, duration_ms, include_sql[], rows_affected |
meta | meta, sendmeta | meta: entity, primary_key, fields[] with name, type, required, nullable |
states | states, sendstates | states: one object per row, keyed by stateflow name, each {field, current, events[]} |
allowedactions | allowedactions, sendallowedactions | allowed_actions[]: the action names declared on the entity |
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 carries a different shape:
{operations_run, operations_ok, operations_fail}.
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 does not. Two components implement mask/redact/hidden — a compliance hook
that runs after the scan, and a compiler-side output filter. data.svc registers neither on
its read path: the hook is absent from its chain, and the output filter is constructed at boot
but never invoked. Nothing on the read path applies or lifts those transforms, so neither flag
changes a single byte of the response body. A host that builds its own hook chain can install
the compliance hook and get the reveal behaviour; data.svc as shipped does not.
Treat the guards as the enforceable contract and do not treat the absence of ?unmask=true as
a redaction control.
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 the compiled SQL text in stats.sql, plus one entry per eager-load
query in stats.include_sql. Bind values are not included — the SQL carries placeholders —
but the table names, column list, injected tenancy and row-level-security predicates are all
visible to anyone who can read the entity. Keep the flag out of browser-facing clients.
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 v2.bad_expression |
| Runtime error | 500, code v2.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}],"count":1}