docsConcepts

Concept Seeding

Platform baseline rows -- the records that should exist in every fresh deployment without hard-coding payloads inside Go services -- are declared directly in the DSL with the seed construct. A seed declaration lives in a dsl/<namespace>/*.memql file next to the concept it materializes:

memql
use agents.concepts.{ agentRole }
 
@description("Public-services orientation: DMV, voter registration, permits, social services, and government benefits.")
seed agentRole civic-navigator {
slug: "civic-navigator"
name: "Civic Services Navigator"
category: "civic"
lockedSkillIds: ["workbench-baseline", "civic-baseline"]
maxSkills: 5
recommendedPolicySlug: "balancedChat"
predefined: true
}

The signature is seed <concept> <name> { ... }. The body is a field list; each field maps to a same-named argument of the concept's canonical create mutation (see "Write path" below).

Scope: global vs. per-user

A seed's scope is stamped with @scope:

  • @scope("global") (the default when omitted): the seed materializes exactly one row, scoped instance-wide rather than per-user (there is no physical partition column backing this -- @scope is a materialization strategy, not a storage location). Used for catalog rows such as agent roles and skills.
  • @scope("perUser"): the seed fans out to one row per v1:identity:user. The materializer computes the row id as <seedName>-<userId> and stamps ownerUserId=<userId> automatically. A perUser seed body must NOT declare its own id -- the loader rejects that. Used for per-user baselines such as the Trainer Agent (dsl/agents/trainerAgent.memql) and the Planner Agent (dsl/agents/plannerAgent.memql).

Long prose fields can be sourced from a template file next to the declaration via @templateFile("templates/<name>.tmpl").

When seeds materialize

The SeedMaterializer (component/memql/seed_materializer.go) runs two trigger paths:

  1. Startup sweep. On engine start, every registered seed is walked and materialized: global seeds become one row each; perUser seeds iterate every existing v1:identity:user and produce one row per user.
  2. Runtime hook. After the sweep, the materializer subscribes to user-creation events. When a new user lands, it re-runs the perUser sweep for just that user. Global seeds are skipped (they do not fan out per user).

Idempotency

Materialization is create-only with deterministic ids: MemQL is time-series, so repeat inserts with the same id stamp a new version while reads still see one logical row. Operator or user edits to a seeded row (e.g. renaming an assistant) survive engine restarts -- the seed declaration is the baseline, not a reset.

Write path

The materializer delegates row writes to the concept's existing canonical create mutation, so the platform has a single write path. The convention:

text
use <namespace>.<conceptName> -> create<ConceptName>

Not mutationCreate<ConceptName>. This page and five comments in component/memql/seed_materializer.go said that, and it was accurate until memql#2036 ("C6: de-prefix construct names + migrate mutation->mutate"), which changed the builder from "mutationCreate" + ucFirst(conceptName) to `"create"

  • ucFirst(conceptName)and renamed the corpus to match -- while sweeping neither this page nor those comments. In the months since, a seed author following the stale wording named their mutationmutationCreateFoo, the materializer looked for createFoo`, and the write failed at runtime.

That is a sharper version of the #2806 lesson, and the reason the naming rule is now gated rather than merely written down: the rename was deliberate and correct, and its own documentation still drifted, because nothing was checking. Corrected in memql#2853.

Each seed body field maps to a same-named mutation arg, and the concept's id arg follows the case-corrected <conceptName>Id pattern (agent -> agentId, partition -> partitionId).