Cross-cutting convention for any skill that proposes a change to the
active schema pack. Read first before invoking schema-author. The
goal: keep the pack small enough that an agent can hold the whole type
graph in its head, but expressive enough that custom domains
(research, legal, founder ops) get first-class types.
You see a cluster of pages that share a domain meaning.
│
▼
How many pages in the cluster?
│
┌─────┴───────┬──────────────┐
▼ ▼ ▼
<20 20-100 100+
│ │ │
▼ ▼ ▼
One-off. Big enough. First-class.
Don't pack- Add an alias Add a new
codify. to an existing page_type with
type OR a its own prefix,
Use the narrow prefix primitive, and
nearest branch. flags.
existing
type +
frontmatter
tag.
One-off (don't add to pack):
"I have 3 pages under
2026-projects/skunkworks-spec/. Should I add askunkworkstype?"
No. Three pages doesn't justify a permanent pack entry. Type these as
the nearest existing match (concept or note) and use a frontmatter
project: tag. If the cluster grows to 20+, revisit.
20-100 pages — alias OR narrow prefix:
"I have 50 pages under
people/researchers/that overlap with mypersontype. Should I add aresearchertype?"
Two valid options:
- Alias on
person—add-alias person researcher. Closure queries forresearcherwill surfacepersonrows too. - New type sharing the
entityprimitive —add-type researcher --primitive entity --prefix people/researchers/. Distinct type, can be marked--extractableor--expertindependently.
Pick alias when researchers are people first, researchers second (they share enrichment rules, expert-routing semantics, link verbs). Pick new type when researcher-specific behavior diverges (different extractable rules, different link verbs, different rubric).
100+ pages — first-class type:
"I have 4000 pages under
meetings/. I want them typed asmeeting, not the legacy defaultnote."
Add the type:
gbrain schema add-type meeting \
--primitive temporal \
--prefix meetings/ \
--extractable
gbrain schema sync --apply
The sync --apply backfills all 4000 pages. From here forward,
imports under meetings/ infer meeting type via the pack.
- Don't add a type for a directory you imported once for triage. Pack types are permanent decisions; one-time imports are not.
- Don't add a type just to silence
dead_prefixesinschema stats. A dead prefix is a signal that the prefix is mis-declared or the corpus moved. Remove the prefix or migrate the content, don't add an empty type. - Don't promote a candidate from
schema suggestwithout verifying the path prefix matches real content. The suggester is heuristic; it can propose types that overlap existing ones. Runlint --with-dbbeforeadd-typeto catch prefix collisions pre-write. - Don't add
--expertto a type that has nopath_prefixes. Theexpert_routing_without_prefixlint rule warns about this exact shape: an expert-routed type with no prefix never matches a put_page inference, sowhoknowssilently never surfaces it. - Don't mutate
gbrain-baseorgbrain-recommended. Fork first.
Removing a type is RARE. Only do it when:
- The type was added in error (typo, premature abstraction).
- The corpus the type was meant for has been migrated to a different type.
- The type is dangling (no
path_prefixesactually match pages, no queries reference it, no other type's aliases/link_types reference it).
remove-type is guarded by the STILL_REFERENCED check (codex C14): if
ANY other type's aliases / enrichable_types / link_types / frontmatter_links
references the target, the remove fails loud with the reference list.
Break those references first.
If your pack lives in source control (~/.gbrain/schema-packs/<name>/
is a git repo), commit after every batch of mutations. The
mutation_count_anomaly lint rule warns at >50 mutations in 7 days —
that's the hint to start committing rather than relying on disk-only
state.
A pack can declare migration_from: {pack: <name>, version: <semver-range>}
to register itself as the successor to another pack. When a brain's
active pack matches the declared from, the pack_upgrade_available
onboard check surfaces the successor + a manual_only RemediationStep
pointing at the unify-types PROTECTED Minion handler.
v0.41.22 ships gbrain-base-v2 as the declared successor to
gbrain-base@1.x — collapses 94 noisy types to 15 canonical via
declarative mapping_rules. Run via gbrain onboard --check --explain
(preview) → gbrain jobs submit unify-types --allow-protected --params '{"target_pack":"gbrain-base-v2"}' (apply). See
skills/schema-unify/SKILL.md for the full playbook.
Authoring a successor pack: declare
migration_from: {pack: <parent>, version: "1.x"} in the manifest
plus mapping_rules: (discriminated union over retype / page_to_link /
page_to_alias kinds). Catch-all sentinel from_type: '*unknown*' MUST
appear last. Subtype_field is restricted to ALLOWED_SUBTYPE_FIELDS
(subtype, legacy_type, origin, format, kind, period, domain) per
codex D9 — third-party packs cannot inject title / slug / type.
When NOT to upgrade:
- Custom types not covered by the successor's mapping_rules → fork the
successor first (
gbrain schema fork gbrain-base-v2 my-pack), edit rules, then target your fork. - Mid-ingest or autopilot maintenance → wait. Unify holds the
gbrain-unifydb-lock for ~10 min on big brains. - Federated brain with sources you don't want to touch → scope per
source via
--params sourceId.