Hooks and the execution pipeline
Every extension point in the Data API is a hook on a five-phase chain: built-in behaviour, schema-declared pointcuts: and triggers:, access rules, computed fields, stateflow guards. They differ only in the phase and priority they occupy. Nothing runs outside the chain.
The five phases
Section titled “The five phases”| Phase | Runs | Fires on | Still able to change |
|---|---|---|---|
BeforeValidate | Before any validation, before compilation | every operation | the payload, the query AST, the mutation AST |
AfterValidate | After validation, before compilation | every operation | the payload, the query AST, the mutation AST |
BeforeExec | After the AST is compiled to SQL, before the statement runs | every operation | nothing that reaches the SQL — the statement is already built |
AfterExec | After a mutation statement returns | create, update, delete | the returned rows, response metadata |
AfterScan | After rows are scanned and relations eager-loaded | read, and named actions returning rows | the returned rows |
AfterExec never fires on a read. AfterScan never fires on an entity create, update or delete — not even one with RETURNING. The mutation path scans the returned rows and goes straight to AfterExec, so read-side hooks (masking, decryption, virtual computed fields) do not touch the row a POST or PUT echoes back.
Operations are a bitmask — create, read, update, delete — and a hook runs when any bit overlaps with the current operation.
Read lifecycle
Section titled “Read lifecycle”| # | Stage | What it may change |
|---|---|---|
| 1 | Drain gate — a closed engine refuses with exec: engine closed | — |
| 2 | Observer timer starts | — |
| 3 | Per-request timeout wraps the context, when the request carries one | — |
| 4 | Entity resolved from the compiled schema | — |
| 5 | A remote entity short-circuits to the HTTP router and returns | skips every later stage |
| 6 | AST built from the query, or from filters / select / order_by / limit / offset | — |
| 7 | Row cap injected as a LIMIT — see the row cap | the AST |
| 8 | Hook context built: identity from the request principal, then asof / version / unmask / untokenize from the request flags | — |
| 9 | BeforeValidate | payload, AST |
| 10 | AfterValidate | payload, AST |
| 11 | AST compiled to SQL; row-level security is injected as a WHERE predicate here | — |
| 12 | BeforeExec | nothing that reaches this statement |
| 13 | Statement executed on the tenant’s pool | — |
| 14 | Rows scanned into both row views | — |
| 15 | Includes executed — one extra statement per relation, then children grouped onto parents | rows |
| 16 | AfterScan | rows |
| 17 | Cursor metadata built | — |
Write lifecycle
Section titled “Write lifecycle”A write is dispatched by the entity’s history strategy before the standard path runs. The first matching branch wins:
| Order | Entity | Path |
|---|---|---|
| 1 | materialization: remote | HTTP router, no SQL compiled |
| 2 | Versioned history (type2, type6) | update rewritten to close-current + insert-new; delete rewritten to close |
| 3 | typeaudit | pre-image fetched, standard path, then one audit row per changed field |
| 4 | type3 | pre-image fetched and folded into the payload as <field>_prev |
| 5 | type4 | standard path, plus a pre-image copy into the history table |
| 6 | everything else | standard path |
The standard path itself:
| # | Stage | What it may change |
|---|---|---|
| 1 | Mutation AST built or accepted from the caller | — |
| 2 | ?purge threaded onto the mutation | — |
| 3 | Payload extracted from the mutation data when not supplied separately | — |
| 4 | Hook context built | — |
| 5 | BeforeValidate | payload, mutation AST |
| 6 | AfterValidate | payload, mutation AST |
| 7 | Payload copied back into the mutation AST | — |
| 8 | AST compiled to SQL | — |
| 9 | BeforeExec | nothing that reaches this statement |
| 10 | Placeholders normalised for the dialect | — |
| 11 | Statement executed — Query when the mutation has RETURNING, otherwise Execute | — |
| 12 | Rows and affected-row count populated | — |
| 13 | AfterExec | returned rows |
Ordering
Section titled “Ordering”Within a phase, hooks run in ascending Priority. Ties run in registration order, and registration order is fixed by the service’s boot sequence — so the order is deterministic across restarts.
| Bucket | Priority | Intended for |
|---|---|---|
| Access | 100 | authorization decisions |
| Validation | 200 | input validation |
| Transform | 300 | value rewriting |
| Business | 400 | domain rules |
| Infrastructure | 500 | audit, events, cache |
Any integer is legal, and values between buckets (250, 310) are normal. The supported range is [0, 1000].
What data.svc registers
Section titled “What data.svc registers”This is the complete chain in the deployable service, in run order. Two rows are conditional; everything else is always present.
BeforeValidate
Section titled “BeforeValidate”| Priority | Hook | Operations | Does |
|---|---|---|---|
| 50 | v2.request_options_bridge | all | Copies the query-string flags onto the hook context |
| 100 | defaults | create | Fills defaultvalue: on fields the caller omitted |
| 100 | access | all | Tier-1 action guards, deny-by-default |
| 130–150 | pointcut dispatchers | per method | BeforeBulk (130), BeforeBulkInsert (140), the Before* CRUD methods (150) |
| 200 | validate | create, update | Field validations: and modifiers |
| 250 | entityvalidate | create, update | Entity entityvalidations: |
| 410 | pointcut dispatcher | update | BeforeTransition |
defaults and access are both at 100; defaults was registered first, so field defaults are filled before the access guard runs.
AfterValidate
Section titled “AfterValidate”| Priority | Hook | Operations | Does |
|---|---|---|---|
| 150 | audit | create, update | Fills createdby / createdon / updatedby / updatedon |
| 175 | temporal | read | Resolves ?version=latest-N against the row’s current version number |
| 176 | temporal_type4 | read | Redirects a point-in-time read to the history table |
| 190 | rls_create | create | Evaluates the RLS create rule in-process. Skipped when row-level security is off |
| 260 | filefield | create, update | Stores uploaded file parts. Only when file storage is configured |
| 330–335 | pointcut dispatchers | per method | OnGuardFail, OnAccessDeny (330), OnRLSFilter (335) |
| 350 | businessconstraint | create, update, delete | Multi-entity constraints: |
| 350 | stateflow | create, update | State-machine transition legality |
| 500 | trigger | create, update, delete | before_create / before_update / before_delete triggers |
BeforeExec
Section titled “BeforeExec”| Priority | Hook | Operations | Does |
|---|---|---|---|
| 100 | pointcut dispatcher | all | BeforeBegin |
| 105 | preimage | update, delete | Fetches the pre-mutation row. Runs only for entities declaring an After* / On*Conflict pointcut, or named as the source of a cross-entity refresh trigger |
| 110 | pointcut dispatcher | read | BeforeCacheRead |
| 290 | materialized | create, update | Evaluates write-time computed fields into the payload — after the statement is compiled. See below |
| 380–491 | pointcut dispatchers | per method | BeforeFieldEncrypt (380), OnBulkChunk / BeforeSCDInsert / BeforeSCDExpire (470), BeforeCascade (480), BeforeIndex (490), BeforeReindex (491) |
AfterExec
Section titled “AfterExec”| Priority | Hook | Operations | Does |
|---|---|---|---|
| 420–545 | pointcut dispatchers | per method | AfterTransition (420), AfterBulk (460), AfterBulkInsert (470), the After* CRUD methods and AfterSCDClose (480), OnInsertConflict / OnUpdateConflict / AfterIndex (490), AfterCommit (500), OnRollback (510), OnTerminal (530), AfterCacheWrite (540), OnCacheInvalidate (545) |
| 600 | event | create, update, delete | Registered with no event bus — returns immediately. See below |
| 650 | materialized_refresh_enqueue | create, update, delete | Queues cross-entity computed-field refreshes. Only when the schema declares refresh triggers |
AfterScan
Section titled “AfterScan”| Priority | Hook | Operations | Does |
|---|---|---|---|
| 260 | filefield | read | Resolves stored file references to URLs. Only when file storage is configured |
| 280 | computed | read | Evaluates virtual computed fields onto scanned rows |
| 405–480 | pointcut dispatchers | read | BeforeFieldDecrypt (405), AfterFieldDecrypt (410), OnFieldMask (420), OnComplianceMask (430), AfterQuery / AfterAggregate (480) |
Declared, parsed, and never registered
Section titled “Declared, parsed, and never registered”These keys load without complaint, survive validation, and do nothing at runtime.
| Declaration | Status |
|---|---|
after_create / after_update / after_delete triggers | The after-trigger hook is never constructed. The schema loads; the script never runs |
beforeRead / afterRead / beforePurge / afterPurge in the legacy hooks: block | Permanently inert — the trigger hook subscribes only to create, update and delete, and generates no event name for reads or purges |
events: on an entity | The hook is registered with no bus, so it returns before doing anything. No event is ever published |
subscriptions: at schema or entity level | Parsed and merged into the schema. No runtime code reads it — see Change streams |
Field transforms: | The transform hook (AfterValidate / 300) is not in the chain |
| Field-level encryption | Neither the write-side nor the read-side encryption hook is in the chain |
| Enum label resolution on read | The enum hook is not in the chain — see Enums |
Read-side masking from compliances: | The compliance hook is not in the chain — see Field protection |
| Response caching hooks | Neither the read-side nor the write-side cache hook is in the chain — see Caching |
| Slowly-changing-dimension hook | Not in the chain; versioning is handled by the history dispatch above instead |
| Audit-sink and metrics hooks | Not in the chain; observability comes from the engine observer instead |
| The legacy string-filter row-security hook | Deliberately not wired. Running it alongside the compiler’s predicate translator double-evaluates every rule in two incompatible formats |
script-file: on any hook, pointcut, trigger or rule | Fails at evaluation with file-referenced scripts not yet wired. Only inline bodies execute |
tx-mode: on a scripted endpoint | Parsed and validated; no code opens a transaction from it |
Pointcuts
Section titled “Pointcuts”pointcuts: is the declarative hook surface: a schema author names a lifecycle method and attaches a script. It is entity-scoped only — there is no top-level pointcuts: key, and a top-level block is dropped without a message.
entities: - name: sales_order pointcuts: - method: BeforeInsert name: stamp_channel language: javascript rule: | ({ __patch: { channel: ctx.user.roles.indexOf("api") >= 0 ? "api" : "web" } });
- method: AfterUpdate name: notify_downstream async: true order: 10 code: ORDER_HOOK_FAILED tags: [audit] script: language: javascript expression: | data.create("order_event", { order_id: ctx.row.id, kind: "updated" });| Key | Type | Default | Meaning |
|---|---|---|---|
method | string | — | Canonical method name. Case-sensitive; an unknown value fails the load |
name | string | — | Label used in logs, metrics and error messages |
group | string | — | Legacy v1 form, backfilled into method when method is absent |
language | string | javascript | Engine for the inline rule: shortcut |
rule | string | — | Inline body, the shortcut form. Used only when script: is absent |
script | mapping | — | Nested script reference; wins over rule: |
order | int | 0 | Tie-break among pointcuts on the same method; lower first |
async | bool | false | Run off the request path. Ignored on every Before* method and on OnInsertConflict / OnUpdateConflict / OnGuardFail |
code | string | — | Error code surfaced when this pointcut aborts |
tags | list | — | Observability labels |
enabled | bool | true | Explicit false skips the pointcut at dispatch |
The 49 methods
Section titled “The 49 methods”| Method | Phase | Operations | Priority |
|---|---|---|---|
BeforeQuery | BeforeValidate | read | 150 |
AfterQuery | AfterScan | read | 480 |
BeforeAggregate | BeforeValidate | read | 150 |
AfterAggregate | AfterScan | read | 480 |
BeforeInsert | BeforeValidate | create | 150 |
AfterInsert | AfterExec | create | 480 |
OnInsertConflict | AfterExec | create | 490 |
BeforeBulkInsert | BeforeValidate | create | 140 |
AfterBulkInsert | AfterExec | create | 470 |
BeforeUpdate | BeforeValidate | update | 150 |
AfterUpdate | AfterExec | update | 480 |
OnUpdateConflict | AfterExec | update | 490 |
BeforeUpsert | BeforeValidate | create, update | 150 |
AfterUpsert | AfterExec | create, update | 480 |
BeforeDelete | BeforeValidate | delete | 150 |
AfterDelete | AfterExec | delete | 480 |
BeforeSoftDelete | BeforeValidate | delete | 150 |
AfterSoftDelete | AfterExec | delete | 480 |
BeforeCascade | BeforeExec | delete | 480 |
BeforePurge | BeforeValidate | delete | 150 |
AfterPurge | AfterExec | delete | 480 |
BeforeRestore | BeforeValidate | update | 150 |
AfterRestore | AfterExec | update | 480 |
BeforeBulk | BeforeValidate | all | 130 |
AfterBulk | AfterExec | all | 460 |
OnBulkChunk | BeforeExec | all | 470 |
BeforeBegin | BeforeExec | all | 100 |
AfterCommit | AfterExec | all | 500 |
OnRollback | AfterExec | all | 510 |
BeforeTransition | BeforeValidate | update | 410 |
AfterTransition | AfterExec | update | 420 |
OnGuardFail | AfterValidate | update | 330 |
OnTerminal | AfterExec | update | 530 |
BeforeSCDInsert | BeforeExec | create | 470 |
BeforeSCDExpire | BeforeExec | update | 470 |
AfterSCDClose | AfterExec | update | 480 |
BeforeIndex | BeforeExec | all | 490 |
AfterIndex | AfterExec | all | 490 |
BeforeReindex | BeforeExec | all | 491 |
BeforeFieldEncrypt | BeforeExec | create, update | 380 |
AfterFieldDecrypt | AfterScan | read | 410 |
BeforeFieldDecrypt | AfterScan | read | 405 |
OnFieldMask | AfterScan | read | 420 |
BeforeCacheRead | BeforeExec | read | 110 |
AfterCacheWrite | AfterExec | all | 540 |
OnCacheInvalidate | AfterExec | all | 545 |
OnAccessDeny | AfterValidate | all | 330 |
OnRLSFilter | AfterValidate | read | 335 |
OnComplianceMask | AfterScan | read | 430 |
A dispatcher for every one of these is installed. The dispatcher fires at its phase whether or not the mechanism it is named after is active — OnComplianceMask runs at AfterScan on every read of an entity that declares it, regardless of whether any masking happened.
The return-value contract
Section titled “The return-value contract”| Script returns | Effect |
|---|---|
null / nothing | Continue |
false from a Before* method | Abort with the pointcut’s code:, or POINTCUT_ABORT |
{ __abort: true, code, message } | Abort with that code and message |
{ __patch: { field: value } } from a Before* method | Shallow-merge into the payload |
{ __replace: { … } } from OnInsertConflict | Stored on the hook context. Nothing reads it back today |
| Anything else | Ignored |
Failure and async semantics
Section titled “Failure and async semantics”After* methods plus OnAccessDeny, OnRLSFilter, OnComplianceMask, OnRollback, OnCacheInvalidate and OnTerminal are observers: a script crash there is recorded on the hook context and the operation proceeds. Every other method returns a typed error that aborts the request.
Do not put anything that must not silently fail in an After* pointcut.
async: true sends the work to a process-wide worker pool — 8 workers, a queue of 2048 — shared across tenants.
What a pointcut script sees
Section titled “What a pointcut script sees”One binding, named ctx.
| Key | Populated on |
|---|---|
ctx.op | Always — create / update / delete / query |
ctx.entity | Always — the entity name |
ctx.tenant | Always — the full customer:product:env:tenant key |
ctx.user.id, ctx.user.roles | Always |
ctx.method | Always — the pointcut method name |
ctx.metadata | Always — the per-request bag shared across hooks |
ctx.payload | Whenever a payload exists. A copy |
ctx.row | After* and On*Conflict, single-row — the first scanned row |
ctx.rows | Bulk* methods, when rows were scanned |
ctx.previous | After* and On*Conflict on update and delete, from the pre-image hook |
ctx.tx | When the operation is inside a transaction. Opaque to scripts |
Triggers
Section titled “Triggers”triggers: is the older declarative surface, and it uses a different binding vocabulary from pointcuts.
entities: - name: sales_order triggers: - name: block_locked_orders event: before_update priority: 10 language: javascript script: | payload.status !== "locked";| Key | Meaning |
|---|---|
name | Label used in the abort message |
event | before_create / before_update / before_delete — matched case-insensitively |
priority | Order among triggers on the same event; lower first |
language, expression, script, script-file, function | The script body, inline on the trigger — not nested under a script: mapping |
Bindings: tenant, user_id, roles, entity, operation (Create / Read / Update / Delete, capitalized), payload on mutations, rows after a scan.
A before_* trigger that returns boolean false aborts with trigger "<name>": operation aborted by trigger. Any other return value is ignored.
Writing a hook in JavaScript
Section titled “Writing a hook in JavaScript”js and javascript both select the JavaScript engine; the other accepted language: values are expr (the default when the key is empty), cel, lua, starlark, go and wasm. Anything else fails with script: unsupported runtime.
The engine is ECMAScript 5.1 with parts of newer editions. There is no module loader, no require, no fetch, and no console — logging goes through log.info(…).
One script engine is built per (tenant, datastore) and cached for the life of that tenant’s data engine. It holds the compiled-script cache, so compilation cost amortises across a tenant’s requests without sharing compiled code between tenants.
Return value
Section titled “Return value”The value of the last expression evaluated is the result. For a guard, return a bool.
Guard results are coerced: nil and false are false, 0 and "" are false, and any non-empty string or non-zero number is true. A guard that accidentally returns the string "false" evaluates as allow. A value that cannot be coerced fails with script: expression did not return a boolean.
A panic inside a script is recovered and returned as an evaluation error, not a crash.
Globals
Section titled “Globals”| Namespace | Functions |
|---|---|
data.* | query, find_one, find_by_id, count, exists, aggregate, create, create_many, update, update_many, delete, upsert |
schema.* | entity, fields, field, relations, stateflow, field_group — read-only, no database access |
json.*, string.*, math.*, array.*, time.*, template.*, hash.*, log.* | Pure-compute utilities |
cpet.* | customer, product, environment, tenant — identity strings, always present, empty when the host supplies no identity |
| Root | ulid(), uuid(), nanoid(), now(), tsnano(), interpolate() |
fs.* | Host filesystem — read: readFile, readLines, readJSON, readDir, glob, readFileGlob; write: writeFile, writeLines, writeJSON, appendFile; move: copyFile, moveFile, rename, deleteFile; directories: mkdir, mkdirAll, removeDir, removeAll; inspect: exists, stat, isDir, isFile; temp: tempFile, tempDir; paths: abs, join, dir, base, ext; metadata: chmod, chown, symlink, readlink |
A legacy flat surface is also registered — bare query, queryOne, queryById, count, exists, aggregate, insert, insertMany, update, updateMany, deleteRecord, upsert, getEntity, getFields, getField, getRelations, getStateflow, getFieldGroup, hash, hmac, randomUUID, randomBytes. Prefer the namespaced form: it cannot be shadowed by a tenant-shipped local.
data.* re-enters the engine
Section titled “data.* re-enters the engine”A data.* call goes back through Engine.Query or Engine.Create, which means it runs the full hook chain again — access rules, row-level security, validation, audit stamping. A script that calls data.update on the entity it is hooked to will re-enter its own pointcuts.
Timeouts and limits
Section titled “Timeouts and limits”No script gets the request context either. The evaluator discards the context it is handed and calls the interpreter directly, so no hook, pointcut, trigger, rule or guard body can be cancelled by the request, and none of them can read anything the request carries beyond the bindings listed above.
A worked pointcut
Section titled “A worked pointcut”AfterInsert is an observer method, so a failure here is recorded and the insert still succeeds.
entities: - name: sales_order pointcuts: - method: AfterInsert name: high_value_approval script: language: javascript script: | if (ctx.row.total_amount > 100000) { data.create("approval_task", { id: ulid(), entity: "sales_order", ref_id: ctx.row.id, opened_by: ctx.user.id, opened_at: now() }); }The data.create call re-enters the engine, so the new approval_task row is subject to that entity’s own access rules, row-level security and validations — evaluated with an empty principal, as above, not with the identity that issued the original insert.
Transactions
Section titled “Transactions”| Property | Behaviour |
|---|---|
| Scope | One BEGIN per transactional call. A single-entity POST / PUT / PATCH / DELETE is not wrapped — it is one autocommit statement |
| Multi-operation envelope | The multi-entity bulk request body runs every operation in one transaction; any failure rolls back all of them |
| Bulk update | Opens one transaction and issues one full update per record |
| Isolation | Caller-supplied isolation level, read-only flag, and per-transaction schema. The schema option issues a SET search_path after BEGIN on PostgreSQL |
| Commit | The callback returning nil commits |
| Rollback | The callback returning an error rolls back and returns that error |
| Panic | Rolls back, then re-raises the original panic |
| Nesting | Refused. A transactional call on an already-transactional engine returns exec: nested transaction not supported. There is no savepoint support |
| No factory | exec: no backend configured |
Closing an engine flips a closed flag and polls the in-flight counter until it reaches zero or the context expires. After that every operation returns exec: engine closed without touching the pool. Closing the engine does not close the pool — pool ownership stays with the caller, and several engines can share one pool and drain independently.
Hook coverage on the bulk paths
Section titled “Hook coverage on the bulk paths”| Path | Phases that run |
|---|---|
| Bulk create | BeforeValidate and AfterValidate only, per record |
| Bulk update | The full chain, once per record, inside one transaction |
| Bulk delete | The full chain once for the whole batch — it compiles to one DELETE … WHERE id IN (…), honouring ?purge |
Named actions on the chain
Section titled “Named actions on the chain”A named action runs the same chain as entity CRUD, with the action and its parameters on the hook context instead of an entity payload:
- Resolve the action and pick the section by HTTP method —
GET→read,POST→create,PUT/PATCH→update,DELETE→delete. A missing section is a 405 carrying anAllowheader. - Validate parameters: unknown names rejected, defaults applied,
requiredandenumenforced. - Inject
:user_id,:tenantand:roles; force-set{{schema}}and{{datastore}}. BeforeValidate→AfterValidate→ render SQL →BeforeExec→ execute →AfterExec→AfterScanwhen rows were returned.
Note the order at the end: on the action path AfterExec runs before AfterScan.
| Sharp edge | Detail |
|---|---|
{{schema}} and {{datastore}} are never caller-overridable | Identifiers cannot be bound as values, so a caller-supplied override would let a qualified identifier escape into another tenant’s schema |
:roles is a comma-joined string | So role IN (:roles) is portable. Split on the comma in SQL for real array semantics |
| Parameter type coercion is permissive | Only the enum check is strict; the raw value passes through. Do not rely on the declared type for sanitisation |
Scripted endpoints and the chain
Section titled “Scripted endpoints and the chain”A scripted endpoint is a whole HTTP handler written in script. It does not run on the hook chain — the request never resolves an entity, so no phase fires. The chain is reached only when the script calls data.*, and then it is the chain of whatever entity that call targets, with no principal on it — the same empty-identity re-entry described above.
Two consequences for the pipeline: an endpoint is not wrapped in a transaction (tx-mode: parses and is never acted on), and its script runs under the same absent-timeout conditions as every other script here.
Errors
Section titled “Errors”The HTTP layer classifies by error type first — access denial 403, validation 422, not-found 404 — and falls back to matching the message text for untyped errors: access denied / not authorised / not authorized / forbidden → 403, not found → 404, validation / invalid / is required → 422, everything else → 500. The full status rules and the code table are on error codes.
| Message | Raised by | HTTP |
|---|---|---|
hook <name> (<Phase>): <cause> | Wrapper applied to every hook failure | from the cause |
access denied (tier <n>): <reason> | The access hook refused the operation | 403 |
the rule’s own message: | Field or entity validation refused the write | 422 |
invalid transition on <entity>.<field>: <reason> | The stateflow refused the transition | 422 |
trigger "<name>": operation aborted by trigger | A before_* trigger returned false | 500 |
pointcut <Method>/<Name>: <cause> | A synchronous pointcut script failed | 500 |
exec: entity not found | No rows matched, or the named action is not registered | 404 |
exec: forbidden | The database refused the statement — insufficient privilege | 403 |
exec: validation failed | The database refused the statement — not-null or check violation | 422 |
exec: conflict | Duplicate key or foreign-key violation | 500 |
exec: engine closed | An operation arrived after shutdown began | 500 |
exec: nested transaction not supported | A transaction was opened inside a transaction | 500 |
exec: no backend configured | No transaction factory is wired | 500 |
exec: no compiler available | No SQL compiler for the backend | 500 |
POINTCUT_ABORT | Default code for a Before* pointcut returning false or __abort without a code | — |
POINTCUT_ERR | Default code for a non-observer pointcut script crash | — |
script: unsupported runtime | The language: token is not one of the eight accepted values | 500 |
script: expression did not return a boolean | A guard returned a non-coercible value | 500 |
script evaluation: file-referenced scripts not yet wired | A script reference used script-file: with no inline body | 500 |
pointcut "<name>": unknown method "<m>" | Schema-load failure — the method name is misspelled or unsupported | load fails |
servicefn: not yet implemented | A stubbed legacy script function was called | 500 |
Sharp edges
Section titled “Sharp edges”- Payload edits stop mattering after
AfterValidate. AfterScandoes not run on entity writes, so a created row is echoed back unmasked and without virtual computed fields.after_*triggers,events:,subscriptions:,transforms:and field encryption all load and none of them run.- The
beforeRead,afterRead,beforePurgeandafterPurgeslots of the legacyhooks:block can never fire. - A trigger cannot modify the payload; a pointcut must return
__patchto do so. - The write-time computed-field hook runs one phase too late for its value to reach the column.
ctx.headers,ctx.field,ctx.fieldName,ctx.bulkChunk,ctx.errorandctx.reasonare always undefined.async: trueis ignored on everyBefore*method, and async work is dropped under backpressure.- A crash in an
After*pointcut is swallowed. script-file:never executes in a hook, pointcut, trigger or rule.- No script has an execution timeout, a memory cap or a stack limit, and none of them receive the request context.
- JavaScript, Lua, Starlark and Go scripts get host filesystem access.
- A
data.*call from a script re-enters the chain with no principal — empty user, no roles, deny-by-default inactive. - Bulk create runs two phases out of five and can leave audit columns blank.
- Transactions do not nest, and a single-record write over HTTP is not in one.
Continue with
Section titled “Continue with”| Concern | Page |
|---|---|
| The four access tiers and their rule vocabulary | Access rules |
Rule-to-WHERE translation and its failure mode | Row-level security |
| Declarative field and entity rules | Validations |
| State machines, guards and transition scripts | Stateflows |
| Computed fields, virtual and materialized | Fields |
| History strategies and what each one writes | Entities |
| Query-string flags the bridge hook reads | Request flags |
| Custom HTTP routes implemented in script | Scripted endpoints |
| The published pointcut interfaces | Pointcuts |
| Entities served over HTTP instead of SQL | Remote entities |
| Endpoint-level HTTP reference | Data API reference |