This document explains schema usage, metadata shape, and composition semantics in structured-context.
A schema defines the valid structure for nodes in a space: entity types, field constraints, hierarchy behavior, type aliases, and executable rules.
structured-context uses JSON Schema Draft-07 plus a custom top-level $metadata keyword.
Set schema in the space config entry:
{
"name": "my-space",
"path": "/path/to/space",
"schema": "schemas/strict_ost.json"
}Resolution order: space schema > global schema. Schema resolution fails if none is configured.
Flexible planning schema spanning strategy + OST-like flow.
Main types:
visionmissiongoal(alias:outcome)opportunitysolutionexperiment(aliases:assumption_test,test)
Canonical 4-level OST structure.
Main types:
outcomeopportunitysolutionassumption_test
This schema composes shared structural defs and strict metadata/rules from partials.
structured-context registers the following format annotations beyond standard JSON Schema. All apply to string properties and are validated at schema validation time.
| Format | Validates | Example |
|---|---|---|
date |
ISO 8601 date (YYYY-MM-DD) |
"2026-03-31" |
path |
Non-empty filesystem path — absolute, relative, or a plain name | "notes", "./subdir/file.md", "/abs/path" |
wikilink |
Obsidian wikilink syntax ([[...]]) |
"[[Parent Node]]" |
path and wikilink are also available as shared $ref definitions in _sctx_base.json:
{ "$ref": "sctx://_sctx_base#/$defs/wikilink" }Using format directly is more concise when the full definition isn't needed:
{ "type": "string", "format": "wikilink" }YAML parsers (gray-matter, js-yaml) coerce unquoted ISO dates to JavaScript Date objects:
published_date: 2026-03-31 # parsed as a Date object by gray-matterThe markdown plugin automatically coerces Date objects to YYYY-MM-DD strings before validation, so unquoted dates in frontmatter and embedded YAML blocks work correctly with format: "date" fields.
Schemas use this metaschema URL:
https://raw.githubusercontent.com/mindsocket/structured-context/main/schemas/generated/_structured_context_schema_meta.json
Top-level metadata shape:
{
"$metadata": {
"hierarchy": {
"levels": [
"outcome",
{ "type": "opportunity", "selfRef": true },
"solution",
"assumption_test"
],
"allowSkipLevels": false
},
"aliases": {
"experiment": "assumption_test"
},
"rules": [
{
"id": "active-outcome-count",
"category": "workflow",
"description": "Only one outcome should be active at a time",
"scope": "global",
"check": "$count(nodes[resolvedType='outcome' and status='active']) <= 1"
}
]
}
}| Field | Type | Notes |
|---|---|---|
hierarchy |
object | Optional per provider; at most one provider may define it after composition |
hierarchy.levels |
(string | HierarchyLevel)[] |
Ordered root→leaf types |
hierarchy.allowSkipLevels |
boolean |
Optional; allows parent to be any ancestor level |
relationships |
Relationship[] |
Optional; defines related node links outside the primary hierarchy |
aliases |
Record<string, string> |
Optional type alias map |
rules |
Rule[] |
Optional flat rule array |
Relationships define links between node types that are not part of the primary structural hierarchy. They are handled during parsing and template generation.
| Field | Type | Default | Description |
|---|---|---|---|
parent |
string |
Required | The parent's canonical type name |
type |
string |
Required | The child's canonical type name |
field |
string |
"parent" |
The frontmatter field that holds the wikilink(s) for this relationship |
fieldOn |
string |
"child" |
"child" (child holds a link to parent) or "parent" (parent holds an array of child links) |
format |
string |
"page" |
Hint for template-sync: "table", "list", or "heading" |
matchers |
string[] |
[] |
Heading text to match (strings or /regex/). Case-insensitive. |
embeddedTemplateFields |
string[] |
[] |
Field names to include in templates when format is "table" |
multiple |
boolean |
true |
Whether multiple children are expected |
fieldOn: "child" (default) — child node has a field pointing to its parent. Embedded parsing sets this field on each child node; validation checks that it resolves to a node of the declared parent type.
fieldOn: "parent" — parent-side array — the parent node has an array field (field) holding wikilinks to child nodes. When field is required, it must be specified. Embedded parsing appends [[Child]] entries to the parent node's field array; validation checks that each entry resolves to a node of the declared child type.
Example — child-side (default):
"relationships": [
{
"parent": "opportunity",
"type": "assumption",
"format": "table",
"matchers": ["Assumptions", "/assum.*/"],
"embeddedTemplateFields": ["assumption", "status", "confidence"]
}
]Example — parent-side array:
"relationships": [
{
"parent": "activity",
"type": "task",
"field": "tasks",
"fieldOn": "parent",
"format": "list",
"matchers": ["Tasks"],
"multiple": true
}
]With this configuration, embedded task items under an Activity's "Tasks" heading populate activity.tasks as [[Task Title]] wikilinks, and validation confirms each entry resolves to a task node.
HierarchyLevel options:
| Option | Default | Meaning |
|---|---|---|
type |
required | Canonical type name |
field |
"parent" |
Frontmatter field holding wikilink(s) |
fieldOn |
"child" |
"parent" means the parent points to children |
multiple |
false |
Field contains array of wikilinks |
selfRef |
false |
Allows same-type parent |
selfRefField |
undefined | Separate field for same-type parent links |
format |
undefined | Embedding hint: "list", "table", or "heading" — enables hierarchy embedding when set |
matchers |
undefined | Heading text patterns (strings or /regex/) to detect embedding sections |
embeddedTemplateFields |
undefined | Column/field names for table stubs generated by template-sync |
String shorthand ("goal") normalizes to:
{ "type": "goal", "field": "parent", "fieldOn": "child", "multiple": false, "selfRef": false }.
When a hierarchy level has format and matchers, it participates in hierarchy embedding — the same section-based parsing used for relationships. Two patterns are supported:
Child-level embedding — a typed-page heading signals that following content should produce nodes of the child type (next level in hierarchy):
"levels": [
"goal",
{
"type": "opportunity",
"field": "parent",
"fieldOn": "child",
"format": "list",
"matchers": ["Opportunities", "User Opportunities"]
}
]With this config, a goal page with ### Opportunities followed by a list creates opportunity nodes without explicit [type:: opportunity] annotations.
Bare wikilinks in embedded lists — when a list item is a bare wikilink (- [[Node Title]]) inside an embedding section, it populates a field on the parent without creating a new node:
### tool
- [[Zephyr]] ← populates activity.tools = ["[[Zephyr]]"]
- New Custom Tool ← creates a new tool nodeThis requires fieldOn: "parent" (the parent node holds the array field).
Parent-level references (fieldOn: "child", child holds the field) — a heading matching the immediate parent type lets a node reference its parents by wikilink:
"levels": [
{ "type": "capability", "field": "parent", "fieldOn": "child", "multiple": false },
{
"type": "application",
"field": "capabilities", // application nodes have a capabilities field
"fieldOn": "child",
"multiple": true
}
]With this config, an application page may include ### capability with wikilink items to populate application.capabilities:
## My Application ^application1
### capability
- [[Task Management]] ← populates application.capabilities
### tool
- [[Zephyr]] ← populates application.tools (fieldOn: parent)Metadata is composed across the $ref graph with deterministic behavior:
- Traverse external
$refgraph in DFS order. - Apply root schema metadata last.
Merge rules:
hierarchy: may be defined in partials; last one wins. This allows partials to define a default hierarchy that composing schemas can override.aliases: shallow merged; later file wins per key.relationships: collected from all files; order preserved.rules: merged byid.- Duplicate rule
idwith different payload errors by default. - A later rule may replace an earlier one only with
"override": true.
When no provider defines hierarchy, hierarchy-based behavior is disabled (show tree shape, hierarchy validation, parent-edge checks). space_on_a_page parsing still requires hierarchy and will error without it.
Inside $metadata.rules, entries can be inline rules or $ref imports:
"rules": [
{ "$ref": "sctx://my-pack#/$defs/workflowRule" },
{ "$ref": "sctx://my-pack#/$defs/ruleSet" }
]Import targets may be:
- a single rule object
- an object containing
rules: []
Imported rules are normalized into one executable flat list before validation.
{
"$metadata": {
"rules": [
{
"id": "active-outcome-count",
"override": true,
"category": "workflow",
"description": "Require exactly one active outcome",
"scope": "global",
"check": "$count(nodes[resolvedType='outcome' and status='active']) = 1"
}
]
}
}- Files starting with
_are auto-loaded partials. - Both bundled partials and local schema-directory partials are registered.
- Local partial
$idvalues must not collide with bundled IDs. $refresolution is transitive across files.- Partials with no
$metadatashould prefer$schema: "http://json-schema.org/draft-07/schema#"so they validate standalone as plain JSON Schema fragments. - Bundled partials as entity libraries:
_sctx_base.json,_strategy_general.json,_knowledge_wiki.json, and_ost_strict.jsonprovide reusable entity definitions and metadata. Composing schemas can reference these via$refrather than redefining common entity types. - Partials can carry metadata: Partials may include
$metadata(hierarchy, aliases, relationships, rules). This makes them self-contained units that bundle both type definitions and behavioral metadata.
Use the shipped metaschema URL in $schema for best cross-tool behavior.
Notes:
- Custom
$idvalues likesctx://...are still supported by the CLI registry. - Some generic editors may not resolve custom URI schemes for
$ref; CLI behavior is authoritative. - Do not rely on editor-only mappings for runtime correctness.
For schemas migrating from older metadata structure:
- Move any legacy metadata from
$defs._metadatato top-level$metadata. - Convert
hierarchyarray tohierarchy.levelsobject shape. - Move
allowSkipLevelsunderhierarchy. - Convert grouped rule containers to flat
rules[]with per-rulecategory. - If duplicate rule IDs are intentional, mark later rules with
override: true. - Re-run
sctx schemas show --space <name>andvalidateto confirm merged metadata/rules.
Schema files are parsed as JSON5 (// comments and trailing commas are allowed).