Skip to content
TagProcessor Interface
AutoXXS (320px)XS (375px)SM (640px)MD (768px)LG (1024px)XL (1280px)XXL (1536px)
SketchMaterialiOSTamagui
DataInjectionKeyPatternsServiceTransactionProcessResearchProductQualityPerformanceSpecDomainFunctionTechnologyArchitectureConfigMiddlewareDataDatabaseDrizzleMigrationModelop-sqliteSchemaSQLState ManagementDraftKeystoneMergePatchPatchesPersistenceReactiveRedoStoreUndoTestingDeviceFactoryIsolationTypeScriptZodTopicsCommunicationBidsNVCDesignDesign ImplicationsEducationPedagogyFoundationsPsychologyAttachmentFloodingRelatingAuthentic RelatingUIEditorReact Native

TagProcessor Interface

Fragment reference TagProcessor Interface
tags
instruction

The TagProcessor<T> interface

TagProcessor<T> generic interface definition
compilerinterfacetypescript tagprocessor-interface
packages/compiler/src/registry.ts
export interface TagProcessor<T = unknown> {
tagName: string;
/** Bundle property path where extracted data should be stored (e.g., 'api.actions', 'requirements') */
bundlePath?: string;
/** If true, promote tag's id/title to bundle root (default: false) */
promotesIdentity?: boolean;
/** If set, this tag's data will be collected into the named aggregate bundle */
aggregatePath?: string;
/** Key name for the source bundle ID in the aggregate entry (e.g., 'domainId', 'featureId'). Defaults to 'sourceId' */
aggregateIdKey?: string;
/** Authoritative Zod schema for this tag's attributes (set to TagClass.Schema). Used by generate-schema.ts to emit markdoc.config.mjs. */
zodSchema?: z.ZodObject<any>;
/** Schema definition for editor support (attributes, descriptions, types) */
schema?: {
attributes?: Record<string, SchemaAttribute>;
description?: string;
};
/** If true, the agent-pass walk() continues into children after this processor fires. */
isContainer?: boolean;
/**
* Declares where documents of this type appear in the public sidebar.
* `section` is the top-level section; `group` is the optional sub-group label.
* `weight` controls ordering within the section (lower = earlier).
*/
sidebar?: {
section: 'Product' | 'Specification' | 'Implementation' | 'Journal';
group?: string;
weight: number;
collapsed?: boolean;
groupByDomain?: boolean;
};
match(node: Markdoc.Node): boolean;
extract(file: string, node: Markdoc.Node, ast: Markdoc.Node, context: CompilerContext): T;
render?(fm: Frontmatter, ast: Markdoc.Node, data: T, context: CompilerContext): string;
merge?(bundle: Record<string, unknown>, data: T): void;
}

Declarative properties

Most wiring is handled automatically through declarative properties on the processor:

PropertyPurpose
bundlePathDot-path where extract() data is pushed in the agent bundle (e.g. 'api.actions', 'requirements'). Replaces manual merge() for leaf processors.
promotesIdentityWhen true, the tag’s id and title are promoted to the bundle root. Used by root document tags (feature, domain, role, etc.).
aggregatePathName of the aggregate bundle this tag feeds (e.g. 'glossary', 'policies'). Items are collected automatically from all documents.
aggregateIdKeyKey name for the source document ID in aggregate entries. Defaults to 'sourceId'.
zodSchemaTag class’s static Schema (e.g. Rule.Schema). Used by generate-schema.ts to auto-generate markdoc.config.mjs β€” no manual schema registration needed.
isContainerWhen true, the agent-pass walker descends into children after this processor fires. Without it, child tags with processors are silently skipped.
sidebarDeclares where this document type appears in the public Starlight sidebar (section, group, weight).

Recommendation: For complex tags, the processor should act as a thin adapter that delegates to a Core Model (see core/tags/). This encapsulates validation, tree traversal, and rendering logic in a testable class.

Core Model adapter delegation pattern
compilerpatterncore-model processor-core-model-delegation
extract(file: string, node: Node, ast: Node, context: CompilerContext) {
return new MyTagModel(node).extract();
}

The extract() signature

extract(file: string, node: Markdoc.Node, ast: Markdoc.Node, context: CompilerContext): T
ParameterDescription
fileRelative content file path
nodeThe matched Markdoc tag node
astFull document AST root β€” use for cross-node lookups within the same file
contextCompiler context β€” tracker, cross-document index maps, entity links

The render() signature

render(fm: Frontmatter, ast: Markdoc.Node, data: T, context: CompilerContext): string
ParameterDescription
fmParsed frontmatter of the current document
astFull document AST root β€” use ast.children?.find(...) to locate nodes
dataThe value returned by extract() for this document
contextCompiler context β€” tracker, cross-document index maps, entity links

ast is the full document root, not the matched node. Processors navigate back to their node inside render via ast.children?.find(n => n.tag === this.tagName).

Bundle placement β€” how extract() data reaches the agent bundle

The agent pass determines where extract() data goes based on the processor’s declarative properties, checked in this order:

  1. merge() β€” if the processor implements merge(), it is called directly. Use for top-level processors that own the entire bundle shape (feature, domain, role, story, milestone, manifest).
  2. bundlePath β€” if set, data is pushed into the bundle at the specified dot-path via ObjectPath.push(bundle, bundlePath, data). Use for leaf processors that produce array items (requirement, criterion, policy, action, event, term).
  3. promotesIdentity β€” if true, the tag’s id and title are promoted to the bundle root (already handled before merge/bundlePath).
  4. Fallback β€” data fields are spread directly onto the bundle (legacy path).

Rule of thumb: if extract() returns an object to be spread into the top-level bundle, implement merge. If it returns a single item to append to an array, set bundlePath to the target array path.

Registering child tag processors

For child tags whose data is parsed by their parent processor and only need schema visibility in the editor, use the shorthand helpers instead of writing a full processor:

// Preferred β€” derives schema from the Tag domain class
registerTagClass(MyChildTag, 'Description of the child tag');
// Legacy β€” manual attribute map
registry.register(schemaOnly('childtag', { name: { type: 'string', required: true } }, 'Description'));

Graceful Validation β€” Compiler Pass Invariant

The compiler pass must never throw or crash due to validation errors. All validation β€” schema checks, required attributes, naming conventions, cross-document references β€” is graceful. Issues are collected by the ValidationTracker singleton (tracker) and reported after the pass completes. The pass always finishes processing all files.

This invariant applies to:

  • extract() implementations in processors β€” wrap suspicious attribute reads defensively and emit via tracker.addRuntimeIssue() instead of throwing
  • Aggregate-building utilities β€” sort comparators, map/filter helpers, and any post-processing over collected bundles must guard against undefined fields (e.g. (a.term ?? '').localeCompare(b.term ?? ''))
  • passes/agent/index.ts β€” the agent pass sorts, filters, and maps all bundles; never assume a field is present

How to record an issue without crashing:

tracker.addRuntimeIssue(file, 'Descriptive message', 'error', 'tag:tagname.attribute');
ParameterValues
fileRelative content file path
messageHuman-readable description
severity'error' or 'warning'
pathDot-path of the failing attribute (used for deduplication in the lint workflow)

Never use throw inside a processor or pass for validation failures β€” always use tracker.addRuntimeIssue.