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:
| Name | What | Where |
|---|---|---|
args | Caller-passed arguments object. Read fields as args.X. | every body |
actor | The 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 |
now | RFC3339 timestamp captured at evaluation start. | every body |
partition | Active partition for this call. | every body |
config | Allow-listed config entries. See component/config/policy_exposable.go for the surface. Read as config.X. | every body |
trace | Reserved 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 |
event | The 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.
| Field | Type | Stamped by | Notes |
|---|---|---|---|
id | string | mutation execution | Canonical id format: {concept}:{shortId} (the {partition}: prefix was retired in #56). Canonical internally; bare-ified toward clients. See identifiers.md. |
concept | string | mutation execution | The concept id, e.g. v1:library:artifact. |
type | string | concept declaration | Currently mirrors concept; reserved for future versioning differences. |
createdAt | datetime | mutation execution | RFC3339 timestamp at insert time. |
createdBy | string | mutation execution | Stamped from the request actor's identity. |
schema | string | concept declaration | Concept schema version hash (engine-derived). |
provenance | object | mutation execution | Origin 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:
| Field | Why a concept may not declare it |
|---|---|
args | Caller-arg namespace at the head of any path. |
actor | Auth-envelope namespace. |
now | Engine timestamp. |
config | Allow-listed config namespace. |
trace | Reserved engine name. |
meta | Engine namespace at a filter head. |
row | The 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.
filter row => row.id == args.folderId && row.archived != true // ^^^^^^ intrinsic ^^^^^^^^^^^^ payload propertyThe 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:
| Path | What |
|---|---|
actor.userId | The acting user's id. |
actor.role | Cluster role: owner / admin / developer / writer / reader. |
actor.identityId | The credential row (token, magic-link, PAT). |
actor.isClusterOwner | Bool short-circuit; bypasses the per-partition ACL. |
actor.primaryEmail | The acting user's primary email address. |
actor.now | RFC3339 timestamp at evaluation start (the shape-body spelling of the reserved now). |
actor.isOwner | Legacy 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:
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:
| Keyword | Construct |
|---|---|
concept | Row schema declaration. |
shape | Reusable field projection. |
spec | Atomic boolean predicate. |
trait | Concept-agnostic atomic predicate. |
query | Read function. |
mutation | Write function, declared mutation <Concept> <name> { ... } and called mutation <name>(...). |
logic | Imperative orchestration block. |
automation | Event-triggered workflow. |
tool | AI-callable surface. |
prompt | AI prompt template. |
provider | AI vendor + model config. |
builtin | Go-backed operation wrapper. |
seed | Declarative row template. |
policy | AI 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:
| Annotation | Where it applies |
|---|---|
@description("...") | Every construct. |
@enabled / @disabled | Lifecycle 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. |
@internal | RETIRED 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:
| Item | Source |
|---|---|
| Top-level engine names | component/memql/keyword_slices.go |
| Row intrinsics | component/memql/intrinsic_fields.go |
| Caller envelope | component/memql/sense/builtins.go + runtime_evaluator.go |
| Construct keywords | parser.TopLevelDeclKeywords and parser.StructFormKeywords in component/language/parser, projected by component/language/dslspec |
| Annotations | the 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.