MemQL Attribute Matrix
This page lists every annotation the language accepts, the constructs and fields that accept it, and the form its arguments take there. It is generated from the annotation registry in component/language/annotations, which the construct parsers and the concept loader check every annotation against, and make docs-matrix regenerates it.
How to read it
Under At a glance, a row is an annotation and a column is a construct or a kind of field: a filled cell says how the annotation is written there, and an empty cell means it is refused there, with one of the codes under Refusals. The words in a cell are argument forms: flag means no arguments, empty empty parentheses, string one string, strings one or more strings, number one number, keywords keyword arguments and expression an expression. Each annotation links to its entry under Annotations, which shows it written on every construct that accepts it, lists its keys and says what it does.
At a glance
One table per family of constructs, and one for the fields a construct declares.
Functions
| Annotation | query | mutation | logic | automation |
|---|---|---|---|---|
@actor | flag | flag | flag | flag |
@addToSet | strings | |||
@appendFields | strings | |||
@cache | number or keywords | |||
@createOnly | strings | |||
@description | string | string | string | string |
@disabled | flag | flag | flag | flag |
@eventField | strings | |||
@filter | expression | |||
@loop | keywords | |||
@mcp | flag | flag | flag | |
@mergeFields | strings | |||
@mode | keywords | |||
@noUnset | strings | |||
@public | flag | flag | ||
@removeFromSet | strings | |||
@requiresCapability | strings | strings | strings | |
@requiresRank | string | string | string | |
@scrubPii | flag | |||
@serverOnly | flag | flag | ||
@shopperFormExtension | keywords | |||
@template | flag | |||
@trigger | keywords | |||
@unbounded | string |
Data
| Annotation | concept | shape | spec and trait | seed |
|---|---|---|---|---|
@actor | flag | |||
@composable | flag or keywords | |||
@description | string | string | string | string |
@disabled | flag | flag | ||
@displayCard | keywords | |||
@mirroredTo | strings | |||
@origin | string | |||
@relationship | keywords, in the body | |||
@row | flag | |||
@rowAuthz | keywords | |||
@scope | string | |||
@templateFile | string | |||
@type | string | |||
@version | string | string |
Model access
| Annotation | prompt | provider | policy | rule | tool |
|---|---|---|---|---|---|
@allowedRoles | strings | ||||
@base | flag | ||||
@default | flag | ||||
@defaultProvider | string | ||||
@description | string | string | string | string | string |
@destructive | flag | ||||
@disabled | flag | flag | flag | flag | |
@exclude | strings | ||||
@executionTime | string | ||||
@extends | string | ||||
@fallback | string | ||||
@handler | keywords | ||||
@level | string | string | |||
@locked | flag | ||||
@mcp | flag | ||||
@modality | string | ||||
@model | string | ||||
@onUnavailable | string | ||||
@policy | string | ||||
@precedence | number | ||||
@primary | string | ||||
@requiresAgentRole | strings | ||||
@requiresConfirmation | flag | ||||
@requiresRank | string | ||||
@templateFile | string | ||||
@vendor | string | ||||
@when | empty or keywords |
Capabilities
| Annotation | builtin | action | capability |
|---|---|---|---|
@alias | string | ||
@args | keywords | ||
@description | string | string | string |
@disabled | flag | flag | flag |
@executor | string | ||
@requiresCapability | strings | ||
@sdk | flag | ||
@sideEffect | string |
Fields
| Annotation | concept field | args field | tool field | prompt field | builtin field |
|---|---|---|---|---|---|
@autoInjected | flag | ||||
@default | string | string or number | |||
@description | string | string | string | string | |
@enum | strings | strings | strings | strings | |
@internal | flag | ||||
@maximum | number | number | |||
@maxLength | number | number | |||
@minimum | number | number | |||
@minLength | number | ||||
@open | flag | ||||
@pattern | string | string | |||
@pii | flag | ||||
@required | flag | flag | flag | flag | flag |
@secret | flag | ||||
@serverSet | flag | ||||
@variant | keywords |
By construct
Every construct and every kind of field, with each annotation it accepts, how the annotation is written there, and an example.
query
| Annotation | Written as | Example |
|---|---|---|
@actor | no arguments | @actor |
@cache | one number or keyword arguments | @cache(300) |
@description | one string | @description("List the caller's open tickets, newest first.") |
@disabled | no arguments | @disabled |
@mcp | no arguments | @mcp |
@public | no arguments | @public |
@requiresCapability | one or more strings | @requiresCapability("read", "principal") |
@requiresRank | one string | @requiresRank("developer") |
@serverOnly | no arguments | @serverOnly |
@unbounded | one string | @unbounded("a small catalog of fixed size") |
The fields of its args block take the annotations under args field.
mutation
| Annotation | Written as | Example |
|---|---|---|
@actor | no arguments | @actor |
@addToSet | one or more strings | @addToSet("disabledDeployables") |
@appendFields | one or more strings | @appendFields("attachmentIds") |
@createOnly | one or more strings | @createOnly("status", "attempts") |
@description | one string | @description("Rename one of the caller's tickets.") |
@disabled | no arguments | @disabled |
@mcp | no arguments | @mcp |
@mergeFields | one or more strings | @mergeFields("preferences") |
@noUnset | one or more strings | @noUnset("bootstrappedAt") |
@public | no arguments | @public |
@removeFromSet | one or more strings | @removeFromSet("disabledDeployables") |
@requiresCapability | one or more strings | @requiresCapability("execute", "app:deployables/publish") |
@requiresRank | one string | @requiresRank("admin") |
@scrubPii | no arguments | @scrubPii |
@serverOnly | no arguments | @serverOnly |
@shopperFormExtension | keyword arguments | @shopperFormExtension(pack="wholesale", form="application") |
The fields of its args block take the annotations under args field.
logic
| Annotation | Written as | Example |
|---|---|---|
@actor | no arguments | @actor |
@description | one string | @description("Close every ticket that has been idle for a week.") |
@disabled | no arguments | @disabled |
@eventField | one or more strings | @eventField("id", "status") |
@requiresCapability | one or more strings | @requiresCapability("execute", "app:deployables/deploy") |
@requiresRank | one string | @requiresRank("admin") |
The fields of its args block take the annotations under args field.
automation
| Annotation | Written as | Example |
|---|---|---|
@actor | no arguments | @actor |
@description | one string | @description("On a new ticket, notify its owner.") |
@disabled | no arguments | @disabled |
@filter | an expression | @filter(row => row.status == "open") |
@loop | keyword arguments | @loop(maxDepth=4, until=row => row.status == "done") |
@mcp | no arguments | @mcp |
@mode | keyword arguments | @mode(queued, max=10) |
@template | no arguments | @template |
@trigger | keyword arguments | @trigger(event="node.created", concept="v1:cluster:node") |
The fields of its args block take the annotations under args field.
concept
| Annotation | Written as | Example |
|---|---|---|
@composable | no arguments or keyword arguments | @composable(as="ticket", fields="title,status", list="openTickets") |
@description | one string | @description("A support ticket raised by a customer.") |
@displayCard | keyword arguments | @displayCard(primary="title", secondary="status") |
@mirroredTo | one or more strings | @mirroredTo("shopify") |
@origin | one string | @origin("memql") |
@relationship | keyword arguments, repeatable, in the body | @relationship(type="parent", field="ownerUserId", target=user, direction="outgoing") |
@rowAuthz | keyword arguments | @rowAuthz(owner="ownerUserId", clusterOwner) |
@type | one string | @type("collection") |
@version | one string | @version("1.0.0") |
The fields in its body take the annotations under concept field.
shape
| Annotation | Written as | Example |
|---|---|---|
@actor | no arguments | @actor |
@description | one string | @description("Ticket summary card.") |
@row | no arguments | @row |
spec and trait
| Annotation | Written as | Example |
|---|---|---|
@description | one string | @description("Matches the rows that are still open.") |
@disabled | no arguments | @disabled |
seed
| Annotation | Written as | Example |
|---|---|---|
@description | one string | @description("Knowledge baseline for every professional role.") |
@disabled | no arguments | @disabled |
@scope | one string | @scope("perUser") |
@templateFile | one string | @templateFile("templates/assistant.tmpl") |
@version | one string | @version("1.0.0") |
prompt
| Annotation | Written as | Example |
|---|---|---|
@defaultProvider | one string | @defaultProvider("fleet") |
@description | one string | @description("Distil a cluster of episodes into one memory.") |
@disabled | no arguments | @disabled |
@level | one string | @level("fast") |
@templateFile | one string | @templateFile("prompts/consolidateMemory.tmpl") |
The fields in its body take the annotations under prompt field.
provider
| Annotation | Written as | Example |
|---|---|---|
@base | no arguments | @base |
@default | no arguments | @default |
@description | one string | @description("OpenAI GPT-5.4 Mini -- balanced cost and latency.") |
@disabled | no arguments | @disabled |
@extends | one string | @extends("openai") |
@modality | one string | @modality("embedding") |
@model | one string | @model("gpt-5.4-mini") |
@vendor | one string | @vendor("OpenAI") |
policy
| Annotation | Written as | Example |
|---|---|---|
@description | one string | @description("Local strongest, then an app, then the cheapest federated model.") |
@fallback | one string, repeatable | @fallback("app:*") |
@primary | one string | @primary("fleet:strongest") |
rule
| Annotation | Written as | Example |
|---|---|---|
@description | one string | @description("Operator turns reason locally first.") |
@disabled | no arguments | @disabled |
@exclude | one or more strings, repeatable | @exclude("fleet:qwen3.5:7b") |
@level | one string | @level("strong") |
@locked | no arguments | @locked |
@onUnavailable | one string | @onUnavailable("degrade") |
@policy | one string | @policy("localFirst") |
@precedence | one number | @precedence(60) |
@when | empty parentheses or keyword arguments | @when(level="fast") |
tool
| Annotation | Written as | Example |
|---|---|---|
@allowedRoles | one or more strings | @allowedRoles("assistant", "specialist") |
@description | one string | @description("Create a to-do for the caller.") |
@destructive | no arguments | @destructive |
@disabled | no arguments | @disabled |
@executionTime | one string | @executionTime("fast") |
@handler | keyword arguments | @handler(type="function", name="createTodo") |
@mcp | no arguments | @mcp |
@requiresAgentRole | one or more strings | @requiresAgentRole("assistant") |
@requiresConfirmation | no arguments | @requiresConfirmation |
@requiresRank | one string | @requiresRank("developer") |
The fields in its body take the annotations under tool field.
builtin
| Annotation | Written as | Example |
|---|---|---|
@alias | one string, repeatable | @alias("memqlVersion") |
@args | keyword arguments | @args(profile="object") |
@description | one string | @description("Preflight and start a campaign send.") |
@disabled | no arguments | @disabled |
@executor | one string | @executor("integration.campaigns.startSend") |
@requiresCapability | one or more strings | @requiresCapability("execute", "app:deployables/deploy") |
@sdk | no arguments | @sdk |
The fields in its body take the annotations under builtin field.
action
| Annotation | Written as | Example |
|---|---|---|
@description | one string | @description("Tag a release in the repository.") |
@disabled | no arguments | @disabled |
The fields of its args block take the annotations under args field.
capability
| Annotation | Written as | Example |
|---|---|---|
@description | one string | @description("Create a git tag and a GitHub release for a version.") |
@disabled | no arguments | @disabled |
@sideEffect | one string | @sideEffect("write") |
The fields of its args block take the annotations under args field.
concept field
Written after a field's type in the body of a concept.
| Annotation | Written as | Example |
|---|---|---|
@description | one string | @description("The ticket's one-line title.") |
@internal | no arguments | @internal |
@maximum | one number | @maximum(100) |
@maxLength | one number | @maxLength(120) |
@minimum | one number | @minimum(0) |
@minLength | one number | @minLength(1) |
@open | no arguments | @open |
@pattern | one string | @pattern("^[a-z][a-z0-9-]*$") |
@pii | no arguments | @pii |
@required | no arguments | @required |
@secret | no arguments | @secret |
@serverSet | no arguments | @serverSet |
@variant | keyword arguments | @variant(discriminator="kind") |
args field
Written after a field's type in the args block of a query, mutation, logic, automation, action or capability.
| Annotation | Written as | Example |
|---|---|---|
@enum | one or more strings | @enum("open", "closed") |
@maximum | one number | @maximum(60) |
@maxLength | one number | @maxLength(120) |
@minimum | one number | @minimum(30) |
@pattern | one string | @pattern("^[A-Za-z]{2,3}$") |
@required | no arguments | @required |
tool field
Written after a field's type in the body of a tool.
| Annotation | Written as | Example |
|---|---|---|
@autoInjected | no arguments | @autoInjected |
@default | one string | @default("5") |
@description | one string | @description("Max results to return.") |
@enum | one or more strings | @enum("exec", "fs_read") |
@required | no arguments | @required |
prompt field
Written after a field's type in the body of a prompt.
| Annotation | Written as | Example |
|---|---|---|
@default | one string or one number | @default("en") |
@description | one string | @description("The episodes to distil.") |
@enum | one or more strings | @enum("short", "long") |
@required | no arguments | @required |
builtin field
Written after a field's type in the body of a builtin.
| Annotation | Written as | Example |
|---|---|---|
@description | one string | @description("The campaign to send.") |
@enum | one or more strings | @enum("patch", "minor") |
@required | no arguments | @required |
Annotations
One entry per annotation, in alphabetical order: where it is accepted and how it is written there, its keys, and what it does. An annotation marked repeatable may be written more than once on one declaration; any other is refused the second time with annotation_repeated.
@actor
| On | Written as | Example |
|---|---|---|
| query, mutation, logic, automation, shape | no arguments | @actor |
- On a query, a mutation, a logic or an automation: Declares that the body reads the authenticated actor (actor.userId, actor.role, actor.identityId, actor.isClusterOwner, actor.primaryEmail, actor.now). The actor-binding load rule refuses a body that reads actor.* without it (memql#2621).
- On a shape: Shape kind marker: the shape projects the authenticated actor's envelope (actor.userId / actor.role / ...), and carries no signature concept.
@addToSet
| On | Written as | Example |
|---|---|---|
| mutation | one or more strings | @addToSet("disabledDeployables") |
On an update mutation: treat the named array-typed payload fields as SETS and UNION the written elements into the stored array -- deduped, existing order kept, new members appended in the order given. The membership half @appendFields is not: append is not deduped and has no counterpart that removes, so a toggle built on it duplicates on a double click. Pairs with @removeFromSet. Format: @addToSet("disabledDeployables"). See memql#4951.
Both follow the same rules:
- Only an update mutation may carry it; on an insert it is refused at load, because an insert has no stored set to change.
- A field absent from the write is untouched: the stored array survives the read-merge.
- Members compare by their rendered form, so
1and"1"are one member. A stored number comes back from JSON as a float and its string spelling as a string, and keeping both would be a set that quietly grew. - A stored value that is not an array reads as empty, and a scalar written where an array is expected is one member, as with
@appendFields. - A field named by two of
@appendFields,@addToSetand@removeFromSetis refused at load: each rewrites the same key, so which one won would depend on the executor's order rather than on anything the mutation says.
@alias
| On | Written as | Example |
|---|---|---|
| builtin | one string, repeatable | @alias("memqlVersion") |
Additional name the builtin is registered under. Repeatable.
@allowedRoles
| On | Written as | Example |
|---|---|---|
| tool | one or more strings | @allowedRoles("assistant", "specialist") |
DEPRECATED (rule deprecated_allowed_roles, memql#5438): write @requiresAgentRole for an agent kind and @requiresRank for a person's role -- memqlmigrate --rewrite=allowed-roles rewrites it. It compares ONE role string whose meaning depends on who is calling: the acting agent's role (assistant / specialist) in an agent's tool loop, and the person's cluster role over MCP. Two axes in one list is why it is leaving: neither list can be read without knowing which path the call arrives by. Until its window closes it keeps that behaviour, on every path a tool is listed or called by -- read by component/memql/tool_gate.go through Tool.IsAllowedForRole (tool_types.go).
@appendFields
| On | Written as | Example |
|---|---|---|
| mutation | one or more strings | @appendFields("attachmentIds") |
On an update mutation: append the named array-typed payload fields' elements to the stored array instead of replacing it wholesale, so a single-writer mutation can accumulate list items (e.g. attach one id). Format: @appendFields("attachmentIds").
@args
| On | Written as | Example |
|---|---|---|
| builtin | keyword arguments | @args(profile="object") |
@args takes these keys, each written as key=value.
| Key | Type | Meaning |
|---|---|---|
profile | string | The argument profile: none, object, optionalObject, stringOrObject, optionalString or optionalStringOrObject. Inferred from the body when absent. |
stringKey | string | The key a bare string argument binds to, for the profiles that accept one. |
additionalProperties | string | Whether an object argument may carry keys the body does not declare (default false). |
when | string | An inert note naming the type of routingRuleActivate's when argument, which its body cannot declare (when is a keyword). No loader reads it. |
excludes | string | An inert note naming the type of routingRuleActivate's excludes argument. No loader reads it. |
Parse-time argument contract for builtin functions.
@autoInjected
| On | Written as | Example |
|---|---|---|
| tool field | no arguments | @autoInjected |
The field is filled by the runtime rather than the model: it is kept out of the input schema the model sees and stamped server-side (the calling agent's id, for example).
@base
| On | Written as | Example |
|---|---|---|
| provider | no arguments | @base |
Mark a vendor-level base provider (auth + type only).
@cache
| On | Written as | Example |
|---|---|---|
| query | one number or keyword arguments | @cache(300) |
@cache takes these keys, each written as key=value.
| Key | Type | Meaning |
|---|---|---|
ttl | string | Cache TTL in whole seconds. Positional preferred (#2618): @cache(300); keyword ttl="300" keeps parsing. |
Override the result-cache TTL for the query. Preferred form (#2618): @cache(300) -- the single ttl arg makes position unambiguous. The keyword form @cache(ttl="300") keeps parsing. Pure reads cache BY DEFAULT (a 60s backstop) without this annotation; @cache sets a different TTL, longer or shorter. @cache(0) is the explicit "never cache" opt-out -- @nocache was retired with it (epic memql#5375), and one annotation for the TTL is the whole reason. The engine keys the cache on the plan signature (query/sort/limit/depth/shape + the keyset cursor) and evicts on any write to the read concept via the cache.invalidate.* broadcast channel -- cross-node eviction needs no per-concept routing rule.
@composable
| On | Written as | Example |
|---|---|---|
| concept | no arguments or keyword arguments | @composable(as="ticket", fields="title,status", list="openTickets") |
@composable takes these keys, each written as key=value.
| Key | Type | Meaning |
|---|---|---|
as | string | The label a composed file names the row kind by, e.g. "invoice". |
fields | string | Comma-separated fields to compose from, e.g. "number,issuedAt,total"; each must be a field the concept declares or a row intrinsic. |
list | string | The bare name of the query that lists the rows to compose from. |
The Materializer's mark (epic memql#4977, D2): this concept's rows are worth composing a file FROM. Bare, it takes the defaults; as= names the row kind, fields= lists the fields to compose from and list= names the query that lists the rows. Read by clients/os (src/apps/materializer/useCompose.ts) through integration.compose.composableConcepts.
@createOnly
| On | Written as | Example |
|---|---|---|
| mutation | one or more strings | @createOnly("status", "attempts") |
On an insert (create-or-upsert) mutation: write the named payload fields ONLY when creating the row. If the target id already exists, the fields are dropped from the delta before the engine read-merge, so the stored value is preserved rather than clobbered -- making a deterministic-id re-stage idempotent for lifecycle fields another writer owns after creation (e.g. stageOutboundRequest seeds status but must not reset a row the outbound worker moved to sent). The inverse of @mergeFields/@appendFields: only valid on insert-kind mutations. Format: @createOnly("status", "attempts"). See fylo#63.
@default
| On | Written as | Example |
|---|---|---|
| provider | no arguments | @default |
| tool field | one string | @default("5") |
| prompt field | one string or one number | @default("en") |
- On a provider: Mark this provider as the default for its modality.
- On a tool field: The default the tool's input schema advertises to the model, written quoted: its text converts to the field's type as a call's default always has --
@default("5")on an integer field,@default("true")on a boolean, one of the values of an enum field -- or the load refuses it (tool_default_type, memql#5430). - On a prompt field: The default the prompt's input schema declares for the field: its text -- quoted, or an unquoted number as written -- converts to the field's type by the rule a tool field's does, or the load refuses it (prompt_default_type, memql#5430).
@defaultProvider
| On | Written as | Example |
|---|---|---|
| prompt | one string | @defaultProvider("fleet") |
Default AI provider for prompt execution: an explicit pin that wins over every routing rule.
@description
| On | Written as | Example |
|---|---|---|
| query | one string | @description("List the caller's open tickets, newest first.") |
| mutation | one string | @description("Rename one of the caller's tickets.") |
| logic | one string | @description("Close every ticket that has been idle for a week.") |
| automation | one string | @description("On a new ticket, notify its owner.") |
| concept | one string | @description("A support ticket raised by a customer.") |
| shape | one string | @description("Ticket summary card.") |
| spec and trait | one string | @description("Matches the rows that are still open.") |
| seed | one string | @description("Knowledge baseline for every professional role.") |
| prompt | one string | @description("Distil a cluster of episodes into one memory.") |
| provider | one string | @description("OpenAI GPT-5.4 Mini -- balanced cost and latency.") |
| policy | one string | @description("Local strongest, then an app, then the cheapest federated model.") |
| rule | one string | @description("Operator turns reason locally first.") |
| tool | one string | @description("Create a to-do for the caller.") |
| builtin | one string | @description("Preflight and start a campaign send.") |
| action | one string | @description("Tag a release in the repository.") |
| capability | one string | @description("Create a git tag and a GitHub release for a version.") |
| concept field | one string | @description("The ticket's one-line title.") |
| tool field | one string | @description("Max results to return.") |
| prompt field | one string | @description("The episodes to distil.") |
| builtin field | one string | @description("The campaign to send.") |
- On a query, a mutation, a logic, an automation, a concept, a shape, a spec or trait, a seed, a prompt, a provider, a policy, a rule, a tool, a builtin, an action or a capability: Human-readable description of this definition. PREFER the /// doc-comment form (#2601): a /// block immediately above the declaration IS the description and wins over this annotation; @description remains the valid compatibility fallback -- the tree gate rejects the redundant long form (including a bare @description shadowed by a /// block). Aim for ~500 characters (editorial target).
- On a concept field: The field's description, emitted into the concept schema.
- On a tool field: The field's description, shown to the model in the tool's input schema.
- On a prompt field: The field's description, carried into the prompt's input schema.
- On a builtin field: The field's description, read into the generated SDKs' docs.
@destructive
| On | Written as | Example |
|---|---|---|
| tool | no arguments | @destructive |
Mark a tool as destructive (mutates/deletes); the tool loop gates it behind a confirmation.
@disabled
| On | Written as | Example |
|---|---|---|
| query, mutation, logic, automation, spec and trait, seed, prompt, provider, rule, tool, builtin, action, capability | no arguments | @disabled |
Takes the construct out of service without deleting it. Every construct is enabled by default and @enabled is an accepted no-op, so @disabled is the only off switch. A disabled construct stays in the tree, is still maintained, and must stay valid, because re-enabling it is only removing the annotation. What it does depends on the construct:
- A disabled query, mutation, logic or builtin is hidden from
functions()and from the MCP surface, its@mcptool included, and a direct call is refused withfunction "name" is disabled;help()still describes it, reporting"enabled": false. A disabled builtin also skips the check that its Go executor exists, so a builtin whose executor is gone can be disabled without refusing boot. - A disabled automation is neither scheduled nor subscribed to its trigger, is dropped from the MCP surface, and is refused on every manual run path: the MCP dry run and live run, the run relay and the HTTP trigger.
- A disabled tool, seed, prompt, provider, action, spec or trait is skipped at load. A tool is not registered, and its name stays reserved so no ungoverned tool is generated from the function behind it. A seed is never materialised and a prompt never registers. A provider is not registered and no auth is resolved for it, and on a
@baseprovider it skips every provider that@extendsit. An action is parsed but not registered. A spec or trait is still validated but never binds, and its name stays reserved, so a call to it says it is disabled rather than not found. - A disabled rule is loaded but never evaluated.
- A disabled capability is still reconciled against its Go capability and checked for duplicates --
@disabledis not a validation bypass -- but it is left out of the catalog:use capabilities....imports stop resolving it, and an action that names it is refused at load withcapability "name" is @disabled. To park DSL ahead of the Go it needs, put the file under a_disabled/directory instead, which the loaders skip.
An authored spec or trait keeps the same state through the authoring lifecycle. Staging or promoting a disabled one is refused (construct is @disabled; enable it (remove @disabled from the source) before promoting). A promoted row that is disabled is skipped when the engine re-hydrates authored constructs and counted as skippedDisabled rather than quarantined; it reserves the name only when nothing live or already reserved holds it, and promoting the corrected source or demoting the name releases it. A staged one is skipped without reserving anything. A bundle that authors a disabled capability is refused (capability "name" is @disabled), because a disabled capability compiles to nothing.
@displayCard
| On | Written as | Example |
|---|---|---|
| concept | keyword arguments | @displayCard(primary="title", secondary="status") |
@displayCard takes these keys, each written as key=value.
| Key | Type | Meaning |
|---|---|---|
primary | string | The field shown as the row's title. Required. |
secondary | string | The field shown under the title. |
tertiary | string | A third field shown in the row's detail line. |
status | string | The field shown as the row's status. |
Rendering hints for concept-agnostic clients (memql#160): the field shown as a row's title (primary=, required) and the fields for the secondary, tertiary and status slots. Each must be a displayable field the concept declares, checked once the property set is known. Read by clients/os (src/apps/concepts/displayCard.ts, consumed by RowsPanel.tsx); test/dslconformance's displaycard_inventory_test.go requires every concept to declare or decline one.
@enum
| On | Written as | Example |
|---|---|---|
| args field | one or more strings | @enum("open", "closed") |
| tool field | one or more strings | @enum("exec", "fs_read") |
| prompt field | one or more strings | @enum("short", "long") |
| builtin field | one or more strings | @enum("patch", "minor") |
The closed set of string values the field accepts: @enum("a", "b"). The enum("a", "b") type is the same constraint in one statement.
@eventField
| On | Written as | Example |
|---|---|---|
| logic | one or more strings | @eventField("id", "status") |
On an event-triggered logic: declare the allowed top-level event payload fields (e.g. @eventField("partitionId", "siParticipantId")). Opt-in field-level validation -- every event.payload.<field> reference in the body is checked against this set at load time, rejecting typos / fields the (possibly synthetic, handler-assembled) triggering event cannot carry (memql#1743). Bare names or payload.-prefixed paths both normalize to the head segment.
@exclude
| On | Written as | Example |
|---|---|---|
| rule | one or more strings, repeatable | @exclude("fleet:qwen3.5:7b") |
On a rule: remove one concrete entry from the chain's resolution, e.g. @exclude("fleet:qwen3.5:7b"). Repeatable.
@executionTime
| On | Written as | Example |
|---|---|---|
| tool | one string | @executionTime("fast") |
Expected execution time hint: "fast", "medium", or "slow".
@executor
| On | Written as | Example |
|---|---|---|
| builtin | one string | @executor("integration.campaigns.startSend") |
Go executor name for builtin functions (integration.X.Y).
@extends
| On | Written as | Example |
|---|---|---|
| provider | one string | @extends("openai") |
Inherit configuration from a base provider.
@fallback
| On | Written as | Example |
|---|---|---|
| policy | one string, repeatable | @fallback("app:*") |
On a policy: an entry tried when the ones before it cannot serve the call. Repeatable (order preserved). Same closed grammar as @primary.
@filter
| On | Written as | Example |
|---|---|---|
| automation | an expression | @filter(row => row.status == "open") |
Filter for automation triggers: a lambda of one parameter over the triggering row, as in @filter(row => row.status == "open").
@handler
| On | Written as | Example |
|---|---|---|
| tool | keyword arguments | @handler(type="function", name="createTodo") |
@handler takes these keys, each written as key=value.
| Key | Type | Meaning |
|---|---|---|
type | string | Handler type: "function", "query", "webhook" or "delegate". Required. |
name | string | Function or builtin name (with type="function"). |
query | string | MemQL query or mutation call (with type="query"). |
url | string | Webhook URL (with type="webhook"). |
method | string | HTTP method for a webhook handler, e.g. "POST". |
Tool handler configuration. Format: @handler(type="query", query="...") / @handler(type="function", name="...").
@internal
| On | Written as | Example |
|---|---|---|
| concept field | no arguments | @internal |
On a concept field: server-only (memql#2035) -- never projected by a shape's default projection and never accepted from a mutation's caller args; emitted as x-internal. (On a construct, @internal is retired, #2708.)
@level
- On a prompt: How much intelligence the call needs: fast, strong, reasoning or embeddings. The router's rules branch on it, so a prompt never names a model (epic memql#5127). Required on every prompt: one without it is refused at load (
prompt_level_missing), a @disabled prompt included. - On a rule: The level to resolve the call at: fast, strong, reasoning or embeddings, OVERRIDING what the call declared.
@locked
| On | Written as | Example |
|---|---|---|
| rule | no arguments | @locked |
On a rule: evaluate before every unlocked rule regardless of precedence. Accepted only in the embedded tree -- the loader refuses it elsewhere.
@loop
| On | Written as | Example |
|---|---|---|
| automation | keyword arguments | @loop(maxDepth=4, until=row => row.status == "done") |
@loop takes these keys, each written as key=value.
| Key | Type | Meaning |
|---|---|---|
maxDepth | int | The most runs of this automation one causal chain may hold. An integer from 1 to the depth cap (MEMQL_AUTOMATION_MAX_CHAIN_DEPTH, default 16). |
until | expression | The convergence predicate: a lambda of one parameter over the triggering row, until=row => row.status == "done". The automation's @filter must hold its negation as a top-level conjunct, which is what stops the loop. |
On an automation that closes a deliberate cycle: permits the cycle the load would otherwise refuse, bounds it to maxDepth runs of this automation per causal chain, and names the predicate that ends it. The @filter must exclude the rows where until holds.
@maximum
| On | Written as | Example |
|---|---|---|
| concept field | one number | @maximum(100) |
| args field | one number | @maximum(60) |
- On a concept field: The INCLUSIVE upper bound on a numeric value.
- On an args field: The INCLUSIVE upper bound on a numeric argument (memql#4522).
@maxLength
| On | Written as | Example |
|---|---|---|
| concept field, args field | one number | @maxLength(120) |
- On a concept field: The most characters a string value may carry.
- On an args field: The most characters a string argument may carry (a rune count, enforced on strings).
@mcp
| On | Written as | Example |
|---|---|---|
| query, mutation, automation, tool | no arguments | @mcp |
Expose this construct on the MCP connector surface. On a query/mutation/automation it promotes the construct into its own first-class MCP tool (otherwise it stays reachable via the generic run_query / run_mutation / run_automation dispatchers). On a tool it opts the tool into the curated connector allowlist: once ANY tool carries @mcp, tools/list reflects only @mcp tools (otherwise -- zero tagged -- the full tool surface is reflected, so the annotation is inert until the curated set is tagged).
@mergeFields
| On | Written as | Example |
|---|---|---|
| mutation | one or more strings | @mergeFields("preferences") |
On an update mutation: deep-merge the named object-typed payload fields into the stored object instead of replacing them wholesale, so sibling keys survive a single-key write. Format: @mergeFields("preferences").
@minimum
| On | Written as | Example |
|---|---|---|
| concept field | one number | @minimum(0) |
| args field | one number | @minimum(30) |
- On a concept field: The INCLUSIVE lower bound on a numeric value.
- On an args field: The INCLUSIVE lower bound on a numeric argument (memql#4522).
@minLength
| On | Written as | Example |
|---|---|---|
| concept field | one number | @minLength(1) |
The fewest characters a string value may carry.
@mirroredTo
| On | Written as | Example |
|---|---|---|
| concept | one or more strings | @mirroredTo("shopify") |
Declares WHO ELSE HOLDS A COPY of this MemQL-origin concept: @mirroredTo("shopify"), or several names in one annotation. Every write to the concept appends one v1:platform:outboxEntry per target, in the write's own transaction, which a per-connector drain worker delivers with an idempotency key, backoff, dead-lettering and audit. Only valid on a MemQL-origin concept -- @mirroredTo beside an external @origin is REFUSED at load, because re-mirroring somebody else's data onward is the origin's job and a mirror that also publishes is a second origin wearing the first one's badge. Each named connector must be registered or the engine refuses boot: a mirror target nobody drains is a silent drop. See data-origins.md.
@modality
| On | Written as | Example |
|---|---|---|
| provider | one string | @modality("embedding") |
Provider modality (e.g., "chat", "audio", "image", "embedding").
@mode
| On | Written as | Example |
|---|---|---|
| automation | keyword arguments | @mode(queued, max=10) |
@mode takes these keys. A key of type flag is written bare, and every other as key=value.
| Key | Type | Meaning |
|---|---|---|
single | flag | One run at a time in this process; a fire while one runs is refused with a warning. |
queued | flag | Fires wait their turn in order; more than max waiting are refused. |
restart | flag | A fire cancels the run in flight and starts again. |
parallel | flag | Runs concurrently; more than max at once are refused. The default when no @mode is written, with no max. |
max | int | With queued, the most fires that may wait (default 10); with parallel, the most runs at once. |
How concurrent fires of this automation behave in one process: single, queued, restart or parallel, with max for queued and parallel. Without it an automation runs every fire in parallel.
@model
| On | Written as | Example |
|---|---|---|
| provider | one string | @model("gpt-5.4-mini") |
Model identifier (e.g., "gpt-5.4-mini", "claude-sonnet-4-6").
@noUnset
| On | Written as | Example |
|---|---|---|
| mutation | one or more strings | @noUnset("bootstrappedAt") |
On any mutation: declare the named payload fields ONE-WAY -- a write may set them or change one non-empty value to another, but may never take a stored non-empty value back to empty. On the read-merge path a named field arriving empty is dropped from the delta when the stored row holds a non-empty value. Closes the gap read-merge cannot (it only inherits fields ABSENT from a delta, so a body writing f: args.f ?? "" blanks the stored value with an explicit empty string). Distinct from @createOnly, which forbids any post-create write; @noUnset forbids only set -> unset, so a legitimately-later stamp still lands. Format: @noUnset("bootstrappedAt"). See memql#3415.
Empty means nil, a blank or whitespace-only string, or an empty array or object. A numeric or boolean zero is a value, not an unset: 0 and false are written like any other value, and treating them as empty would make @noUnset unwritable for those types.
@onUnavailable
| On | Written as | Example |
|---|---|---|
| rule | one string | @onUnavailable("degrade") |
On a rule: what happens when the chain is exhausted at the level -- "degrade" walks down to the next level and records that it did, "park" returns the refusal with the door report. Empty reads as degrade.
@open
| On | Written as | Example |
|---|---|---|
| concept field | no arguments | @open |
On a nested object block: the block accepts keys it does not declare, suppressing the closed-by-default additionalProperties: false (memql#3641). For a block whose keys are data rather than schema.
@origin
| On | Written as | Example |
|---|---|---|
| concept | one string | @origin("memql") |
Declares WHERE CHANGES TO THIS CONCEPT ARE MADE -- the system that owns the data. @origin("memql") (the default when the annotation is absent) means MemQL originates it; @origin("<connector>") names an external system, which makes the concept a MIRROR. A mirror is READ-ONLY BY CONSTRUCTION: component/memql refuses every write to it -- mutation, tool handler, raw insert or staged write -- that does not come from the connector the origin names, so what the badge says is what a reader may assume. The name must be a registered connector or the engine REFUSES BOOT naming the concept: a mirror nobody fills is a lie. Pairs with @mirroredTo to derive dataState (mirror | origin | native), which the registry, both SDKs and client data-origin displays read. See data-origins.md.
@pattern
| On | Written as | Example |
|---|---|---|
| concept field | one string | @pattern("^[a-z][a-z0-9-]*$") |
| args field | one string | @pattern("^[A-Za-z]{2,3}$") |
- On a concept field: A regular expression a string value must match, in Go's RE2 syntax. It is compiled at load on a concept field and an args field alike, so an expression that does not compile is refused there (
pattern_invalid) rather than at the first write or call. - On an args field: A regular expression a string argument must match, compiled once at load.
@pii
| On | Written as | Example |
|---|---|---|
| concept field | no arguments | @pii |
The field is personally identifying: emitted as x-pii, and the hard-delete scrub (@scrubPii, memql#1711) zeroes every such field generically.
@policy
| On | Written as | Example |
|---|---|---|
| rule | one string | @policy("localFirst") |
Required on a rule: the policy this rule resolves the call through.
@precedence
| On | Written as | Example |
|---|---|---|
| rule | one number | @precedence(60) |
On a rule: evaluation order, highest first. A tie between two rules of the same locked-ness is a load error.
@primary
| On | Written as | Example |
|---|---|---|
| policy | one string | @primary("fleet:strongest") |
Required on a policy: the first entry the AI Router resolves. A provider name, a fleet: / app: / federation: selector, or policy:<name>.
@public
Per-row-authz marker: this query/mutation is intentionally callable without a caller-scope filter (concept catalogs, pre-auth login paths). See per-row-authz-audit.md.
@relationship
| On | Written as | Example |
|---|---|---|
| concept | keyword arguments, repeatable, in the body | @relationship(type="parent", field="ownerUserId", target=user, direction="outgoing") |
@relationship takes these keys, each written as key=value.
| Key | Type | Meaning |
|---|---|---|
type | string | STRUCTURAL type -- what the engine does with the edge. Closed set: parent, owns, createdBy, alias, equals, contains, references. |
field | string | Local field holding the foreign key; a dotted path reaches into a nested object block. |
fieldSource | string | Where the foreign key lives: "payload" (the default) or "table" (the edge table). |
target | string | Target concept: a bare concept name resolved through a file-top use import. |
direction | string | "outgoing" or "incoming". |
as | string | DOMAIN label -- what the edge means, e.g. as="assignedTo". Optional. Any lowerCamelCase identifier; validated for form only and never checked against a list, so a new verb never needs an engine release (memql#3652). |
Foreign-key relationship metadata. Format: @relationship(type="parent", field="x", target=concept, direction="outgoing"), plus an optional as="domainVerb" label.
@removeFromSet
| On | Written as | Example |
|---|---|---|
| mutation | one or more strings | @removeFromSet("disabledDeployables") |
On an update mutation: treat the named array-typed payload fields as SETS and REMOVE the written elements from the stored array, keeping the order of what remains. Removing something absent is a no-op rather than an error, so the mutation is idempotent and two callers removing the same member both succeed. Pairs with @addToSet, and the pair is what lets a set be edited one member at a time instead of read-modify-written whole. Format: @removeFromSet("disabledDeployables"). See memql#4951.
Both follow the same rules:
- Only an update mutation may carry it; on an insert it is refused at load, because an insert has no stored set to change.
- A field absent from the write is untouched: the stored array survives the read-merge.
- Members compare by their rendered form, so
1and"1"are one member. A stored number comes back from JSON as a float and its string spelling as a string, and keeping both would be a set that quietly grew. - A stored value that is not an array reads as empty, and a scalar written where an array is expected is one member, as with
@appendFields. - A field named by two of
@appendFields,@addToSetand@removeFromSetis refused at load: each rewrites the same key, so which one won would depend on the executor's order rather than on anything the mutation says.
@required
| On | Written as | Example |
|---|---|---|
| concept field, args field, tool field, prompt field, builtin field | no arguments | @required |
- On a concept field, a tool field, a prompt field or a builtin field: The field is required: a concept write without it fails the schema, and a caller must pass an args / tool / prompt / builtin field that carries it. The
!sigil after the type is the same thing. - On an args field: The caller must pass the argument. The
!sigil after the type is the same thing.
@requiresAgentRole
| On | Written as | Example |
|---|---|---|
| tool | one or more strings | @requiresAgentRole("assistant") |
The AGENT-KIND gate on a tool: only an agent whose role is one of the values may be offered the tool or call it -- @requiresAgentRole("assistant") (memql#5438). The values are the v1:agents:agent concept's own role enum (assistant, specialist), and the load VALIDATES them against that declaration, so a value no agent can hold refuses boot rather than admitting nobody. It is the axis @requiresRank and @requiresCapability cannot express: they judge the PERSON a call is for, this judges WHICH AGENT is calling. A person calling over MCP is not an agent, so a tool carrying it is neither listed nor callable for them. It replaces the agent half of the deprecated @allowedRoles; a tool with neither gate is callable by every agent and by an authenticated person over MCP.
@requiresCapability
| On | Written as | Example |
|---|---|---|
| query | one or more strings | @requiresCapability("read", "principal") |
| mutation | one or more strings | @requiresCapability("execute", "app:deployables/publish") |
| logic, builtin | one or more strings | @requiresCapability("execute", "app:deployables/deploy") |
The CAPABILITY a caller must hold to invoke the construct -- @requiresCapability("read", "principal") (epic memql#5166, D11). The sibling of @requiresRank, and the difference is the point: a RANK is a floor on the cluster's ladder, a CAPABILITY is a (verb x resource) grant a role was given, and a cluster can hold one without the other -- developer ranks above admin and holds strictly fewer principal verbs. Declared together, BOTH must pass. VALIDATED AT LOAD against the five verbs and the resource kinds any role in this cluster holds a grant on, so a misspelling refuses boot rather than gating a surface into silence; ENFORCED at execution through the runtime capability catalog, on the direct call and on every plan that expands the construct. It replaces the slug-comparing specs (requiresAdmin, requiresOwnerOrAdmin, requiresDeveloperOrAbove), which could not see a custom role at all: role == "admin" is false for a rank-250 role holding every principal verb. It gates WHO MAY CALL; @rowAuthz still decides WHICH ROWS come back.
@requiresConfirmation
| On | Written as | Example |
|---|---|---|
| tool | no arguments | @requiresConfirmation |
Require explicit user confirmation before the tool executes.
@requiresRank
| On | Written as | Example |
|---|---|---|
| query, tool | one string | @requiresRank("developer") |
| mutation, logic | one string | @requiresRank("admin") |
- On a query, a mutation or a logic: The actor-rank FLOOR: only a caller holding this role, or one ranked above it, may invoke the construct -- @requiresRank("developer") (epic memql#4832, D6). ENFORCED at execution and VALIDATED AT LOAD against the role ladder in dsl/rbac, so a typo refuses boot rather than gating on rank 0 and admitting everyone. This is the server-side counterpart to MemQL OS's per-surface role requirement: the shell keeps hiding what a caller cannot reach (hiding an action beats letting them click it and reading a refusal) and this makes the hidden surface a REFUSED one. Declared on the CONSTRUCT because a surface is a set of constructs and an app id from a browser is a claim, not a fact. It gates WHO MAY CALL; @rowAuthz still decides WHICH ROWS come back.
- On a tool: The person-rank FLOOR on a tool: a call is admitted only when the person it is for -- the authenticated user over MCP, or the user an agent acts for in its tool loop -- holds this role or one ranked above it, and a tool above the caller's rank is not listed to them either. VALIDATED AT LOAD against the role ladder in dsl/rbac exactly as on a query, mutation or logic, and resolved at call time through the same ladder, so a custom role is admitted by its rank. It judges the PERSON and never the agent -- @requiresAgentRole is that gate -- and it replaces the person half of the deprecated @allowedRoles.
@row
| On | Written as | Example |
|---|---|---|
| shape | no arguments | @row |
Shape kind: projects a concept's payload + row intrinsics (concept bound via the shape <Concept> <name> signature).
@rowAuthz
| On | Written as | Example |
|---|---|---|
| concept | keyword arguments | @rowAuthz(owner="ownerUserId", clusterOwner) |
@rowAuthz takes these keys. A key of type flag is written bare, and every other as key=value.
| Key | Type | Meaning |
|---|---|---|
public | flag | The public tier: globally readable by intent. |
clusterOwner | flag | The administrative tier; beside owner=, the composite -- the owner, or a cluster owner (memql#4312). |
owner | string | The owned tier: the payload field compared against actor.userId, or "id" for a self-owned concept (memql#3029). |
via | string | The granted tier: the relationship spec that grants visibility. |
account | string | Beside owner=: also admits anyone whose group ties them to the row's account field (epic memql#5165). |
rankVisible | flag | Beside owner=: reads widen to anyone at or above the owner's rank (epic memql#4832). |
rankStrict | flag | Beside owner= and rankVisible: writes widen to rows owned strictly below the caller's rank, and the cluster-owner write escape is withdrawn. |
unowned | string | Beside owner= and rankVisible: the actor rank from which a row with an empty owner is readable. |
requiresIdentity | flag | Beside public: readable by authenticated callers only (memql#4809). |
rankFloor | string | Beside clusterOwner: relaxes the READ to a rank floor while the write stays cluster-owner (memql#5216). |
Declares WHO MAY SEE this concept's rows, once on the concept, instead of as an actor.* term every filter over it must remember to carry. Four tiers, one spelling each: @rowAuthz(public) (globally readable by intent -- spelled explicitly, because "no annotation" and "declared public" are different states), @rowAuthz(clusterOwner) (administrative), @rowAuthz(owner="<field>") (the field is compared against actor.userId; it must be a field the concept declares, OR the literal "id" for a SELF-OWNED concept whose owner is the row itself -- memql#3029; id and only id, since createdBy means who WROTE the row, not whose row it is), and @rowAuthz(via="<spec>") (a relationship spec grants visibility).
A fifth FORM, not a fifth tier: @rowAuthz(owner="<field>", clusterOwner) is the composite -- the owner, OR a cluster owner (memql#4312), in either order -- and the form an operator console needs over per-user rows, since a plain owner= tier has no cluster-owner bypass. The other keys (account=, rankVisible, rankStrict, unowned=, requiresIdentity, rankFloor=) qualify a tier rather than naming one.
ENFORCED ON THE READ PATH since Phase 3 (memql#3172): declaring a tier CHANGES WHAT READS RETURN. Two mechanisms, and neither consults the filter to decide whether to engage -- the tier's predicate is ANDed into the plan before the read runs (resolved from the construct's declared binding, so the narrowing pushes down into SQL), and every row leaving the engine is separately admitted against the tier ITS OWN concept declares, which is the only mechanism available to a raw client-supplied query string, to graph expansion, or to a TOP-LEVEL BUILTIN CALL whose rows come out of a Go handler (memql#3982) -- none of which has a filter to AND anything into.
SUBSCRIPTIONS are gated by the same row admission (memql#4309): a graph.node.* event reaches a stream only if the tier admits the row for that stream's actor, a granted row arrives id-only with payload_omitted set for the client to re-read, and an UNDECLARED concept is delivered to everyone exactly as its reads already return to everyone -- the live feed mirrors the read path rather than running a second rulebook. The write side is enforced too: update/delete refuse when the target row's declared owner is not the actor (memql#3174).
Implementation: component/memql/rowauthz_enforce.go, called from parser.go. MEASURED BY TestClusterOwnerTierInjectsTheAdminGate, TestFilteredReadPathAppliesTheRowGate, TestGraphExpansionAppliesTheTraversalGateBeforeItEmitsTheRow, TestTopLevelBuiltinAppliesTheRowGate and TestSubscriptionFanOutAppliesTheRowGate -- named so a reader can check whether this is still true rather than trust the sentence. Trusting it would have been wrong before: this doc described the tier as parsed-but-unread for as long as Phase 3 had been live, which is false in the one direction that costs a reader a wrong authorization assumption, and because the doc also feeds editor hover, the error reached every author who hovered the annotation (memql#3727). See per-row-authz-audit.md and memql#2803.
@scope
| On | Written as | Example |
|---|---|---|
| seed | one string | @scope("perUser") |
Seed scope: "global" seeds once for the cluster, "perUser" once for every user.
@scrubPii
| On | Written as | Example |
|---|---|---|
| mutation | no arguments | @scrubPii |
On an update mutation (the hard-delete / data-deletion path): after the partial payload merges, zero EVERY field the bound concept marks @pii. The field set is derived from the schema, so a newly-annotated PII field is scrubbed automatically with no change to the mutation. Bare flag, no arguments. See memql#1711.
@sdk
| On | Written as | Example |
|---|---|---|
| builtin | no arguments | @sdk |
Generator marker (sdk/gen reads from source); no engine effect.
@secret
| On | Written as | Example |
|---|---|---|
| concept field | no arguments | @secret |
The field holds a secret. It is emitted as x-secret, and every validation surface that quotes a rejected value replaces the value with <redacted> while the argument name and the declared constraint (enum members, bounds, pattern) survive, so the diagnostic stays usable.
The covered surfaces come from an exhaustive enumeration of validators -- every jsonschema .Validate( call site and every value-quoting rejection message in the engine -- not from adding one entry at a time. Three incremental passes each shipped a scope statement that walked past a surface the next pass found, so extend the list by re-running the enumeration, never by appending to it.
- The function-args validator (
component/memql/function_validator.go): enum, minimum, maximum, pattern and date-time (memql#3036). - The tool-args validator (
MemQLEngine.validateToolArgs,component/memql/tool_execution.go), compiled from the same args schema and running before the function-args validator on the agent path, so it is the surface a rejected secret reaches first. Both of its exits redact (memql#3182): the message returned to the model, and the WARN, which redacts per key from the args schema instead of serializing the whole args map, and falls back to a values-freeargKeyslist when the tool's arguments cannot be classified. - The automation args binder (
component/automations/args_binding.go), which applies the same rules to event payloads: agraph.node.createdevent carries the concept row's fields flattened into its payload. The loader stamps the secret flag from the trigger topic's concept, so the enum and pattern refusals, and the WARN log they are written to, all print<redacted>, closing the one path by which a row value could reach a structured log (memql#3183). - Concept payload validation (
Concept.validate, soCreateandDelete), where@minimum,@maximumand@formatdeclared on the concept are enforced. The six jsonschema keywords that interpolate the instance value (minimum,maximum,exclusiveMinimum,exclusiveMaximum,multipleOf,format) all redact, and every other message stays byte-identical to an unannotated field's (memql#3184). - The DSL-callable
validateandpreflightbuiltins (component/memql/executor_builtin.go), which compile the same concept schema and return every leaf message to the caller in a result payload.
The last two resolve secrecy through the schema at the failing instance location, so they cover @secret at any nesting depth and inherit it onto array elements, unlike Concept.SecretFields(), which is top-level only and is not the accessor those paths use.
On the two args-validator surfaces, matching is by argument NAME, not by write target: an args field is redacted when its name appears among the bound concept's @secret fields. A mutation writing apiKey: args.credential into a @secret apiKey leaves credential unredacted, and renaming between argument and field is common, so do not rely on the write target. The concept-payload and builtin surfaces resolve through the schema and are not subject to this rule.
A @secret value is not redacted from query results: any query that projects it returns it in full. That is an authorization decision, which needs a definition of "elevated" and depends on the per-row authorization model (memql#2803).
Length is never redacted anywhere, deliberately and uniformly: value too long (N runes, max M) and the jsonschema minLength and maxLength messages report a rune count for a secret field too. They quote no value, but a length is a disclosure; redacting it on one surface would make that surface the only one that differs while withholding nothing the others already print.
Prompt input-schema validation (PromptTemplate.ValidateData, component/memql/ai_prompts.go) is unclassified: it runs a jsonschema over caller data and interpolates the instance the same way, but its schema is built from the prompt's own field list rather than from a concept, so there is no x-secret in it to read. Nothing is redacted there; treat it as uncovered, not as safe.
So @secret stops a credential leaking through a validation diagnostic. It is not a general secrecy guarantee, and not a reason to treat a credential in the graph as protected.
@serverOnly
Bars the construct from client-originated calls while leaving server-side Go free to call it (memql#2800). ENFORCED at execution against auth.CallOrigin -- unlike the retired @internal, which only hid a construct from discovery. Use only when caller-scoping is impossible: the auth path resolving sub -> user before an actor exists, or an automation acting on a user other than the actor. Callers must stamp auth.ContextWithInternalOrigin.
@serverSet
| On | Written as | Example |
|---|---|---|
| concept field | no arguments | @serverSet |
The field is stamped server-side (createdAt, createdBy, status, ...): never accepted from a mutation's caller args, but projected like any other field. Emitted as x-serverSet (memql#2035).
@shopperFormExtension
| On | Written as | Example |
|---|---|---|
| mutation | keyword arguments | @shopperFormExtension(pack="wholesale", form="application") |
@shopperFormExtension takes these keys, each written as key=value.
| Key | Type | Meaning |
|---|---|---|
pack | string | The pack whose declared shopper form this mutation extends, e.g. "wholesale". |
form | string | The form's route name on that pack, e.g. "application". |
On a mutation: this construct is ONE CLIENT DOMAIN'S ADDITION to a shopper form a PACK declares -- the fields that client collects which the pack does not know about, and the concept they are stored on. It ADDS FIELDS TO AN EXISTING PUBLIC ROUTE and never opens one: putting an endpoint on a hosted site's origin stays the privilege of Go compiled into the engine. The field list is this mutation's own args block MINUS the server-stamped names (storeId, siteId, submissionId), which the body declares so it can write them. The bff validates the pack's fields and these together BEFORE anything is written, runs the pack's construct first so the pack's own gate decides, then this mutation with the same stamped submission id. Legal on a MUTATION alone -- a logic may call builtins, and a public form pointed at one would reach them under the site owner's borrowed authority. At most one extension per form. Format: @shopperFormExtension(pack="wholesale", form="application").
@sideEffect
| On | Written as | Example |
|---|---|---|
| capability | one string | @sideEffect("write") |
On a capability: the coarse risk class @sideEffect("read"|"write"|"exec"). It is the authoritative side-effect class: it lives on the capability, not on the action that calls it, and must equal the class of the Go capability the declaration names, so an authored or generated action cannot claim a lower one.
@template
| On | Written as | Example |
|---|---|---|
| automation | no arguments | @template |
On an automation: this is a work-spine TEMPLATE, invoked by a v1:work:run that named it rather than fired by the graph (memql#5048). It is the third way an automation can be reachable, alongside an event trigger and a schedule. A @template automation must carry no @trigger -- the load-time gate refuses the combination, so "called" and "triggered" stay distinct.
@templateFile
| On | Written as | Example |
|---|---|---|
| seed | one string | @templateFile("templates/assistant.tmpl") |
| prompt | one string | @templateFile("prompts/consolidateMemory.tmpl") |
- On a seed: A template file whose rendered text is the seeded row's content.
- On a prompt: The prompt's template: a .tmpl file beside the prompt, rendered with the input fields.
@trigger
| On | Written as | Example |
|---|---|---|
| automation | keyword arguments | @trigger(event="node.created", concept="v1:cluster:node") |
@trigger takes these keys, each written as key=value.
| Key | Type | Meaning |
|---|---|---|
before | string | Adjust the incoming row before create, update, or write; requires concept. |
event | string | Event pattern, e.g. "node.created" (with concept=) or a raw topic such as "system.startup". |
concept | string | Concept id the triggering event targets; required by the structured node.* event kinds. |
schedule | string | Cron schedule with a leading seconds field, e.g. "0 0 * * * *". |
filter | expression | The trigger filter as a keyword: a lambda of one parameter over the triggering row, filter=row => <predicate>. The standalone @filter(...) annotation is the usual spelling and sets the same filter. |
on | string | A synonym for event=: on=<concept>.<created|updated|deleted>, with the concept named through the file's use import, folds to the same graph.node.<action>.<concept> pattern event= names (resolved by the automation loader and the concept resolver). A later epic retires the synonyms (D15/D17). |
Event trigger for automations. Format: @trigger(event="graph.node.created.*.v1:ns:concept") or @trigger(schedule="0 0 * * * *").
@type
| On | Written as | Example |
|---|---|---|
| concept | one string | @type("collection") |
The concept's row kind: "object" (the default), "collection" or "reference".
@unbounded
| On | Written as | Example |
|---|---|---|
| query | one string | @unbounded("a small catalog of fixed size") |
On a list-returning query: opt out of the pagination authoring rule and the implicit 50-row runtime cap. Format: @unbounded("reason"). The reason string is REQUIRED -- it documents why this query is a legitimate full-set read (small bounded catalog, sweep job, etc.) and is enumerated by the pagination audit report. A query that paginates/sorts is already bounded and must NOT carry @unbounded; the engine clamps the realized window to MEMQL_MEMORY_ENGINE_MAX_WINDOW regardless. See authoring-rules.md.
@variant
| On | Written as | Example |
|---|---|---|
| concept field | keyword arguments | @variant(discriminator="kind") |
@variant takes these keys, each written as key=value.
| Key | Type | Meaning |
|---|---|---|
discriminator | string | The field whose value picks the branch. |
A discriminated union: @variant(discriminator="kind") on an object field, followed by one block per branch; the discriminator field's value picks the branch.
@vendor
| On | Written as | Example |
|---|---|---|
| provider | one string | @vendor("OpenAI") |
The provider's type, which picks the client that serves it: OpenAI (or OpenAIChat), OpenAITTS or OpenAIEmbedding for OpenAI, and Anthropic (or AnthropicChat) for Anthropic, matched without regard to case. Fleet and SubscriptionApp are accepted on a @base provider only: their models are named from a policy (fleet:<model>, app:<id>) rather than declared as children. Any other type leaves the provider registered but unavailable (unsupported provider type). Streaming is a parameter (streaming true in params), not a type, and a child that @extends a base takes the base's type.
@version
- On a concept: Version tag for the concept: a semver string, @version("1.0.0"). Metadata only -- canonical ids are not versioned by it (#2613).
- On a seed: Version tag for the seed.
@when
| On | Written as | Example |
|---|---|---|
| rule | empty parentheses or keyword arguments | @when(level="fast") |
@when takes these keys, each written as key=value.
| Key | Type | Meaning |
|---|---|---|
level | string | The level the call declared: fast, strong, reasoning or embeddings. |
modality | string | The modality derived from the call site: chat, streamingChat, tools, streamingTools, structured, vision, embedding, speech, transcribe. |
prompt | string | The DSL prompt this call renders. Empty matches a Go call site with no prompt. |
role | string | The AGENT's role slug. Distinct from actorRole: this is what is acting. |
actorRole | string | The calling human's cluster role. Distinct from role: this is who is watching. |
tag | string | A call tag, e.g. "background" or "backgroundEscalation". |
touches | string | A concept id PREFIX the call's footprint matches (startsWith semantics). |
On a rule: the condition set, as keyword arguments. Closed keys, every one optional, all present keys ANDed: level, modality, prompt, role, actorRole, tag, touches. @when() with no arguments matches every call.
Retired
A retired name is refused where the table says, with annotation_retired and a hint that says what to write instead.
| Annotation | Where | Write instead |
|---|---|---|
@actor | spec and trait | It is a shape-only marker since epic #2281 -- to predicate on the caller, bind an @actor shape in the signature (spec <shape> <name>) and read its projected key by bare name. |
@async | everywhere | Refused on automations since memql#2712: an automation already runs asynchronously off its event or schedule trigger, and nothing reads the annotation -- delete it. |
@audit | everywhere | Removed from the allow-lists in memql#989; nothing reads it -- delete the annotation; run memqlmigrate --rewrite=attributes. |
@cache | concept | A concept carries no cache setting -- a read caches, so set the TTL on the query that reads the concept (@cache(300)); a write to the concept evicts every cached read of it. |
@caller | shape | Use @actor (#221); the field accessor was renamed at the same time (caller.X -> actor.X). |
@clientExecution | tool | It dispatched the tool to the connected browser over the client-tool relay, which was removed with the cognition node (epic memql#4988). Every tool now needs a server-side @handler. |
@concepts | shape | Bind the concept in the signature instead: shape <Concept> <name> { ... }, with the concept imported by a file-top use line. |
@default | concept field | Memql#5375: it is never applied on insert -- it was published as the JSON-Schema default keyword, which no validator applies; write args.<field> ?? <default> in the mutation body. It stays live on a tool / prompt / builtin field, where the body IS the schema the model reads; run memqlmigrate --rewrite=attributes. |
@default | args field | It is never applied; write args.<field> ?? <default> in the body (a concept-field @default is not a substitute -- it is never applied on insert either). |
@deprecated | everywhere | Memql#5375: it was rendered by help() and editor hover and read by nothing that changes behaviour -- delete it, or say so in the construct's @description; run memqlmigrate --rewrite=attributes. |
@description | args field | It was never retained (no AST slot); document the field with a /// doc comment on the line above it (memql#3336). |
@enabled | everywhere | Memql#5375: constructs are enabled by default, so @enabled was an explicit no-op that read like a switch -- delete it, and use @disabled to deactivate; run memqlmigrate --rewrite=attributes. |
@idempotent | everywhere | Removed from the mutation allow-list in memql#989; nothing reads it -- delete the annotation; run memqlmigrate --rewrite=attributes. |
@immutable | everywhere | Memql#5375: declared metadata with no write guard behind it -- delete it; a field that must not change is enforced by the mutation that writes it; run memqlmigrate --rewrite=attributes. |
@internal | everywhere except concept field | Retired under the 2026.08 epoch (#2620 ruling / #2708); it only hid the construct from external discovery surfaces (tool listing, MCP promotion, the help()/listFunctions internal flag) while leaving it callable -- delete the annotation. |
@kind | action | An action is always one primitive capability call now, and a composite of several is an automation, so there is nothing to mark; remove it. |
@latestMode | everywhere | Memql#5375: the engine derives time-dependence from asOf latest in the body, so the annotation restated it and could contradict it -- delete it; run memqlmigrate --rewrite=attributes. |
@namespace | everywhere | Memql#5375: a concept's namespace is its domain directory, or that directory's one-line namespace.pin -- the annotation could only restate one of those or silently disagree with it; delete it, and pin a deliberate divergence with a namespace.pin file (#2614); run memqlmigrate --rewrite=attributes. |
@nocache | everywhere | Memql#5375: write @cache(0) -- one annotation for the cache TTL, with 0 meaning never; run memqlmigrate --rewrite=attributes. |
@permission | everywhere | Buried (#2631 ruling close-out / #2713); the @role twin -- documented but never enforced (its one help-payload reader was dead; the load gate rejects it) -- access control lives at the actor layer (RBAC + the @public per-row-authz classification). |
@rateLimit | everywhere | Memql#5375: Tool.RateLimit was cloned and copied into a Function field nothing reads, so the declared ceiling did not exist -- delete it; the live ceilings are the provider chokepoint in ai_guard.go and the run budget in component/work; run memqlmigrate --rewrite=attributes. |
@reliability | action | Reliability is runtime state the engine keeps, not something the source declares; remove it. |
@retry | everywhere | Removed from the allow-lists in memql#989; nothing reads it -- delete the annotation; run memqlmigrate --rewrite=attributes. |
@role | everywhere | Buried (#2631 ruling / #2709); it was documented but never enforced (nothing ever checked the value at runtime; the load gate rejects it) -- access control lives at the actor layer (RBAC + the @public per-row-authz classification). |
@row | spec and trait | It is a shape-only marker since epic #2281 -- to predicate on row metadata, bind a @row shape in the signature (spec <shape> <name>) and read its projected key by bare name. |
@schedule | automation | A scheduled automation is written @trigger(schedule="<cron>"), the one spelling (D15, epic memql#5370); memqlmigrate --rewrite=bodies rewrites it. |
@scope | concept | Remove the annotation; every concept lives in the default partition post-#56. |
@scopes | everywhere | Memql#5375: Tool.Scopes was advertised on the gRPC tool descriptor and checked nowhere, so it read as an authorization gate while gating nothing -- delete it; use @requiresCapability for a real one; run memqlmigrate --rewrite=attributes. |
@shape | spec and trait | A spec binds its shape or concept in the signature (epic #2281): spec <boundName> <name> = row => <predicate>, with boundName resolved through the file-top use import. |
@sideEffect | action | The authoritative side-effect class lives on the capability declaration the action calls, where an action cannot overstate or understate it; remove it from the action. |
@timeout | everywhere | Removed from the allow-lists in memql#989; nothing reads it -- delete the annotation; run memqlmigrate --rewrite=attributes. |
@unique | everywhere | Memql#5375: declared metadata with no uniqueness check behind it (memql#2960), so it read as a constraint while constraining nothing -- delete it; run memqlmigrate --rewrite=attributes. |
@useX, for any X that starts with an upper-case letter | everywhere | Declare the dependency with a file-top use <module>.{ ... } import instead, and put a bound concept in the signature (query <Concept> <name> { ... }). |
@visibility | everywhere | Removed in the genesis simplification; it chose which node types load a construct, and every binary now loads everything while build tags decide which integrations are active -- delete the annotation. |
Refusals
Every refusal names the construct and the annotation, says what to write instead, and ends with its code in square brackets, as in [annotation_unknown].
| Code | Meaning |
|---|---|
annotation_unknown | No construct or field accepts the name. The refusal suggests the nearest name the construct does accept, and lists them all. |
annotation_retired | The name is retired where it was written. The refusal carries the migration hint, which says what to write instead. |
annotation_misplaced | Other constructs or fields accept the name, but not this one. The refusal names where it is accepted and shows it written there. |
annotation_form | The construct accepts the annotation, but not written this way: a string where it takes a number, or arguments on a flag. The refusal says what it takes and shows its example. |
annotation_key | A keyword argument the annotation does not have, or a key written in the wrong shape: a flag given a value, or a valued key written bare. The refusal lists the keys and shows the example. |
annotation_repeated | An annotation that is not repeatable is written twice on one declaration, so a reader cannot tell which one takes effect. Keep one. |