ObjectStackObjectStack

System Context (isSystem)

The authoritative table of every platform behaviour keyed off `ExecutionContext.isSystem` — what an elevated write gets, what it loses, and what the flag deliberately does NOT do. Built by census over the whole repo, not by recall.

System Context (isSystem)

ExecutionContext.isSystem is the platform's one elevation flag. Setting it on a write or read means "this operation is the engine acting on its own behalf" — the seed loader replaying package fixtures, a plugin's boot reconciler, a service self-write, a migration.

This page is the authority for what that flag actually does. It exists because the flag is not one concept: it is a single boolean read at 80 distinct sites across 18 packages, and knowing three of those behaviours gives no hint that the other seventy-seven exist. Every documented app-side bug traced to isSystem had the same shape — the metadata was complete and correct, and the gap was observable only by querying the resulting rows.

Elevation is total, and it is not granular. isSystem is not "skip the permission check". It short-circuits authorization, ownership stamping, read-only protection, referential-integrity checks, sharing materialisation, approval locks and provenance stamping — in eighteen packages that do not know about each other. Read the table before you set it; prefer a scoped user context whenever one exists.

Which isSystem this page is about

Four unrelated declarations share the identifier. This page documents only the first. The others are ordinary metadata fields on a stored document and have nothing to do with elevation.

DeclarationWhat it isThis page?
ExecutionContext.isSystempackages/spec/src/kernel/execution-context.zod.ts:233The elevation flag on an operation's context
Object.isSystempackages/spec/src/data/object.zod.ts:1249Marks a system object (protected from deletion; defaults its org-wide sharing to public when no sharingModel is set)
EmailTemplate.isSystempackages/spec/src/system/email-template.zod.ts:125Built-in template; tenants may override but should not delete
Environment.isSystempackages/spec/src/cloud/environment.zod.ts:136Platform-infrastructure environment, not user data

The collision is a genuine hazard rather than a naming nit: Object.isSystem changes an object's default sharing, and ExecutionContext.isSystem changes whether sharing grants are materialised — so a search for "isSystem sharing" returns both, and they are unrelated decisions.

A fifth, closely-spelled family — isSystemObjectName() / isSystemObject() in packages/runtime/src/action-execution.ts:53, packages/mcp/src/mcp-http-tools.ts:178 — keys on the sys_ name prefix, not on any flag.

How the flag is set

isSystem is server-constructed and never client-supplied. Inbound HTTP cannot set it (packages/rest/src/rest-server.ts:2079, :2096), and neither can an action body (packages/runtime/src/domains/actions.ts:122). It is written by internal callers only, as an option on the engine call:

await engine.insert('crm_account', row, { context: { isSystem: true } });

Its parse-time default is false (execution-context.zod.ts:233), so an absent context is never elevated.


The table

Grouped by lane. Every row cites the site that reads the flag. "What you lose" is the part that costs app-side bugs — it is the protection or the side effect that silently does not happen.

1. Authorization and scoping

#Behaviour when isSystemPackageWhat you get / what you loseAnchor
1The whole security middleware short-circuits before any gate runsplugin-securityGet: every CRUD/FLS/tenant/owner gate below skipped in one branch. Lose: all of rows 2–6 at once — this is the single largest behaviour on the pagesecurity-plugin.ts:825
2owner_id is not auto-stamped on INSERT (the step 3.5 anchor guard is inside the block row 1 skips)plugin-securityLose: the row lands owner_id = NULL, so the default owner_only_writes policy hides it from its own creator. Get: nothing — this is a gap, not a capabilityguard at security-plugin.ts:14661560, skipped by :825
3Row-level read filter resolves to "no filter"plugin-securityGet: unscoped reads. Lose: row-level scoping entirelysecurity-plugin.ts:2747
4Field-level security returns all fieldsplugin-securityGet: every column readable. Lose: field maskingsecurity-plugin.ts:2898
5Export permission granted unconditionallyplugin-securityGet: canExport is truesecurity-plugin.ts:2964
6Write bypass = true, effective write scope = orgplugin-securityGet: widest write scope without holding any capabilitysecurity-plugin.ts:705, :727
7Metadata-plane schema masking exempt (ADR-0106 D4)metadata-coreGet: unmasked object schema. Note: the exemption is a caller property — it short-circuits before the security service is consultedobject-schema-fls.ts:167
8explain() may target a principal other than the callerplugin-securityGet: no manage_users / delegated-admin checksecurity-plugin.ts:2321
9Anonymous-deny treats the caller as authenticatedcoreGet: passes the 401 seam with no userIdanonymous-deny.ts:114
10Permission-set projection middleware skippedplugin-securityLose: projection of permission-set-derived columnspermission-set-projection.ts:670
11Session-resolution middleware skippedplugin-authGet: no session lookup attemptedauth-plugin.ts:1023
12Per-request performance timings disclosedobservabilityGet: timing headers a normal caller cannot pullperf-timing.ts:474

2. Write pipeline and data integrity

#Behaviour when isSystemPackageWhat you get / what you loseAnchor
13readonly strip bypassed — UPDATE, single rowobjectqlGet: a readonly field CAN be written. Lose: the protection that stops a caller seeding e.g. approval_statusengine.ts:6860
14readonly strip bypassed — UPDATE, bulk/predicateobjectqlSame, on the multi-row pathengine.ts:7004
15readonly strip bypassed — INSERT (engine pass)objectqlSame, on createengine.ts:6078
16readonly strip bypassed — INSERT (protocol ingress)metadata-protocolisSystem is the only exemption here. preserveAudit is deliberately not read on this path (#6640) — a non-system historical import is still stripped on createprotocol.ts:1114
17Strict-drop refusal never firesobjectqlLose: a caller that opted into loud refusal gets silence — strict refuses exactly what the strip would have taken, and the strip took nothingengine.ts:6098, readonly-strict-errors.ts:44
18Referential-integrity check skippedobjectqlGet: writes proceed against unreachable/unresolvable targets. Lose: an isSystem caller can write a dangling referenceengine.ts:3314
19Tenant-audit warning silenced; bypassTenantAudit threaded to the driverobjectqlGet: unscoped system writes stop warning. Lose: the signal that would flag a genuine user-path scoping bugengine.ts:2073, :2075, :2102
20Engine-owned / append-only write guard bypassedplugin-securityGet: generic writes to managedBy engine-owned objectssystem-write-guard.ts:96, :120
21Identity write guard bypassed (ADR-0092)plugin-authGet: direct writes to identity tables through the generic data pathidentity-write-guard.ts:98

3. Sharing (plugin-sharing)

The largest single consumer — 19 of the 80 sites.

#Behaviour when isSystemWhat you get / what you loseAnchor
22Sharing-rule grant materialisation is skipped on all four record-write hooksLose: no sys_record_share rows are created. A fully configured sharing rule grants nothing on seeded data until a rule is re-evaluated or the boot backfill runs. This is the behaviour that motivated #4707rule-hooks.ts:157, :165, :180, :194
23Sharing write verdict short-circuits to allowGet: writes pass the sharing gate unconditionallysharing-service.ts:438
24Record visibility / manage-shares checks return trueGet: no ownership or Modify-All requirementsharing-service.ts:635, :722, :1195
25grant() skips the enforcement + manage-shares assertionsGet: the rule evaluator can materialise through the public APIsharing-service.ts:808
26revoke() deletes directly, before the non-manual-source guardGet: the evaluator can revoke its own grants. Lose: the CONFLICT guard that warns a rule-materialised grant will be silently re-granted on the next reconcilesharing-service.ts:884 (guard at :908)
27listShares() skips the management gateGet: full enumeration of who can see a recordsharing-service.ts:936
28sys_record_share reads are not self-scopedGet: tenant-wide share listing without manage_sharingsharing-plugin.ts:839
29Share-link policy enabled check bypassed; system callers re-enter under a system contextGet: link creation/resolution while the policy is offshare-link-service.ts:264, :312, :316, :374, :404
30Sharing-rule provenance stamp skippedLose: the row is not marked as an admin customization — seeder / defineRule / boot reconcilers are "the package door"sharing-rule-provenance.ts:50
31Sharing-rule service write path returns earlyLose: the same provenance/gating step on the service surfacesharing-rule-service.ts:99

4. Approvals, reports, attachments, comments, knowledge

#Behaviour when isSystemPackageWhat you get / what you loseAnchor
32Approval record lock released — a locked record is writableplugin-approvalsGet: engine self-writes (the status mirror) pass. Lose: the lock that stops edits while an approval is live. Note there is deliberately no admin exemption here — only isSystemlifecycle-hooks.ts:325
33Delegation write guard bypassedplugin-approvalsGet: service / seed / import may write delegation rows naming another delegatorlifecycle-hooks.ts:432
34Approval actor / submitter / pending-approver checks bypassed (7 sites)plugin-approvalsGet: approve, reject, recall, reassign without being a pending approver or the submitterapproval-service.ts:658, :713, :2257, :2403, :2570, :2641, :2830, :2870
35Saved-report ownership is assignable, and an update may reassign itplugin-reportsGet: ownerId from input is honoured. A non-system caller always owns what it creates and can never reassignreport-service.ts:334, :355
36Saved-report access / mutation gates bypassedplugin-reportsGet: read and overwrite any reportreport-service.ts:273, :302, :377, :567
37Attachment access hooks return early (write + read AST)service-storageLose: attachment visibility scopingattachment-access-hooks.ts:95, :144, :276
38Comment access hooks return early (write + read AST)plugin-auditLose: comment visibility scopingcomment-access-hooks.ts:241, :346, :378, :423
39Knowledge search returns hits unfilteredservice-knowledgeLose: the permission filter over search resultsknowledge-service.ts:308

5. Actions, metadata plane, provenance

#Behaviour when isSystemPackageWhat you get / what you loseAnchor
40Object API-exposure gate bypassed (apiEnabled / apiMethods)runtimeGet: internal self-writes ignore exposure declarations — these govern external exposure, not engine self-writesaction-execution.ts:125
41Action requiredPermissions bypassedruntimeGet: engine self-invocation runs any actionaction-execution.ts:388
42manage_metadata bypassed on metadata writesruntime, restGet: schema writes without the capabilitydomains/meta.ts:468, rest-server.ts:4047
43Anonymous-deny seam satisfied on the domain dispatchersruntimeGet: passes with no userIddomains/actions.ts:129, domains/ai.ts:128, domains/automation.ts:151, domains/meta.ts:171, domains/security.ts:92
44MCP principal check satisfiedruntimeGet: MCP surface reachable with no userdomains/mcp.ts:60
45Audience-binding suggestion recording skippedplugin-securityLose: install-time suggestions are not recorded for system callerssuggested-audience-bindings.ts:262
46Email-template / webhook provenance stamps skippedplugin-email, plugin-webhooksLose: the row is not marked as an admin customizationemail-template-provenance.ts:59, webhook-provenance.ts:50
47Automation flow data nodes re-add the owner_id stamp (the one place row 2's gap is compensated inline)service-automationGet: a flow-authored INSERT under system elevation still lands owned, when the run resolved a user. Fill-only — flow-authored values winruntime-identity.ts:279, called from builtin/crud-nodes.ts:309

What isSystem does not do

Just as costly as the list above. Each of these is a separate switch, and assuming isSystem covers it is a documented source of bugs.

AssumptionRealityAnchor
"It suppresses triggers / record-change automation"No. Only skipTriggers does. A bare { isSystem: true } on a seed write re-fired automation on freshly seeded rows and wedged first bootseed-loader.ts:13101313 (#3760), flow.zod.ts:630
"It skips the state machine"No. That is skipStateMachine, carried by seed replay and by treatAsHistorical importsengine.ts FSM gate; see State Machine
"It skips validation rules"No. Field shape, format, script and the rest still run. The readonly strip runs before validation precisely so a discarded value is not judgedengine.ts:60606078
"It preserves a supplied updated_at / updated_by"No. That is preserveAudit, a separate opt-in — and an UPDATE-path exemption onlyfield.zod.ts:820 (#3493 / #6640)
"It stamps created_by"No. Audit stamping reads userId from the context. A user-less system write stamps nothing — that is today's behaviour, not an errorruntime-identity.ts:268272
"It bypasses every guard"No. The last-admin guard applies to every context, isSystem included — the deprovision path that actually locks an org out is the system onelast-admin-guard.ts:247
"A client can request it"No. Never settable from inbound HTTP or from an action bodyrest-server.ts:2079, :2096; domains/actions.ts:122

Known rough edges

Recorded rather than smoothed over, because a reader who hits one of these should recognise it instead of re-deriving it.

  1. The owner_id gap has two independent compensations and no shared mechanism. Row 2 is a real gap; the platform repairs it twice, in unrelated places — inline for automation flow writes (runtime-identity.ts:279, whose own comment states the reason: "the security middleware that stamps it short-circuits on isSystem — so the writer fills it here"), and as a boot-time sweep for seeded rows (plugin-security/src/claim-seed-ownership.ts). Any third system write path gets neither. If you add one, stamp ownership yourself.

  2. Sharing materialisation is skipped silently. Row 22 produces "configured but inert": nine installed sharing rules, matching records, correct positions — and sys_record_share empty, with nothing logged. The boot backfill does eventually fix it, so the behaviour is not wrong; it is undiscoverable. An INFO line for exactly this case is queued as #6783 and is not shipped at the time of writing — do not read this row as already observable.

  3. Strict write observability is inert under elevation. Row 17: a caller that asked to be told loudly about dropped fields is told nothing, because nothing was dropped. The two facts are indistinguishable from the outside.

  4. revoke() skips its own conflict guard. Row 26 is correct for the rule evaluator and surprising for anything else: a system caller can delete a rule-materialised grant that the next reconcile silently restores.

  5. applySystemFields does not read this flag. It is named as if it did. packages/objectql/src/registry.ts:307 is schema-side column provisioning — which columns an object carries — and consumes ExecutionContext.isSystem zero times. The write-time ownership behaviour people attribute to it is row 2, in plugin-security.


Decision on record: the flag is deliberately not being split

Maintainer ruling, #4707, 2026-08-06. Recorded here at the ruling's own request, so the proposal stops being re-opened.

Ownership injection, readonly bypass and sharing materialisation are independent decisions, and a seed loader plausibly wants the first two but not the third. The concept is nevertheless staying as one boolean:

  • Shipped semantics. isSystem is a published contract with 80 read sites in 18 packages. Splitting it is a breaking contract change across all of them.
  • No business pull. No app has asked for the combinations a split would enable; the observed need was to understand the flag, which is what this page serves.
  • Combinatorics are worse for AI authors, not better. Three independent switches are eight states, most of them untested and several of them incoherent (grant materialisation without ownership). One flag plus this table is judged more mistake-proof at authoring time than a surface where a wrong combination is expressible and silently valid.

The trade-off accepted with that ruling is that elevation stays coarse: you cannot ask for the ownership behaviour without also taking the sharing behaviour. Where a narrower need exists, the platform answers it with a separate, explicit option next to isSystemskipTriggers, preserveAudit, skipStateMachine, runAs — rather than by subdividing the flag. That is the pattern to follow for any new narrow exemption.


Maintaining this table

The table's value is exhaustiveness, so it is built by census, not by recall. To re-verify after a change:

grep -rn "isSystem" --include="*.ts" --include="*.tsx" packages examples \
  | grep -v node_modules | grep -v "/dist/"

Classify each hit into: a consumer of ExecutionContext.isSystem (a table row), a producer (isSystem: true on a call — not a behaviour), one of the three unrelated metadata fields, a sys_-prefix name helper, or a declaration. As of this page's census on main: 1179 total occurrences — 588 in tests, 591 in sources; of the source occurrences, 16 declarations, 240 producers and 121 consumers. Of the 121 consumers, 80 are behaviour-bearing reads of the elevation flag (the rows above), 12 read one of the unrelated metadata isSystem fields, 11 are sys_-prefix name helpers, 5 only propagate the flag onward, and 13 are generated i18n, form declarations or schema prose.

A new read of ExecutionContext.isSystem belongs in this table in the same PR that introduces it.

On this page