docsLanguage

MemQL Reserved Names

This document is the single index of every identifier MemQL reserves in the author surface. Field names, arg names, imported names, and function names that collide with one of these are rejected at load time.

Authoring against this list is much cheaper than re-running the engine to find out which name was reserved.


1. Engine-provided names (top-level)

These are available inside every body and cannot be redeclared as local args / fields:

NameWhatWhere
argsCaller-passed arguments object. Read fields as args.X.every body
actorThe auth envelope. Closed member set (#2623, section 3 below): userId, role, identityId, isClusterOwner, primaryEmail, now, plus the legacy isOwner alias. Reading it requires @actor (#2621).every body
nowRFC3339 timestamp captured at evaluation start.every body
partitionActive partition for this call.every body
configAllow-listed config entries. See component/config/policy_exposable.go for the surface. Read as config.X.every body
traceReserved engine name. (It carried the policy-trace handle of the decision-policy tier, retired in #984; the name stays reserved so resolution rules remain unambiguous.) Nothing binds it, so reading it is refused (body_unknown_name).every body
eventThe triggering event (event.topic, event.kind). An automation forwards it whole to a logic as logic l(event: event); a dotted event.<field> read in an automation is refused, the payload being bound into its args. A logic has no trigger: it declares event in its args and reads args.event.an automation body

An args field whose name collides with any of these is a load-time error. Defined in component/memql/keyword_slices.go and enforced during args parsing.

These names, plus row, meta, and payload, are also the reserved filter heads: in the engine's internal query form (the string an SDK sends to Execute), a bare path rooted at one of them reads an engine namespace instead of the bound concept's payload. That makes them reserved on a concept's payload schema too -- see section 2. An authored expression never reads a payload field bare: a filter reads row.status through its lambda parameter, and a bare name is a lambda parameter, a reserved root, a local, or a function or predicate being called (see Expressions).


2. Row intrinsics (concept rows)

Every persistent row has these engine-stamped fields. They are reserved on every concept's payload schema -- redeclaring one in a concept body is a hard error. They are also the canonical names the executor reads at SQL push-down time.

FieldTypeStamped byNotes
idstringmutation executionCanonical id format: {concept}:{shortId} (the {partition}: prefix was retired in #56). Canonical internally; bare-ified toward clients. See identifiers.md.
conceptstringmutation executionThe concept id, e.g. v1:library:artifact.
typestringconcept declarationCurrently mirrors concept; reserved for future versioning differences.
createdAtdatetimemutation executionRFC3339 timestamp at insert time.
createdBystringmutation executionStamped from the request actor's identity.
schemastringconcept declarationConcept schema version hash (engine-derived).
provenanceobjectmutation executionOrigin metadata: {kind, name, trigger, via}. Supports nested paths.

partition is reserved on the payload schema too, but it is not a stamped row intrinsic: partitioning was retired in #56, "MemoryNodes" has no partition column, and the name stays reserved purely as a retired-name guard so a concept cannot reintroduce a field implying a tenancy dimension the engine no longer has (see gotcha #12 and component/database/memory-nodes/constants.go).

Defined in component/memql/intrinsic_fields.go. Cross-referenced in docs/public/language/authoring-rules.md (gotcha #19).

The engine namespaces are reserved on the payload schema too

The names in section 1 are equally unavailable as concept properties:

FieldWhy a concept may not declare it
argsCaller-arg namespace at the head of any path.
actorAuth-envelope namespace.
nowEngine timestamp.
configAllow-listed config namespace.
traceReserved engine name.
metaEngine namespace at a filter head.
rowThe row-intrinsic namespace.

The list a concept is checked against (memoryNodes.reservedPayloadFields) is a superset of the reserved filter heads (component/memql's reservedFilterHead), pinned by TestReservedPayloadFieldsSupersetOfFilterHeads. Before memql#3613 it was not, and each of the eight missing names was declarable: the concept registered with the field intact, no skip and no warning, while every filter naming it bare read the namespace instead. provenance was fully silent -- the SQL push-down compiled clean and the in-process post-filter agreed on the same wrong field, so the query returned the wrong rows forever. actor was silent and authorization-relevant: filter actor.userId == args.v compiled to ? = ? over the caller's own envelope and a caller-supplied argument, const-folding to true whenever a caller passed their own id -- the predicate contributed nothing and the query returned every row of the concept.

The gate is case-insensitive, matching how a filter head is classified, so Provenance and ACTOR are refused as well. Only whole names are reserved: arguments, metadata, configuration, and rowCount are ordinary properties.

Naming an intrinsic in a filter

A filter, spec or trait reads every field through its lambda parameter. row.id, row.createdAt and row.provenance.kind are intrinsics, and row.archived is a payload property: the intrinsic names are reserved on every payload schema, so row.<name> reads the intrinsic when the name is one and the payload property otherwise, and the two can never collide.

memql
filter row => row.id == args.folderId && row.archived != true
// ^^^^^^ intrinsic ^^^^^^^^^^^^ payload property

The intrinsics a filter pushes down are id, concept, type, createdAt, createdBy and provenance.<leaf>. schema is a real stamped column but is not filter-comparable. partition is reserved on the payload schema (see section 1 above) but, post-#56, is not a row intrinsic at all -- there is no partition column to compare.

Before edition 2026 a filter named payload properties bare and intrinsics through a row. namespace (memql#2779), because a bare id could not be told apart from a payload property of the same name. The lambda parameter removed the bare form, and with it the second spelling.

A sort key takes the same namespace -- sort "row.createdAt", "desc" -- and rejects a non-sortable leaf rather than silently ordering on a JSONB path that does not exist. Authored sort keys are gated exactly like filter predicates (memql#2786): the tree is migrated, and naming an intrinsic bare in a .memql sort clause fails CI. provenance has no sort form -- it is object-valued with no ordering, so row.provenance is rejected.

A sort key rooted at a reserved head is refused outright (memql#3613), at runtime as well as in authored .memql. The same bare token used to mean the row envelope in a filter and the payload in a sort key -- filter provenance.kind == x compiled to provenance->>'kind' while sort "provenance.kind" compiled to payload #>> '{provenance,kind}'. No concept may declare provenance, so that path exists on no stored row and the ordering was a guaranteed no-op. The explicit payload.<path> form is untouched: its prefix names the surface, so there is nothing left ambiguous.

Otherwise the bare spelling remains valid at RUNTIME, where callers pass sort keys in through the SDK and the query API; the .memql gate covers authored sort clauses only.

Other surfaces: a shape body projects row.id / row.createdAt; a spec or trait body reads through its lambda parameter exactly as a filter does; a mutation insert/update block writes id: / createdAt: as target keys rather than references.


3. Actor-envelope fields

Inside an @actor shape or a context-spec, the engine exposes a fixed envelope. Field paths under actor. are restricted to:

PathWhat
actor.userIdThe acting user's id.
actor.roleCluster role: owner / admin / developer / writer / reader.
actor.identityIdThe credential row (token, magic-link, PAT).
actor.isClusterOwnerBool short-circuit; bypasses the per-partition ACL.
actor.primaryEmailThe acting user's primary email address.
actor.nowRFC3339 timestamp at evaluation start (the shape-body spelling of the reserved now).
actor.isOwnerLegacy alias of actor.isClusterOwner.

This is the ONE canonical envelope (#2623), uniform across query filters, mutation values, logic bodies, and context-specs; the engine-side table is auth.ActorEnvelopeFields and the dslspec editor table is drift-pinned to it. actor.config.<key> is retired from this list -- no resolver ever implemented it; read allow-listed config through the bare reserved config.<key> instead.

The fields are the only names valid under actor. -- a typo like actor.userid (lowercase) is a hard error AT LOAD (#2625: it was a runtime error on the query/mutation surfaces and a SILENT NIL on the logic/spec surfaces before), and an actor-unknown-property squiggle at edit time. The caller.X / @caller spellings are retired in #221; the parser rejects them with a migration hint pointing at the canonical actor.X / @actor.

Reading the envelope is a DECLARED capability (#2621): a query, mutation, logic, or automation whose body references actor.* must carry a bare @actor annotation in its preamble, or it fails load with a file-attributed error (used-requires-declared, the same shape as the logic event-binding rule). Declared-but-unused is legal. Spec and trait bodies do not take @actor: a spec reads the envelope by binding an @actor shape, and its lambda parameter is then spelled actor and is the envelope -- spec actorEnvelope requiresOwner = actor => actor.role == "owner". The seed-file @actor("system") (a seed-write identity) is a different construct and is unaffected.


4. Imported names (use declarations)

Cross-file dependencies are declared with file-top use imports:

memql
use library.concepts.{ artifact, folder }
use common.traits.{ isActiveRecord }

The dotted path maps to a file on disk (library.concepts → dsl/library/concepts.memql); the brace list names the constructs pulled into local scope under their declared names -- there is no aliasing clause. Every imported name shares the same identifier namespace as the top-level engine-provided names in section 1, so the five reserved names (actor, now, partition, config, trace) can never be construct names and can never enter scope through an import.

Resolution lives in component/memql/dslimports/. (An earlier design sketch for path-based import "./foo" as <name> aliasing was superseded by the use model that shipped with the import-model pivot, memql PRs #47 / #48 / #49.)


5. Reserved keywords (declaration headers)

Each construct's declaration header uses a reserved keyword. These cannot be used as identifier names anywhere in the author surface:

KeywordConstruct
conceptRow schema declaration.
shapeReusable field projection.
specAtomic boolean predicate.
traitConcept-agnostic atomic predicate.
queryRead function.
mutationWrite function, declared mutation <Concept> <name> { ... } and called mutation <name>(...).
logicImperative orchestration block.
automationEvent-triggered workflow.
toolAI-callable surface.
promptAI prompt template.
providerAI vendor + model config.
builtinGo-backed operation wrapper.
seedDeclarative row template.
policyAI provider-selection record (@primary / @fallback). The decision-policy tier is retired (#984).

Plus body-level keywords inside specific constructs: args, filter, refine, sort, paginate, count, shape, insert, update, accept, stamp, precondition, params, auth. Their reservation is scoped to the construct that defines them. In a logic and an automation, a statement opens with if, for, switch (case, default), parallel (branch), publish or return, or with a name and :=, and a call closes with the clauses on surface(...), retry(n) and on error continue (Bodies). No statement or loop variable may take a reserved name above (body_reserved_name).

The expression keywords in and startsWith (memql#4208) are reserved wherever an expression is parsed, so neither can name a lambda parameter or a local. when, has and not are still lexer keywords, and in an authored expression the parser refuses each by name with its replacement: when(args.x) { ... } is written (args.x == nil || <predicate>), x has v is v in x, and x not in list is !(x in list).


6. Reserved annotation names

The full annotation surface is per-construct and enforced at parse time by the annotation registry (component/language/annotations, memql#5359), the one check every construct parser and field list runs; a refusal names the construct, the annotation and what to write, and ends with a stable annotation_* code. Cross-construct annotations:

AnnotationWhere it applies
@description("...")Every construct.
@enabled / @disabledLifecycle on queries / mutations / logic / automations / traits / tools / builtins / prompts / providers / specs / seeds. Enabled is the default on every kind (#2604-#2608); @enabled is an accepted no-op. @disabled means "not loaded right now", not "deprecated" -- that axis is @deprecated. @disabled on a @base provider propagates to every child that @extends it.
@deprecated("hint")Removed (#989): no registry placement lists it, so it is refused everywhere (annotation_unknown, memql#5359) -- at parse time on every construct and field list, and at load on a concept, its body and its fields, which the concept translator checks. Use @disabled to deactivate.
@internalRETIRED at construct level under the 2026.08 epoch (#2620 ruling / #2708) -- it only hid a callable construct from discovery surfaces; the annotation registry refuses it with a migration hint (annotation_retired, memql#5359) on every construct and on the args, tool, prompt and builtin field lists -- at parse time on those, and at load on a concept. Field-level @internal on concept properties remains live -- a concept field is the one place the registry accepts it -- and is enforced (rejected from caller args + excluded from a shape's default projection). Its neighbours are not uniform: @pii is enforced (@scrubPii + the memql#2883 projection gate), while @secret is enforced on every validation surface that quotes a rejected value -- and is still not a secrecy guarantee. The list comes from an EXHAUSTIVE enumeration of validators, not from appending one entry at a time: three incremental passes each walked past a surface the next found, and the exhaustive sweep turned up two more the original four-surface model did not contain. It redacts in the function-args validator (memql#3036); the tool-args validator (validateToolArgs), compiled from the same args schema and running BEFORE the function-args validator on the agent path, in both the message returned to the model and the WARN, which now redacts per key instead of serializing the entire args map (memql#3182); the automation args binder, in all three refusal messages and in the WARN log they are written to, closing the one path by which a concept row value could reach a structured log (memql#3183); concept payload validation, where @minimum / @maximum / @format declared on the concept are actually enforced, across all six jsonschema keywords that interpolate the instance value (memql#3184); and the DSL-callable validate / preflight builtins, which surface leaf messages in a result payload returned to the caller. The last two resolve secrecy through the schema at the failing instance location, so they cover nested and inherited @secret -- unlike SecretFields(), which is top-level only. On the two args-validator surfaces matching is by argument NAME rather than by write target (a mutation writing apiKey: args.credential leaves credential unredacted). It does NOT redact from query results (an authorization decision deferred under memql#2803). Length is never redacted anywhere, deliberately and uniformly (value too long and the jsonschema minLength/maxLength messages report a rune count for a secret field too). Prompt input-schema validation (PromptTemplate.ValidateData) is UNCLASSIFIED: it interpolates the instance the same way but its schema is not concept-derived, so there is no x-secret to read -- treat it as uncovered, not as safe.

Cross-construct dependencies do NOT go through annotations. The legacy @useConcept / @useShape / @useQuery / @useMutation / @useLogic / @useBuiltin / @useSpec / @useTrait / @useTool / @usePrompt / @useProvider / @useAutomation family was retired in the import-model pivot (memql PRs #47 / #48 / #49, 2026-05-19) and is rejected at parse time. The canonical post-migration shape:

  • File-top Form B imports declare cross-file dependencies: use library.concepts.{ artifact }, use common.traits.{ isActiveRecord }.
  • Concept binding lives in the construct signature for seeds / queries / mutations / shapes: query <Concept> <name> { ... }, mutation <Concept> <name> { ... }, shape <Concept> <name> { ... }, seed <Concept> <name> { ... }.

Inside a prompt's body, two legacy forms are refused with a migration hint: the @input { ... } wrapper (declare the fields directly in the body) and an inline @template(...) (use @templateFile("...") with a .tmpl file beside the prompt). They are forms of the prompt body, not annotations, so the attribute matrix does not list them. @template written before an automation is a live annotation: it marks a work-spine template.

The attribute matrix, generated from the registry, lists every annotation, the constructs and fields that accept it, the form it is written in, and every retired annotation with what to write instead.


7. Where this list lives in code

This file is documentation; the source of truth is split across several Go files. When the list below changes, update this doc:

ItemSource
Top-level engine namescomponent/memql/keyword_slices.go
Row intrinsicscomponent/memql/intrinsic_fields.go
Caller envelopecomponent/memql/sense/builtins.go + runtime_evaluator.go
Construct keywordsparser.TopLevelDeclKeywords and parser.StructFormKeywords in component/language/parser, projected by component/language/dslspec
Annotationsthe registry in component/language/annotations; the attribute matrix is generated from it (make docs-matrix)
Imported names (use)component/memql/dslimports/dslimports.go

Why a doc index instead of just code?

Because authors write .memql files in editors first and read engine errors second. The fast way to write a correct file is to know the rule before it bites; the slow way is to fix the file after the engine boots and rejects it. This doc closes that gap.