Skip to content
Talk to our solutions team

Hooks and the execution pipeline

Every extension point in the Data block 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.

PhaseRunsFires onStill able to change
BeforeValidateBefore any validation, before compilationevery operationthe payload, the query AST, the mutation AST
AfterValidateAfter validation, before compilationevery operationthe payload, the query AST, the mutation AST
BeforeExecAfter the AST is compiled to SQL, before the statement runsevery operationnothing that reaches the SQL. The statement is already built
AfterExecAfter a mutation statement returnscreate, update, deletethe returned rows, response metadata
AfterScanAfter rows are scanned and relations eager-loadedread, and named actions returning rowsthe 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.

#StageWhat it may change
1Drain gate. A closed engine refuses with exec: engine closed—
2Observer timer starts—
3Per-request timeout wraps the context, when the request carries one—
4Entity resolved from the compiled schema—
5A remote entity short-circuits to the HTTP router and returnsskips every later stage
6AST built from the query, or from filters / select / order_by / limit / offset—
7Row cap injected as a LIMIT: see the row capthe AST
8Hook context built: identity from the request principal, then asof / version / unmask / untokenize from the request flags—
9BeforeValidatepayload, AST
10AfterValidatepayload, AST
11AST compiled to SQL; row-level security is injected as a WHERE predicate here—
12BeforeExecnothing that reaches this statement
13Statement executed on the tenant’s pool—
14Rows scanned into both row views—
15Includes executed. One extra statement per relation, then children grouped onto parentsrows
16AfterScanrows
17Cursor metadata built—

A write is dispatched by the entity’s history strategy before the standard path runs, the first matching branch wins:

OrderEntityPath
1materialization: remoteHTTP router, no SQL compiled
2Versioned history (type2, type6)update rewritten to close-current + insert-new; delete rewritten to close
3typeauditpre-image fetched, standard path, then one audit row per changed field
4type3pre-image fetched and folded into the payload as <field>_prev
5type4standard path, plus a pre-image copy into the history table
6everything elsestandard path

The standard path itself:

#StageWhat it may change
1Mutation AST built or accepted from the caller—
2?purge threaded onto the mutation—
3Payload extracted from the mutation data when not supplied separately—
4Hook context built—
5BeforeValidatepayload, mutation AST
6AfterValidatepayload, mutation AST
7Payload copied back into the mutation AST—
8AST compiled to SQL—
9BeforeExecnothing that reaches this statement
10Placeholders normalised for the dialect—
11Statement executed: Query when the mutation has RETURNING, otherwise Execute—
12Rows and affected-row count populated—
13AfterExecreturned rows

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.

BucketPriorityIntended for
Access100authorization decisions
Validation200input validation
Transform300value rewriting
Business400domain rules
Infrastructure500audit, events, cache

Any integer is legal, and values between buckets (250, 310) are normal. The supported range is [0, 1000].

This is the complete chain in the deployable service, in run order. Two rows are conditional; everything else is always present.

PriorityHookOperationsDoes
50request_options_bridgeallCopies the query-string flags onto the hook context
100defaultscreateFills defaultvalue: on fields the caller omitted
100accessallTier-1 action guards, deny-by-default
150pointcut dispatchersper methodThe Before* CRUD methods, BeforeAggregate, BeforePurge, BeforeRestore, BeforeSoftDelete, BeforeUpsert
200validatecreate, updateField validations: and modifiers
250entityvalidatecreate, updateEntity entityvalidations:

defaults and access are both at 100; defaults was registered first, so field defaults are filled before the access guard runs.

PriorityHookOperationsDoes
150auditcreate, updateFills createdby / createdon / updatedby / updatedon
175temporalreadResolves ?version=latest-N against the row’s current version number
176temporal_type4readRedirects a point-in-time read to the history table
190rls_createcreateEvaluates the RLS create rule in-process. Skipped when row-level security is off
260filefieldcreate, updateStores uploaded file parts. Only when file storage is configured
335pointcut dispatcherreadOnRLSFilter
350businessconstraintcreate, update, deleteMulti-entity constraints:
350stateflowcreate, updateState-machine transition legality
360–440pointcut dispatchersper methodBeforeTransition (360), BeforeSCDInsert (395), BeforeFieldEncrypt (440)
500triggercreate, update, deletebefore_create / before_update / before_delete triggers
PriorityHookOperationsDoes
105preimageupdate, deleteFetches 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
110pointcut dispatcherreadBeforeCacheRead
290materializedcreate, updateEvaluates write-time computed fields into the payload: after the statement is compiled. See below
480pointcut dispatcherdeleteBeforeCascade
PriorityHookOperationsDoes
420–545pointcut dispatchersper methodAfterTransition (420), the After* CRUD methods (480), AfterCommit (500), OnRollback (510), OnTerminal (530), OnCacheInvalidate (545)
600eventcreate, update, deleteRegistered with no event bus: returns immediately. See below
650materialized_refresh_enqueuecreate, update, deleteQueues cross-entity computed-field refreshes. Only when the schema declares refresh triggers
PriorityHookOperationsDoes
260filefieldreadResolves stored file references to URLs. Only when file storage is configured
280computedreadEvaluates virtual computed fields onto scanned rows
340–540pointcut dispatchersreadBeforeFieldDecrypt (340), AfterFieldDecrypt (360), OnFieldMask (420), OnComplianceMask (430), AfterQuery / AfterAggregate (480), AfterCacheWrite (540)

When an operation fails, the dispatchers that observe that failure run with the error bound as ctx.error, and the operation still fails with its own error.

FailurePointcuts
An access rule or gate refused the operationOnAccessDeny
A stateflow guard refused the transitionOnGuardFail
A duplicate key, or a precondition the row no longer metOnInsertConflict (create), OnUpdateConflict (update)
The write’s statement failed and was undoneOnRollback

These keys load without complaint and survive validation, but do not take effect at runtime. Use the alternative given rather than relying on the declaration.

DeclarationUse instead
beforeRead / afterRead / beforePurge / afterPurge in the legacy hooks: block, or a triggers: entry on any event other than before_/after_ create, update and deleteRefused at load (can never fire). Use a pointcut for read and purge behaviour
subscriptions: at entity levelRefused at boot on data.svc. See Change streams for the supported path
The legacy string-filter row-security blockSuperseded by the predicate translator. Declare rls: under access:. See Row-level security
script-file: on any hook, pointcut, trigger or ruleOnly inline bodies execute. Put the script body inline
tx-mode: on a scripted endpointRefused at load. An endpoint is not wrapped in a transaction; write the script to be idempotent. See Scripted endpoints

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" });
KeyTypeDefaultMeaning
methodstring—Canonical method name. Case-sensitive; an unknown value fails the load
namestring—Label used in logs, metrics and error messages
groupstring—Legacy v1 form, backfilled into method when method is absent
languagestringjavascriptEngine for the inline rule: shortcut
rulestring—Inline body, the shortcut form. Used only when script: is absent
scriptmapping—Nested script reference; wins over rule:
orderint0Tie-break among pointcuts on the same method; lower first
asyncboolfalseRun off the request path. Ignored on every Before* method and on OnInsertConflict / OnUpdateConflict / OnGuardFail
codestring—Error code surfaced when this pointcut aborts
tagslist—Observability labels
enabledbooltrueExplicit false skips the pointcut at dispatch

Each method runs only when the event its name describes happens.

MethodPhaseOperationsRuns when
BeforeQueryBeforeValidatereadEvery read
AfterQueryAfterScanreadEvery read
BeforeAggregateBeforeValidatereadA read with an aggregate or a groupby
AfterAggregateAfterScanreadA read with an aggregate or a groupby
BeforeInsertBeforeValidatecreateEvery create
AfterInsertAfterExeccreateEvery create
OnInsertConflicton failurecreateA create meets a duplicate key
BeforeUpdateBeforeValidateupdateEvery update
AfterUpdateAfterExecupdateEvery update
OnUpdateConflicton failureupdateAn update meets a duplicate key or a failed precondition
BeforeUpsertBeforeValidatecreate, updateAn upsert
AfterUpsertAfterExeccreate, updateAn upsert
BeforeDeleteBeforeValidatedeleteEvery delete
AfterDeleteAfterExecdeleteEvery delete
BeforeSoftDeleteBeforeValidatedeleteA delete that is not a purge, on an entity with a deletedon field
AfterSoftDeleteAfterExecdeleteThe same
BeforeCascadeBeforeExecdeleteEvery delete
BeforePurgeBeforeValidatedeleteA purge (?purge=true)
AfterPurgeAfterExecdeleteA purge
BeforeRestoreBeforeValidateupdateA restore
AfterRestoreAfterExecupdateA restore
AfterCommitAfterExeccreate, update, deleteThe write has committed: at once outside a transaction, after the commit inside one
OnRollbackAfterExec, on failurecreate, update, deleteThe transaction the write ran in rolled back, or the write’s statement failed
BeforeTransitionAfterValidateupdateA stateflow transition, after the guard; ctx.from, ctx.to and ctx.event are bound
AfterTransitionAfterExecupdateA stateflow transition
OnGuardFailon failureupdateA stateflow guard refused the transition
OnTerminalAfterExecupdateA transition into a final state
BeforeSCDInsertAfterValidatecreateAn insert into a versioned (type2 / type6) entity
BeforeFieldEncryptAfterValidatecreate, updateOnce for each encrypted or tokenized field the write sets, before it is encrypted
BeforeFieldDecryptAfterScanreadOnce for each encrypted or tokenized field the read returns, before it is decrypted
AfterFieldDecryptAfterScanreadThe same, after it is decrypted
OnFieldMaskAfterScanreadOnce for each field compliance masked, redacted or hid
BeforeCacheReadBeforeExecreadThe entity cache answers or fills the read
AfterCacheWriteAfterScanreadThe read’s rows were written to the entity cache
OnCacheInvalidateAfterExeccreate, update, deleteThe write invalidates the entity’s cache
OnAccessDenyon failureallAn access rule or gate refused the operation
OnRLSFilterAfterValidatereadA row rule for read, the entity’s own or one a layer stacked, narrows the read
OnComplianceMaskAfterScanreadCompliance masked, redacted or hid a field

BeforeBegin, BeforeBulk, AfterBulk, OnBulkChunk, BeforeBulkInsert, AfterBulkInsert, BeforeSCDExpire, AfterSCDClose, BeforeIndex, AfterIndex and BeforeReindex are reserved names: a pointcut declared under one fails the load, naming why and what to use instead. Bulk writes run row by row, so BeforeInsert, BeforeUpdate and BeforeDelete cover each row of a bulk request.

Script returnsEffect
null / nothingContinue
false from a Before* methodAbort with the pointcut’s code:, or POINTCUT_ABORT
{ __abort: true, code, message } from a Before* methodAbort with that code and message
{ __abort: true } from an After* or On* methodRecorded on the hook context; the write has already happened, so the request answers as it was
{ __patch: { field: value } } from a Before* method at BeforeValidate or AfterValidateShallow-merge into the payload
{ __patch: … } from BeforeCascadeRefused with POINTCUT_PATCH_REFUSED: the statement is already compiled
{ __replace: { … } } from OnInsertConflictThe create answers with this row instead of the conflict error
Anything elseIgnored

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.

One binding, named ctx.

KeyPopulated on
ctx.opAlways: create / update / delete / query
ctx.entityAlways. The entity name
ctx.tenantAlways. The full customer:env:product:tenant key
ctx.user.id, ctx.user.rolesAlways
ctx.methodAlways. The pointcut method name
ctx.metadataAlways. The per-request bag shared across hooks
ctx.payloadWhenever a payload exists. A copy
ctx.row, ctx.rowsEvery method that runs once the rows exist: After*, On*Conflict, and the read methods that run after the scan (BeforeFieldDecrypt, OnFieldMask, OnComplianceMask). ctx.row is the first row, ctx.rows all of them
ctx.previousAfter* and On*Conflict on update and delete, from the pre-image hook
ctx.from, ctx.to, ctx.eventThe transition methods: the states and the event
ctx.errorThe methods that run on failure: the operation’s error message
ctx.reasonOnAccessDeny: what the access rule or gate said. OnRLSFilter: the read’s row rules, joined with AND. OnFieldMask and OnComplianceMask: why the values stay masked (unmask was not requested, unmask was not granted, or unmask does not reveal a hidden field)
ctx.fieldName, ctx.fieldThe field-level methods: the field this run is for, and its type, compliance and modifiers
ctx.headersRequests over HTTP: the request headers, keyed by canonical name (ctx.headers["X-Access-Reason"]), repeated values joined with , . Headers that carry credentials are left out: Authorization, Proxy-Authorization, Cookie, and any header whose name contains auth, token, secret, password, apikey, api-key, session, signature, credential or cookie
ctx.bulkChunkEach row of an array create in the operations envelope: { index, total, size, position }. The array is one chunk, so index is 0 and total 1; size is the number of rows and position the row’s place among them, counting from 0
ctx.txWhen the operation is inside a transaction. Opaque to scripts

The field-level methods run once for each field the operation touches: BeforeFieldEncrypt for each encrypted or tokenized field the write sets, BeforeFieldDecrypt and AfterFieldDecrypt for each such field the read returns, and OnFieldMask for each field the read masks, redacts or hides.

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";
KeyMeaning
nameLabel used in the abort message
eventbefore_create / before_update / before_delete: matched case-insensitively
priorityOrder among triggers on the same event; lower first
language, expression, script, script-file, functionThe 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.

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. On data.svc only the languages the deployment offers run. By default these are expr, cel, js / javascript, starlark and wasm; lua runs when the deployment lists it in scripts.runtimes (see Configuration), and go is not available on data.svc. A schema that uses a language not offered is refused when the tenant’s engine is built.

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.

The value of the last expression evaluated is the result. For a guard, return a bool.

Guard results are not coerced: true allows, false denies, and anything else, a string (even "false"), a number, a list, no value at all, fails with rule did not return a boolean. On a request that is 500 with code rule_not_boolean; the operation is refused.

A panic inside a script is recovered and returned as an evaluation error, not a crash.

NamespaceFunctions
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
cept.*customer, product, environment, tenant: identity strings, always present, empty when the host supplies no identity
Rootulid(), uuid(), nanoid(), now(), tsnano(), interpolate()

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.

These are the namespaces a script on data.svc sees, in every language. It has no filesystem, network or process access.

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.

Every script runs under a deadline. A hook, pointcut, trigger, rule or computed-field body that does not return is stopped when its budget passes, and the caller is answered instead of waiting on it.

# boot document
scripts:
timeout: 5s # the default; `none` removes the bound

The budget never extends the caller’s own deadline. A request with 200 ms left gives its scripts 200 ms, whatever the boot document says, so a client that disconnects stops the script it started. A scripted endpoint’s tx-timeout: replaces the budget for that invocation.

What the caller sees depends on where the script sat. A hook, guard or endpoint script that is stopped fails its operation, and a REST caller gets script_timeout. A virtual computed field is an enrichment, so the read still answers: the field comes back null with a compute_failed warning naming the budget.

Each execution also has a memory budget, scripts.memory (64 MiB by default). WASM modules and the V8 JavaScript runtime (javascript-v8) are stopped when they exceed it; scripts in the other languages are bounded by scripts.timeout. See Configuration.

Stopping a script means stopping the interpreter, which JavaScript, Lua and Starlark each support. A language that cannot be interrupted would release the caller on time and say so in the error rather than report a stop it did not perform.

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.

PropertyBehaviour
ScopeOne BEGIN per transactional call, a single-entity POST / PUT / PATCH / DELETE is not wrapped. It is one autocommit statement
Multi-operation envelopeThe multi-entity bulk request body runs every operation in one transaction; any failure rolls back all of them
Bulk updateOpens one transaction and issues one full update per record
IsolationCaller-supplied isolation level, read-only flag, and per-transaction schema. The schema option issues a SET search_path after BEGIN on PostgreSQL
CommitThe callback returning nil commits
RollbackThe callback returning an error rolls back and returns that error
PanicRolls back, then re-raises the original panic
NestingRefused. A transactional call on an already-transactional engine returns exec: nested transaction not supported. There is no savepoint support
No factoryexec: 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.

RequestWhat runs
Operations envelope (POST, PUT, PATCH or DELETE on /rest with operations[])Each operation is one write through the full chain, and all of them run in one transaction: a failure rolls every operation back. An array create runs the full chain once for each row, and each row’s pointcuts read its place from ctx.bulkChunk. Update, delete and restore take one row each
Bulk import (POST /bulk, CSV, JSON or JSONL)Each row is created through the full chain. Rows are read in chunks of ?chunk= rows (1000 by default) and are not written in one transaction: rows created before a failing row stay. With ?skiperrors=true a failing row is skipped and reported, and the import goes on

Every path checks each row against the request’s caller.

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:

  1. 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 an Allow header.
  2. Validate parameters: unknown names rejected, defaults applied, required and enum enforced.
  3. Inject :user_id, :tenant and :roles; force-set {{schema}} and {{datastore}}.
  4. BeforeValidate → AfterValidate → render SQL → BeforeExec → execute → AfterExec → AfterScan when rows were returned.

Note the order at the end: on the action path AfterExec runs before AfterScan.

Sharp edgeDetail
{{schema}} and {{datastore}} are never caller-overridableIdentifiers 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 stringSo role IN (:roles) is portable. Split on the comma in SQL for real array semantics
Parameter type coercion is permissiveOnly the enum check is strict; the raw value passes through. Do not rely on the declared type for sanitisation

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 (there is no tx-mode:; the key is refused at load), and its tx-timeout: bounds only what the script asks the engine for, see Timeouts and limits.

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.

MessageRaised byHTTP
hook <name> (<Phase>): <cause>Wrapper applied to every hook failure. The form the log shows. The API sends <cause> as the message and puts hook and phase in detailsfrom the cause
access denied (tier <n>): <reason>The access hook refused the operation403
the rule’s own message:Field or entity validation refused the write422
invalid transition on <entity>.<field>: <reason>The stateflow refused the transition422
trigger "<name>": operation aborted by triggerA before_* trigger returned false; a validation failure with code TRIGGER_ABORTED422
the pointcut’s own messageA Before* pointcut aborted: returned false or { __abort: true, … }. details.code is the abort’s code (else the pointcut’s code:, else POINTCUT_ABORT), details.pointcut its name, details.method the method422
pointcut <Method>/<Name>: <cause>A synchronous pointcut script failed; details.code is the pointcut’s code: or POINTCUT_ERR, details.cause the script’s failure500
exec: entity not foundNo rows matched, or the named action is not registered404
exec: forbiddenThe database refused the statement: insufficient privilege403
exec: validation failedThe database refused the statement: not-null or check violation422
exec: conflictDuplicate key or foreign-key violation409
exec: engine closedAn operation arrived after shutdown began500
exec: nested transaction not supportedA transaction was opened inside a transaction500
exec: no backend configuredNo transaction factory is wired500
exec: no compiler availableNo SQL compiler for the backend500
POINTCUT_ABORTDefault code for a Before* pointcut returning false or __abort without a code—

The exec: and hook … (…): prefixes in this table are the engine’s own and never reach a client. The API’s message is the innermost cause; what the engine knows about where it came from, hook and phase, the validation rule and its code, the database’s SQLSTATE and object names, travels as details. See error codes. | 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 |

  • Payload edits stop mattering after AfterValidate.
  • AfterScan does not run on entity writes, so a created row is echoed back unmasked and without virtual computed fields.
  • after_* triggers run after the write; events: publishes to the change stream a subscriptions: block serves; transforms: and field encryption load and do not run.
  • The beforeRead, afterRead, beforePurge and afterPurge slots of the legacy hooks: block are refused at load: no phase would fire them.
  • A trigger cannot modify the payload; a pointcut must return __patch to do so.
  • The write-time computed-field hook runs one phase too late for its value to reach the column.
  • async: true is ignored on every Before* 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.
  • A data.* call from a script re-enters the chain as the caller: the same user, roles and tenant, the same gates.
  • Transactions do not nest, and a single-record write over HTTP is not in one.
ConcernPage
The four access tiers and their rule vocabularyAccess rules
Rule-to-WHERE translation and its failure modeRow-level security
Declarative field and entity rulesValidations
State machines, guards and transition scriptsStateflows
Computed fields, virtual and materializedFields
History strategies and what each one writesEntities
Query-string flags the bridge hook readsRequest flags
Custom HTTP routes implemented in scriptScripted endpoints
The published pointcut interfacesPointcuts
Entities served over HTTP instead of SQLRemote entities
Endpoint-level HTTP referenceData API reference