Specification
Specification v0.3
The full mdbase specification for typed Markdown collections.00. Overview
Abstract
This specification defines the behavior of tools that treat folders of Markdown files as typed, queryable, link-aware data collections. It covers collection discovery, JSON Schema types, validation, links, CEL queries, ordinary records that save named views, record operations, lifecycle policy, and optional runtime workflows.
Motivation
Markdown files with YAML frontmatter are a common way to store structured content. The pattern appears in static site generators, knowledge management tools such as Obsidian, documentation systems, agent workspaces, and AI agent frameworks that use Markdown for persistent state.
These ecosystems need consistent conventions for frontmatter structure, validation, querying, links, and updates. This specification defines one coherent set of behaviors so that:
- a CLI tool and an editor plugin can operate on the same files with consistent semantics
- an AI agent can read and write Markdown files that a human can inspect and edit
- tool authors can implement a shared behavior contract
- collections can move between conforming tools
Intended implementers
CLI tools for querying, validating, and manipulating Markdown collections from the command line.
Editor plugins for applications such as Obsidian and VS Code that provide validation, completion, navigation, and query interfaces.
Libraries in different languages that applications can use to work with typed Markdown.
AI agent frameworks that need structured, human-readable persistent storage.
Runtime hosts that connect collection events to declared actions and workflows.
What a conforming tool does
Depending on its conformance profiles and optional features, a tool implementing this specification:
- Recognizes collections by the presence of an
mdbase.yamlconfiguration file. - Loads type definitions from Markdown files that wrap JSON Schema.
- Matches records to types through explicit declarations or configured match rules.
- Validates frontmatter against matched schemas and collection-aware rules.
- Resolves links between records and exposes link-aware metadata.
- Executes queries using CEL expressions for filtering, ordering, and projection.
- Executes saved view records when it advertises the optional
view_recordsfeature. - Performs record operations with validation, reference handling, and lifecycle-managed values.
- Exchanges events and actions and runs durable workflows when it claims the corresponding companion profiles.
Conformance profiles define the expected behavior for each capability and its dependencies.
Design Principles
Files are the source of truth. Tools read from and write to the filesystem. Indexes, caches, and derived databases can be rebuilt from collection state.
Human-readable first. Persistent collection data uses open text formats. A user with a text editor can read and modify every record and type file.
Progressive strictness. A collection can begin with untyped records. Types, validation, link rules, and lifecycle policy can be introduced incrementally.
Portable. Conforming tools share the same record and collection semantics. Implementation-specific features use explicit extension namespaces.
Git-friendly. Durable collection state is stored as text that can be reviewed in diffs and versioned with the rest of a project.
Standard building blocks. JSON Schema describes frontmatter shape. CEL provides portable expressions. Named mdbase sections describe behavior that depends on collection context.
How It Works
A collection is a folder with an `mdbase.yaml` marker
my-project/
├── mdbase.yaml # marks this folder as a collection
├── _types/ # type definitions
│ ├── task.md
│ └── person.md
├── tasks/
│ ├── fix-bug.md # a task record
│ └── write-docs.md
└── people/
└── alice.md # a person record
The minimal configuration declares the specification version:
# mdbase.yaml
spec_version: "0.3.0"
Types are defined as Markdown files
A type describes a category of records. Type files usually live in _types/.
Their frontmatter contains a JSON Schema and optional mdbase collection rules;
their body documents the type for people and tools.
---
kind: mdbase.type
name: task
version: 1
match:
path_glob: "tasks/**/*.md"
schema:
dialect: json-schema-2020-12
value:
$schema: "https://json-schema.org/draft/2020-12/schema"
type: object
required: [type, title]
additionalProperties: false
properties:
type:
const: task
title:
type: string
minLength: 1
status:
enum: [open, in_progress, done]
priority:
type: integer
minimum: 1
maximum: 5
assignee:
type: string
due:
type: string
format: date
tags:
type: array
items: { type: string }
collection:
read_defaults:
status: open
links:
assignee:
target_type: person
validate_exists: true
---
# Task
A task represents a unit of work. Set `status` to track progress.
JSON Schema validates the persisted frontmatter object. The collection
section supplies rules that require collection context, including effective
read defaults, links, uniqueness, and path policy.
Data contracts make type meaning portable
Types can implement exact, versioned data contracts stored under
_contracts/. A contract uses JSON Schema for its normalized record interface
and optional binding. The type's implements entry maps contract fields to
record fields. Record contracts normally describe compact application
semantics rather than storage layouts or external wire formats.
Several types can implement one contract, one type can implement several contracts, and several applications can consume the same contract view. Discovery returns the complete implementation set; it never silently picks a provider. Data contracts are passive interoperability and do not grant access.
Records are Markdown files with typed frontmatter
A record provides field values in frontmatter and free-form Markdown in its body. Records may declare a type explicitly or match a type through configured rules.
---
type: task
title: Fix the login bug
status: in_progress
priority: 4
assignee: "[[alice]]"
tags: [bug, auth]
---
The login form rejects addresses containing a `+` character.
Queries filter and sort records with CEL
Queries are YAML objects with optional clauses for filtering, ordering, projection, and pagination:
types: [task]
where: 'status != "done" && priority >= 3'
order_by:
- field: priority
direction: desc
limit: 20
CEL expressions can access frontmatter values, file metadata, dates, lists, and resolved links. Dates are RFC 3339 date strings, so they compare chronologically:
status == "open" && "urgent" in tags
due != null && due < today().addDays(7)
assignee != null && assignee.asFile().team == "engineering"
Expressions follow standard CEL semantics. A filter that raises an error for a record, for example by reading through a broken link, excludes that record and reports a diagnostic.
View records save reusable queries
A collection can install the mdbase.view contract and a type that implements
it, then store one or more named queries in a Markdown record. Shared query scope, named-view filters,
projections, ordering, grouping, and summaries remain machine-readable, while
the Markdown body documents the view for people. Optional presentation metadata
can select a renderer without changing query results.
Validation is progressive
Files in a collection can remain untyped records. Types can be added
incrementally, and validation severity is configurable as off, warn, or
error. JSON Schema controls field shape and unknown-property handling.
Collection rules add checks that depend on other records or paths.
Links connect records across the collection
Records can reference each other with wikilinks such as [[alice]], Markdown
links such as [Alice](../people/alice.md), or declared path values. Link-aware
tools resolve targets, extract links and tags from record bodies, traverse
links in queries, and can update references during rename operations.
Lifecycle policy manages values during writes
Lifecycle policy can assign IDs, timestamps, slugs, copied values, and literals during create and update operations. Lifecycle runs before final validation, so managed fields participate in the same schema and collection checks as supplied frontmatter.
Companion profiles add active behavior
Event and action interfaces are ordinary data contracts. The optional event/action interoperability profile defines how live event sources and action providers declare and exchange them. The optional durable runtime profile adds workflow records, admission, authorization, and recoverable execution. Opening a collection never activates executable behavior.
Conformance is profile-based
Implementations claim the profiles they support, such as Core Read, Collection Semantics, Data Contracts, Links, Query, Core Write, Type Packs, Lifecycle, Event/Action Interoperability, Durable Runtime, and Watch. Profile dependencies keep those claims precise and independently testable.
Specification Structure
| Document | Description |
|---|---|
| 01-concepts.md | Core terminology and data model |
| 02-collection-layout.md | Collection discovery, paths, and reserved state |
| 03-records-and-frontmatter.md | Markdown parsing and YAML value semantics |
| 04-configuration.md | The mdbase.yaml configuration file |
| 05-type-files.md | JSON Schema type wrappers and metadata |
| 05a-data-contracts.md | versioned data contracts, type implementations, and transactional type packs |
| 06-json-schema-profile.md | Supported JSON Schema vocabulary and reference rules |
| 07-collection-semantics.md | Matching, defaults, uniqueness, links, and paths |
| 08-links.md | Link syntax, resolution, traversal, and backlinks |
| 09-lifecycle.md | Managed values and mutation-time policy |
| 10-cel-profile.md | Portable expressions and host bindings |
| 11-querying.md | Filters, ordering, projection, and result envelopes |
| 12-operations.md | Read and write operations, concurrency, and diagnostics |
| 13-migrations-and-compatibility.md | Migration from earlier versions and compatibility |
| 14-conformance.md | Profiles, claims, fixtures, and runners |
Companion documents are versioned or scoped independently of the core chapters:
| Document | Description |
|---|---|
| interop/0.1.md | Event and action interoperability profile 0.1 |
| runtime/0.2.md | Durable runtime companion profile 0.2: standard pack, authorization, admission, execution, and recovery |
| adapters/obsidian-bases.md | Obsidian Bases saved-view adapter |
The portable interoperability testbed runs neutral contract, event/action, and durable-runtime scenarios through black-box adapters. Its canonical transcripts make cross-language and cross-application behavior directly comparable without making one product's internal API normative.
Versioning
This specification uses semantic versioning. The current version is 0.3.0.
Collections declare their specification version with spec_version in
mdbase.yaml. Tools declare the profiles and versions they implement.
Companion profiles, artifacts, and schemas carry their own versions:
| Versioned thing | Declared by | Current | Format |
|---|---|---|---|
| collection specification | spec_version in mdbase.yaml |
0.3.0 |
semantic version |
| event/action interoperability profile | conformance claim interop_profile_version |
0.1 |
major.minor |
| durable runtime profile | conformance claim runtime_profile_version |
0.2 |
major.minor |
| interoperability testbed protocol | testbed evidence | 0.1 |
major.minor |
| canonical schemas | $id path segment, such as /schemas/v0.3/ |
matches the owning specification or profile | v plus major.minor |
| type definition | type file version |
author-defined | positive integer |
| data contract | contract file version |
author-defined | semantic version |
| type pack | pack manifest version |
author-defined | semantic version |
A type version identifies revisions of one local type. Contract and pack versions follow semantic versioning because other artifacts depend on them through version requirements.
Normative Language
The keywords MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are to be
interpreted as described in RFC 2119.
Draft notes use ordinary prose and are non-normative.
License
This specification is released under the MIT License.
01. Concepts
Collection
A collection is a directory tree identified by an mdbase.yaml file. It
contains Markdown records, type files, optional data contract files, optional
runtime records, and optional derived state.
A collection root is the directory containing the active mdbase.yaml.
Record
A record is a Markdown file in the collection that is not excluded, not a type or data contract file, and not another reserved collection file. A record has:
- a collection-relative path
- optional YAML frontmatter
- a Markdown body
- derived file metadata such as basename, extension, folder, size, and modified time
The persisted frontmatter object is the raw record value. Effective read values
may additionally include collection.read_defaults.
Type
A type is a Markdown file, usually under _types/, whose frontmatter has
kind: mdbase.type. The type frontmatter wraps a JSON Schema and optional
mdbase sections.
Types define shape and collection semantics for matching records. Runtime providers and workflows supply executable behavior.
Data Contract
A data contract is a versioned mdbase.contract control file that defines a
portable record interface and optional implementation binding using JSON Schema
2020-12. A record contract normally names shared application semantics; event
and action contracts describe complete messages.
A type implements a data contract through its top-level implements section.
The contract does not replace the type, select a provider, assign ownership, or
grant access. Several types can implement one contract, and several
applications can consume the same implementing type. One type can contain
separate implementations of several contracts.
Schema
In v0.3, "schema" means JSON Schema 2020-12 unless explicitly qualified.
schema.value validates persisted frontmatter object shape. mdbase-specific
sections such as match, collection, lifecycle, and implements are
outside the JSON Schema payload.
Match
Matching decides which type or types apply to an existing record. A record can match no types, one type, or multiple types.
Explicit type declarations take precedence over inferred match rules. When a record matches multiple types, it is valid only if it validates against every matched type's JSON Schema and every matched type's mdbase collection validators.
Collection Semantics
Collection semantics are rules that require knowledge of the file tree or runtime context. Examples:
- link parsing and target resolution
- cross-file uniqueness
- effective read defaults
- path generation
- display metadata
- type matching
- path safety
These rules extend record validation with collection context.
Lifecycle
Lifecycle policy runs during mutating operations. It can materialize managed values such as IDs, creation timestamps, modification timestamps, slugs, and simple transforms.
Lifecycle policy is deterministic operation behavior within Core Write. It runs from type policy during the active mutation.
Expression
Portable v0.3 expressions use the mdbase CEL profile. Expressions appear in queries, projections, runtime conditions, workflow input templates, and optional lifecycle guards.
match.where uses the standalone structured predicate language defined in
Chapter 07.
View
A view is an ordinary Markdown record whose matched type implements the
mdbase.view record contract. It stores shared query scope and one or more
named queries, with optional advisory presentation metadata. The type name is
not significant; the contract implementation is.
Views do not introduce a second query engine. A view-aware tool resolves a named view to the query model from Chapter 11 and executes it through the Query profile. Tools that do not support view execution continue to read and validate view files as ordinary typed records.
View records are passive collection data. Rendering a view, registering a renderer, or connecting user interaction to actions may be tool- or runtime-specific, but the record itself declares no event, action, or executable behavior.
Link
A link is a frontmatter value or body reference that can resolve to another record. mdbase recognizes wikilinks, Markdown links, and bare path strings where a field is declared as link-aware.
JSON Schema validates the local string or array shape. collection.links
declares link meaning, target type, and existence requirements.
Runtime
A runtime is a process, plugin, daemon, CLI, CI job, or agent that executes companion-profile behavior for a collection.
The core collection model is runtime-neutral. Event and action interfaces are
ordinary data contracts with contract_type: event or contract_type: action.
Live event sources and action providers declare the exact contracts they
implement through the event/action interoperability profile. The durable
runtime profile stores workflows, policies, runs, and related state as ordinary
records whose types implement the standard runtime record contracts.
Installing a contract, type, or pack never runs code or grants authority. A host admits a live declaration under its own policy before that code can act.
02. Collection Layout
Identification
A directory is an mdbase v0.3 collection when it contains mdbase.yaml with a
supported v0.3 spec_version.
spec_version: "0.3.0"
Tools MUST NOT treat a parent directory as owning records below a nested
directory that has its own mdbase.yaml.
Recommended Layout
collection/
mdbase.yaml
mdbase.lock.yaml
_types/
meta.md
task.md
view.md
_contracts/
example.task.md
mdbase.view/
1.0.0.md
tasks/
example.md
views/
tasks.md
Only mdbase.yaml is required. Untyped records form a valid collection.
View records are ordinary records identified by their type's mdbase.view
contract implementation, not by location, and require no reserved folder. A collection
MAY organize them under Views/, _views/, or any other non-excluded path.
Unlike the configured types folder, such a folder remains part of the normal
record scan unless explicitly excluded.
Reserved Paths
The following paths are reserved by default:
mdbase.yamlmdbase.lock.yaml, when managed type packs are installed- the configured types folder, default
_types/ - the configured contracts folder, default
_contracts/ .mdbase/for derived implementation state- nested collection roots
Folders holding durable runtime state, such as workflows/ or runs/,
contain ordinary records unless excluded by configuration. Their meaning comes
from their type files, never from their folder names.
Record Discovery
Tools discover records by recursively scanning the collection root for files
with configured record extensions. Each extension fixes the record's format
(Chapter 03); base files are YAML document records. The
default extension set is:
record_extensions: [md]
Tools MUST:
- use forward slash paths in collection APIs
- skip excluded paths
- skip the configured types folder
- skip the configured contracts folder
- skip
.mdbase/ - skip
mdbase.yamlandmdbase.lock.yaml, whatever the record extensions - stop scanning at nested collection roots
- ignore non-record extensions unless configured otherwise
- skip the built-in exclusions below
Every tool applies the same built-in exclusions so that conforming tools discover the same record set:
- any path with a component beginning with
., such as.git/,.obsidian/, or.hidden.md - any path with a component named
node_modules
settings.exclude globs are excluded in addition to the built-in exclusions.
Path Globs
Every mdbase glob, including match.path_glob, settings.exclude, and
collection.unique path scopes, matches a complete collection-relative path
with these rules:
| Pattern | Matches |
|---|---|
* |
zero or more characters other than / |
? |
exactly one character other than / |
[abc], [a-z], [!abc] |
one character other than / in, or not in, the set |
** as a complete path component |
zero or more complete path components |
| any other character | itself |
Matching is case-sensitive and uses Unicode code points. A glob has no leading
/. A ** that is not a complete path component is invalid, as are brace
expansion and backslash escapes. tasks/** matches every path below tasks/,
and tasks/**/*.md matches Markdown files at any depth below tasks/.
Type Discovery
The configured types_folder defaults to _types.
Every Markdown file directly or recursively under the types folder whose
frontmatter declares kind: mdbase.type is a candidate type definition.
Tools MAY warn for files under the types folder that are not valid type files. They MUST NOT treat type files as data records.
Data Contract Discovery
The configured contracts_folder defaults to _contracts.
Every Markdown file directly or recursively under the contracts folder whose
frontmatter declares kind: mdbase.contract is a candidate data contract.
Contract loading, exact-version identity, and implementation validation are
defined in Chapter 05A.
Tools MAY warn for files under the contracts folder that are not valid contract files. They MUST NOT treat data contract files as records.
Runtime Record Discovery
Durable runtime records are discovered like ordinary records. A workflow is a
record whose matched type implements the mdbase.runtime.workflow record
contract, such as the standard pack's runtime_workflow type. Contract
implementation is authoritative; folder names and filenames are conventions and
have no discovery meaning.
Event and action contracts are data contract files under the contracts folder, not records.
Paths And Safety
All collection paths are relative to the collection root and use /.
Operations MUST reject paths that escape the collection root after normalization.
This includes .. traversal, symlink traversal where the implementation follows
symlinks, and absolute paths supplied where a collection-relative path is
required.
Implementations MAY reject platform-reserved filenames or characters when a write operation targets a filesystem where those paths cannot be represented.
03. Records And Frontmatter
Record Formats
A record's format is fixed by its file extension:
| Extension | Format |
|---|---|
base |
YAML document record |
| any other record extension | Markdown record |
The table is part of this specification, not collection configuration, so every
tool reads a file the same way from its path alone. A row is added when a file
type has a consumer, per file type rather than per syntax: a future JSON
format would name the file type it serves (for example canvas), never every
.json file. A collection opts into a format by listing the extension in
settings.record_extensions (Chapter 04). Every format yields the same record
model: persisted frontmatter, a body, and file metadata. Types, contracts,
validation, queries, links, and operations apply to every format alike.
Markdown Record Structure
A Markdown record may begin with YAML frontmatter delimited by --- on the
first line:
---
type: task
title: Fix login
status: open
---
Body text.
If the first non-byte-order-mark bytes are not --- followed by a line ending,
the file has no frontmatter and the full file is body text.
Whitespace or a blank line before the opening delimiter means there is no frontmatter.
YAML Document Record Structure
The whole file of a YAML document record is its frontmatter. There are no delimiters and no body:
filters:
and:
- 'status == "open"'
views:
- type: table
name: Open tasks
An empty file is an empty mapping. The body is always the empty string.
YAML document records let a collection type and query files that another
application owns, such as Obsidian .base files (see the
Obsidian Bases adapter), without a second
storage, discovery, or authorization model.
Frontmatter Value
Frontmatter MUST parse to a YAML mapping. Empty frontmatter is an empty mapping.
If frontmatter is absent, the persisted frontmatter object is {}.
If frontmatter parses to a scalar, sequence, or other non-mapping value, the
record's persisted frontmatter is treated as {} and the record reports an
invalid_frontmatter validation issue with details.reason set to
non_mapping_frontmatter, whose severity follows the validation level in
Chapter 04. A structured update of such a record fails with
invalid_frontmatter at every validation level, so that the original value is
never silently discarded; an explicit document replacement can repair it.
Missing, Null, And Empty
Frontmatter has four distinct states:
- missing means a key is not present in persisted frontmatter
- null means a key is present with YAML null
- empty string means a key is present with
"" - empty list means a key is present with
[]
These states are not interchangeable.
collection.read_defaults applies only to missing keys. It does not replace
explicit null.
Persisted And Effective Frontmatter
frontmatter always means the parsed mapping persisted in the Markdown file.
It does not contain read defaults, computed values, or other derived data.
effective_frontmatter is the derived read/query mapping after applying
collection.read_defaults and any other read-time computation supported by the
active profile.
These names have fixed meanings in every operation and provider. An operation
MUST NOT place effective values in frontmatter, place persisted values in
effective_frontmatter, or change either meaning based on request options.
A complete record document has this shape:
path: tasks/fix-login.md
revision: sha256:opaque
types: [task]
frontmatter:
title: Fix login
effective_frontmatter:
title: Fix login
status: open
body: |
Reproduce and fix the login failure.
document: |
---
title: Fix login
---
Reproduce and fix the login failure.
file:
name: fix-login.md
folder: tasks
size: 142
mtime: 2026-07-26T03:00:00Z
path, revision, types, frontmatter, effective_frontmatter, body, and
file are all required on a complete record document. Empty frontmatter is
represented by {}, not by an absent member.
document is an optional complete UTF-8 source representation. It is returned
when an operation explicitly requests source and MUST contain the exact record
text whose bytes produced revision, including any byte-order mark, YAML
delimiters, comments, quoting, whitespace, line endings, and trailing newline.
frontmatter and body MUST be parsed from that same text. Providers MUST NOT
reconstruct document from parsed frontmatter and body when exact source is
unavailable.
Validation of JSON Schema required is against the persisted or draft
frontmatter object, not against effective read defaults.
Body
The body of a Markdown record is the content after the closing frontmatter delimiter. A YAML document record has no body. The body is not validated by JSON Schema unless a type explicitly models it through a separate mdbase feature.
The body may participate in queries through file.body when body indexing is
enabled or when a tool can read bodies on demand.
File Metadata
Every record exposes a file object to expressions and query results:
| Property | Meaning |
|---|---|
file.path |
collection-relative path |
file.name |
basename with extension |
file.basename |
basename without the final extension |
file.ext |
extension without dot |
file.folder |
collection-relative containing folder |
file.size |
byte size where available |
file.mtime |
modified timestamp where available |
file.ctime |
created timestamp where available |
file.body |
Markdown body when included or needed for filtering |
File metadata is derived. It MUST NOT be written into frontmatter unless a tool explicitly maps it to ordinary fields.
Serialization
Write-capable tools SHOULD preserve unrelated body text and line ending style.
A YAML document record serializes as its frontmatter mapping alone. A create or
update that supplies a non-empty body for a YAML document record fails with
invalid_request before any write. Structured writes re-emit the mapping and
need not preserve comments, key order, or quoting style; a whole-document
document replacement (Chapter 12) is written exactly as supplied. Tools that
edit files another application owns SHOULD use whole-document replacement.
When serializing frontmatter, tools MUST:
- write an explicit null value for a key whose value is null
- omit keys that are missing
- quote empty strings
Tools SHOULD preserve array and object structure and SHOULD produce deterministic key ordering when an operation rewrites a generated file.
A null value in written frontmatter always means explicit null. Removing a key is a distinct operation; Chapter 12 defines how an update requests it.
YAML Profile
Implementations MUST parse UTF-8 Markdown files.
The v0.3 YAML profile SHOULD use a safe YAML parser and MUST NOT execute custom tags.
Tools SHOULD normalize common YAML scalar forms into the corresponding JSON data model before JSON Schema validation. Non-JSON YAML values such as NaN, Infinity, binary values, and timestamps with parser-specific objects MUST be handled by the mdbase YAML profile before schema validation or rejected with a clear diagnostic.
04. Configuration
`mdbase.yaml`
The collection config file is named mdbase.yaml and lives at the collection
root.
Minimal v0.3 config:
spec_version: "0.3.0"
Recommended config:
spec_version: "0.3.0"
settings:
timezone: Australia/Melbourne
types_folder: _types
contracts_folder: _contracts
record_extensions: [md]
validation: error
explicit_type_keys: [type, types]
id_field: id
Required Keys
spec_version is required. During major-zero development, the minor component
is the compatibility boundary. A v0.3 tool MUST reject v0.2 and v0.4
collections unless an explicit compatibility adapter is enabled.
Pre-1.0 draft versions MAY be accepted by explicit compatibility setting.
Settings
| Key | Type | Default | Meaning |
|---|---|---|---|
settings.timezone |
string | local runtime | durable IANA timezone for authority-owned calendar semantics |
settings.types_folder |
string | _types |
folder containing type files |
settings.contracts_folder |
string | _contracts |
folder containing data contract files |
settings.record_extensions |
list of strings | [md] |
record file extensions without dot |
settings.validation |
string | error |
default validation level: off, warn, or error |
settings.explicit_type_keys |
list of strings | [type, types] |
frontmatter keys used for explicit type declarations |
settings.id_field |
string | none | field used for ID-based wikilink resolution; when absent, wikilinks resolve by path and filename only |
settings.exclude |
list of globs | [] |
paths excluded in addition to the built-in exclusions in Chapter 02 |
settings.explicit_type_keys replaces the default key list. An empty list makes
all type membership inferred.
settings.timezone, when present, MUST be an IANA timezone identifier. UTC
is the canonical identifier for Coordinated Universal Time. Numeric offsets and
ambient aliases such as local are invalid because they do not name a durable
calendar authority or model daylight-saving transitions. An invalid configured
timezone makes the collection configuration invalid; an implementation MUST
NOT silently substitute its runtime timezone.
The types and contracts folders MUST be different normalized paths. Both are reserved control-file folders and are excluded from ordinary record discovery.
Unknown config keys MUST produce a warning while normal config loading continues. An explicit strict-config mode MAY reject them.
Validation Levels
settings.validation controls how record validation issues affect operations.
Record validation issues are JSON Schema failures, format_invalid, collection
validator failures such as uniqueness and validate_exists, non-mapping
frontmatter, and data contract view failures.
| Level | Record validation | Reads and queries | Create, update, rename, batch |
|---|---|---|---|
off |
not performed | return records without validation diagnostics | write without record validation |
warn |
performed | return records with warning diagnostics |
write and report warning diagnostics |
error |
performed | return records with error diagnostics |
fail before writing when the resulting record has any issue |
At every level a read returns the record, including an invalid one, and a query evaluates every candidate. Queries do not report per-record validation issues unless the caller requests them.
Validation levels never relax request and safety checks: invalid requests, path
escapes, type-file and configuration errors, type_conflict,
type_membership_changed, lifecycle failures, concurrency conflicts, and
expression compilation errors are always errors.
An explicit validate operation always performs record validation. At level
off it reports issues as warning.
Runtime Host Config
Durable-runtime enablement, worker identity, storage, transport binding, and
policy selection are host concerns and are not part of the core mdbase.yaml
schema. This prevents merely opening a collection from activating executable
behavior.
Portable runtime policies and workflow/state records are ordinary records
implemented by the standard runtime pack. A host MAY preserve private
configuration under an x-* extension or in its own settings store, but that
configuration does not change core contract resolution. Runtime profile 0.2
has one resolution model and no contract mode.
Expressions
Portable v0.3 expressions are CEL. No config key is required to opt into CEL.
Tools MAY support non-portable UI expression dialects. Portable stored v0.3 files MUST use the mdbase CEL profile unless a feature declares a different extension namespace.
View records use CEL for portable filters, projections, selections, and custom
summaries. Compatibility tools MAY read another view or expression format, but
alternate source and round-trip metadata belong under an x-* extension and do
not change the meaning of the portable CEL fields.
Version Compatibility
Patch versions within the same stable minor version MUST be backward compatible.
For prerelease v0.3 versions, tools MUST require and report the exact supported identifier when rejecting a collection.
Environment And Includes
Config includes and environment substitution are optional local extensions. Expanded values MUST be the values used for validation and query behavior. Non-portable config extensions SHOULD use a namespaced key.
05. Type Files
Purpose
A type file connects four parts of the mdbase model:
- a rule for selecting records
- a JSON Schema for validating their persisted frontmatter
- collection behavior such as defaults, links, lifecycle assignments, and path policy
- portable data-contract implementations
The YAML frontmatter is the machine-readable definition. The Markdown body can document the type for people and tools.
Minimal Type
---
kind: mdbase.type
name: task
version: 1
match:
path_glob: "tasks/**/*.md"
schema:
dialect: json-schema-2020-12
value:
$schema: "https://json-schema.org/draft/2020-12/schema"
type: object
required: [title]
additionalProperties: false
properties:
type:
const: task
title:
type: string
minLength: 1
---
# Task
Task records live under `tasks/`.
Type Evaluation Model
Type processing has a collection-load phase and a record-evaluation phase.
During collection load, an implementation:
- discovers candidate type files under the configured types folder
- validates each file against the built-in type-file schema
- canonicalizes type names for lookup and detects name conflicts
- resolves the embedded or referenced JSON Schema
- validates and compiles that schema against the v0.3 JSON Schema profile
- resolves and validates every
implementsentry against the local data contract registry
Each valid definition then enters the collection's type registry. Diagnostics for an invalid definition identify the type-file path and the failing section.
For each record, an implementation:
- parses the record path and raw persisted frontmatter
- determines its matched types using Chapter 07
- validates the raw frontmatter against every matched JSON Schema
- evaluates every matched type's collection validators
- constructs effective read values, including compatible read defaults and projections supported by the implementation
Type selection and record validation are separate operations. An explicit type declaration selects a type and still subjects the record to that type's schema and collection rules.
Write operations use the same model around lifecycle processing: determine membership from the requested draft, run the applicable lifecycle assignments, then confirm membership once before final validation. The complete write order is defined in Chapters 09 and 12.
Required Frontmatter
A type file MUST include:
kind: mdbase.typenameschema
version SHOULD be present and SHOULD be a positive integer.
Type Names
Type names are stable identifiers. They SHOULD use lower-case ASCII names with
letters, numbers, _, and -.
Tools MUST compare type names case-insensitively for matching and conflict detection. Displays SHOULD preserve the casing written by the author.
Two type files whose names differ only by case are conflicting definitions.
Schema Section
schema.dialect identifies the schema profile. v0.3 core defines:
schema:
dialect: json-schema-2020-12
The schema can be embedded:
schema:
dialect: json-schema-2020-12
value:
type: object
properties:
title: { type: string }
or referenced:
schema:
dialect: json-schema-2020-12
ref: "../schemas/task.schema.json"
External references resolve relative to the type-file path. Resolution MUST remain within the collection or installed pack root that owns the schema.
Exactly one of value and ref is required. A type containing both is invalid.
The JSON Schema validates raw persisted frontmatter. Read defaults, projections, and lifecycle values enter at the stages defined by their own chapters.
mdbase Sections
The following top-level sections are defined by v0.3:
| Section | Purpose |
|---|---|
match |
select records for inferred type membership |
collection |
define Markdown-aware collection semantics |
lifecycle |
assign managed values during mutations |
implements |
declare exact, schema-validated data contract implementations |
Portable type-file validation accepts the core sections and x-* extension
sections. Domain and provider metadata belongs under an extension name:
x-local:
owner: research-team
This naming rule lets the type-file schema diagnose misspelled core keys such as
collecton.
Portable interoperability metadata does not belong under x-*. It uses the
first-class implements section defined in Chapter 05A:
implements:
- contract: example.task
version: 1.0.0
fields:
title: title
status: status
Meta Type
Implementations MUST have a built-in schema for validating v0.3 type files.
init or equivalent tooling SHOULD materialize _types/meta.md for inspection.
That file is itself a v0.3 type file matching _types/**/*.md and validating
type-file frontmatter against the v0.3 type-file JSON Schema.
The built-in schema is authoritative during bootstrap. A materialized
_types/meta.md mirrors and documents that behavior.
View Type
Saved views are records whose type implements the mdbase.view record
contract (Chapter 05A). The contract's record schema is
schemas/v0.3/view.schema.json. The repository's _contracts/mdbase.view/1.0.0.md
and _types/view.md are the canonical contract and implementing type; the
implementing type matches type: view and maps every contract field to the
same-named frontmatter field.
A collection that stores portable view records SHOULD install that contract and
a type implementing it, normally through the mdbase.view type pack. A
collection MAY instead implement the contract with its own type name, match
rule, and field mapping.
Unlike the meta type, the view type is not required for bootstrap and is not a
built-in control-file category. A view file remains an ordinary Markdown record
and participates in normal reads, validation, links, writes, and type matching.
A record whose frontmatter says type: view but that matches no type
implementing mdbase.view is not a saved view. View-aware execution is the
optional behavior defined in Chapter 11.
Type Membership
Records may select types explicitly or through inferred matching. Chapter 07 defines the decision process, the structured match language, and the optional CEL Match profile.
Matched type order is deterministic:
- explicit declarations retain declaration order after case-insensitive de-duplication
- inferred matches are ordered by canonical lower-case type name
Multiple Matched Types
If a record matches several types, the implementation validates it independently against every matched type schema and collection validator. The record is valid only when all of those validations pass.
Collection behavior composes as follows:
- uniqueness rules are additive and are each evaluated in the type that declared them
- identical read defaults, link rules, path policies, lifecycle assignments, and projections coalesce
- different values for the same read-default field, link selector, path policy,
lifecycle event and field, or projection name produce
type_conflict - display metadata remains associated with its declaring type; a flattened display uses the first explicit type or first canonical inferred type
Implementations MUST detect these conflicts before applying the affected behavior. A write fails before mutation. A read or query reports the conflict and leaves the conflicted derived value unavailable. The result is independent of filesystem load order.
For a uniqueness rule with scope: type, the comparison set contains every
record that matches the declaring type. A record matching several types
participates independently in each declaring type's uniqueness set.
Write-Time Type Membership
Write operations determine membership from the requested draft and target path before lifecycle runs. Explicit declarations use the same precedence as reads.
After lifecycle has run, the implementation MUST evaluate membership once more.
If the final membership differs from the pre-lifecycle membership, the operation
fails with type_membership_changed. Lifecycle is applied once per operation.
An operation that changes membership intentionally expresses that change in its input patch, target path, or explicit type list.
Reusable Schema Composition
Reusable record shapes use JSON Schema $defs, $ref, allOf, anyOf, and
oneOf. Reusable collection behavior can be generated or supplied by a named
pack or extension.
Display Metadata
Human-facing metadata belongs in collection.display:
collection:
display:
name_field: title
icon: check-circle
JSON Schema title and description remain schema annotations.
collection.display supplies the collection-specific presentation hints.
05A. Data Contracts
Why Data Contracts Exist
A contract describes one portable interface independently from the local type or application that implements it.
For example, personal_task, work_task, and task can all implement
tasknotes.task. Their filenames, additional fields, matching rules, and local
presentation can differ. An application can discover the shared contract,
understand each type's field mapping, and operate without requiring every
collection to use one canonical type name.
mdbase.contract is the shared identity and JSON Schema substrate for passive
record views, events, and actions. contract_type discriminates their
subject-specific schema fields. This chapter defines the shared artifact and
record implementation rules. The optional
event/action interoperability profile defines executable
source/provider declarations and message exchange. Contracts never grant
permission.
Three Portable Artifacts
The complete collection contract model has three intentionally small parts:
- An
mdbase.contractartifact defines a versioned subject-specific interface using JSON Schema 2020-12. - For a
recordcontract, a type'simplementsentry maps that interface to the type and supplies contract-specific binding data. - An optional managed
mdbase.type-packgroups contracts, types, and their referenced schemas for transactional installation and evolution.
Event sources and action providers make runtime declarations because they are executable, instance-specific implementations rather than record types. Requirements, authorization grants, and transports are not collection contract artifacts.
Designing Record Contracts
A record contract SHOULD describe a compact, application-facing semantic interface. Its properties name values that independent consumers can rely on, while an implementing type remains free to choose local field names, matching rules, additional fields, and presentation.
Record contracts SHOULD:
- keep the unconditionally required surface as small as the shared behavior permits
- use optional properties for semantics that not every implementing type can provide
- use
binding_schemafor implementation-specific vocabularies and behavior, such as which local task statuses count as completed - be shared by applications that need the same semantics rather than duplicated under application-specific IDs
A record contract SHOULD NOT reproduce an external interchange or storage format merely so applications can request that serialization. An importer, exporter, or application adapter can translate a semantic record view to JSContact, vCard, or another wire format. A wire-format-shaped record contract remains valid when the implementing type intentionally stores and exposes that exact shape.
Event and action contracts are different: their schemas describe complete messages at an interoperability boundary, so a transport-shaped schema is usually appropriate.
Contract Files
Contract files are Markdown files under the configured contracts folder,
default _contracts/. Their frontmatter has kind: mdbase.contract and
validates against schemas/v0.3/data-contract.schema.json.
---
kind: mdbase.contract
contract_type: record
id: example.task
version: 1.0.0
name: Example task
description: A small portable task interface.
record_schema:
dialect: json-schema-2020-12
value:
$schema: "https://json-schema.org/draft/2020-12/schema"
type: object
required: [title, status]
additionalProperties: false
properties:
title: { type: string, minLength: 1 }
status: { type: string, minLength: 1 }
due: { type: string, format: date }
binding_schema:
dialect: json-schema-2020-12
value:
$schema: "https://json-schema.org/draft/2020-12/schema"
type: object
required: [completed_values]
additionalProperties: false
properties:
completed_values:
type: array
minItems: 1
uniqueItems: true
items: { type: string }
---
# Example task
The body explains the interface to people. Portable behavior is defined by the
frontmatter schemas.
id is a lower-case namespaced identifier. version is the artifact's exact
semantic version.
Version Requirements
Type implementations and interoperability requirements name the contract
versions they support with a version requirement. The portable grammar is a
subset of the npm semver range syntax:
| Form | Satisfied by |
|---|---|
1.4.2 or =1.4.2 |
exactly 1.4.2 |
^1.4.2 |
>=1.4.2 <2.0.0-0 |
^0.4.2 |
>=0.4.2 <0.5.0-0 |
^0.0.4 |
>=0.0.4 <0.0.5-0 |
~1.4.2 |
>=1.4.2 <1.5.0-0 |
>=1.2.0 <2.0.0 |
every comparator: one or more of >, >=, <, <=, or = followed by a version, separated by single spaces |
Versions compare by SemVer 2.0.0 precedence, so a pre-release sorts before its
release and 2.0.0-0 is the lowest version with major 2. A pre-release
version satisfies a requirement whenever it lies within these bounds. Build
metadata is ignored. Other range syntaxes, such as x wildcards, hyphen
ranges, and || alternatives, are not portable.
A requirement resolves to the highest registered version of that contract ID that satisfies it. Resolution is deterministic for a given contract registry and is repeated whenever the registry changes.
Contract Registry
During collection load, a data-contract-aware implementation:
- scans the configured contracts folder recursively
- validates every candidate against the built-in data-contract schema
- resolves and compiles the schemas selected by
contract_type - registers each contract by the exact pair
(id, version) - computes its contract digest
- resolves every type
implementsrequirement to one exact contract version - validates every type
implementsentry against its resolved contract
Several versions of one contract ID may coexist. Two artifacts with the same
ID and version are valid only when their contract digests are identical.
Different content for the same ID and version is
data_contract_conflict.
Resolution is collection-local and offline. Core implementations MUST NOT fetch a missing contract from the network. Applications and installers carry the contract files they require, usually in a type pack.
Type Implementations
implements belongs in the type file because a record implementation is a
claim about each record that matches that one type. A type can implement only a
record contract. Event sources and action providers declare implementations
through the interoperability profile instead.
implements:
- contract: example.task
version: 1.0.0
fields:
title: title
status: workflow_state
due: due_date
"/@type": "/card/@type"
binding:
completed_values: [done, cancelled]
Each implementation contains:
| Key | Meaning |
|---|---|
contract |
exact contract ID |
version |
contract version requirement |
fields |
contract field reference to record field reference mapping |
binding |
optional configuration validated by the contract's binding_schema |
Both sides of fields use the field-reference syntax from Chapter 07. Existing
field paths remain valid. RFC 6901 JSON Pointer is the exact form for keys that
field paths cannot represent, so /@type addresses an @type property and
/a~1b addresses an a/b property. The left side addresses the normalized
contract view. The right side addresses effective record frontmatter. Mapping
is direct: core does not rename values, coerce values, run expressions, or
apply hidden transforms.
The binding object does not transform projected record values. It supplies
validated semantic policy that a contract-aware application can interpret. For
example, a task implementation can expose the local status value unchanged
while declaring several values in binding.completed_values. This preserves
the user's vocabulary and avoids inventing an ambiguous reverse mapping.
A type MUST NOT contain two implementations of the same contract ID. Field mappings MUST address fields declared by the resolved
record_schema and the resolved type schema. Every unconditional top-level
field named by the contract's record_schema.required array MUST be mapped,
either by the matching one-segment field path or by the matching one-token JSON
Pointer.
When a contract has a binding_schema, the implementation's binding value, or
an empty object when omitted, MUST validate against it. When a contract has no
binding_schema, binding MUST be absent or empty.
Private application metadata may remain under x-*, but an x-* object has no
contract-discovery, conformance, or authorization meaning.
Contract Views And Record Validation
To construct a contract view, a tool starts with a record's effective
frontmatter and copies every mapped value to its contract field reference. Missing
optional values remain missing. The resulting object is validated against the
contract's record_schema.
Contract validation complements rather than replaces type validation:
- the type schema validates raw persisted frontmatter
- collection semantics construct the effective record
- the field map constructs a normalized contract view
- the contract's
record_schemavalidates that view
A record can therefore satisfy its type schema and still produce
data_contract_record_invalid for one declared implementation. Implementations
MUST surface that diagnostic whenever they expose the record through that
contract.
Static checks at collection load SHOULD diagnose obviously incompatible mapped schema types early. Runtime contract-view validation remains authoritative when JSON Schema composition makes static implication impractical.
Stable Digests
Digests let a consumer distinguish an approved implementation from a later change that happens to retain the same name.
The contract digest is SHA-256 over RFC 8785 JSON Canonicalization Scheme bytes for this object, using the fully resolved JSON Schema values rather than their storage wrappers or reference paths:
{
"kind": "mdbase.contract",
"contract_type": "record",
"id": "...",
"version": "...",
"record_schema": {},
"binding_schema": {}
}
The digest object contains the subject-specific schema keys selected by
contract_type: record_schema and binding_schema; data_schema and
source_schema; or input_schema, output_schema, error_schema,
provider_schema, and behavior. Absent optional members are omitted.
Human-facing name, description, Markdown body, x-* metadata, schema
wrapper dialects, and local ref paths do not affect portable identity.
Consequently, an inline schema and a local referenced schema with identical
resolved JSON values have the same contract digest, while changing the bytes
at a stable reference path changes the digest.
The implementation digest is SHA-256 over RFC 8785 bytes for:
{
"contract_digest": "sha256:...",
"type": {
"name": "...",
"version": 1,
"match": {},
"schema": {},
"collection": {},
"lifecycle": {}
},
"implementation": {}
}
contract_digest is the digest of the contract version the implementation's
requirement resolved to. collection omits collection.display, and
implementation is the implements entry. Absent optional members are omitted.
This deliberately includes membership, shape, defaults, links, paths,
projections, lifecycle behavior, and the resolved contract. A consumer that
pinned an implementation can detect any portable change that may alter the
records or values it observes. Advisory presentation metadata, the type's
Markdown body, and x-* sections do not affect the digest, so editing an icon
or description never invalidates an approval.
Digest strings use sha256: followed by 64 lower-case hexadecimal characters.
Multiple Implementations
A contract lookup returns a set of conforming type implementations, never an arbitrarily selected provider.
Multiple applications may consume the same type implementation. Implementing a contract does not create an owner, lease, or exclusive provider relationship.
When several types implement one compatible contract requirement:
- read and list experiences SHOULD initially offer their explicit union
- user-facing approval MUST show every included type
- an existing approval MUST pin the exact type names, contract digest, and implementation digests
- a later implementation MUST NOT silently join that approval
- creation MUST use one explicitly selected implementing type
A product may let a user select a subset instead of the union. It MUST NOT hide the selection or silently choose the first filesystem entry.
These rules separate interoperability from authorization. A type's
implements claim says what it can mean; it does not say which application may
read or mutate it.
Contract Access And Whole Records
The portable contract view contains only mapped contract fields. A gateway that grants access "through a contract" MUST expose only that view plus the minimum record identity needed by its protocol. If a record has several approved views and the operation does not identify one unambiguously, the gateway MUST require an explicit contract ID, exact version, and implementing type rather than merge or guess.
Access to unmapped frontmatter, the Markdown body, or arbitrary records is whole-record or whole-collection access and MUST be requested and presented explicitly. Merely implementing a contract never grants either form of access.
Core collection APIs remain authorization-neutral. This distinction is normative for gateways and application protocols that use data contracts as an authorization boundary.
Type Packs
A type pack is a directory or archive with an mdbase-pack.yaml manifest that
validates against schemas/v0.3/type-pack.schema.json.
kind: mdbase.type-pack
id: example.tasks
version: 1.0.0
name: Example task types
resources:
- kind: contract
mode: managed
source: contracts/example.task.md
target: _contracts/example.task.md
digest: sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
- kind: type
mode: managed
source: types/task.md
target: _types/task.md
digest: sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789
mode is required. A managed resource remains owned by the pack and may be
updated or retired by a later pack version only while its live bytes still
match the installed digest. A seed resource is created only when its target
is absent and becomes user-owned immediately; later pack versions neither
replace nor delete it unless an explicit seed-type upgrade is declared below.
A seed type resource MAY declare upgrade_from: one baseline, or a
non-empty list of baselines. A baseline is { digest, document } with an
optional integer version. document is the exact bytes of a starter the
publisher previously shipped for this resource, pinned by SHA-256 in digest;
it is not an assertion that the user's current document is unchanged. version,
when present, is the version that document declares and is only for
presentation. A single baseline is equivalent to a list containing it. Listing
every previously shipped starter that remains supported lets a collection
upgrade from any of them, not only from the most recent. This declaration is
part of the reviewed manifest and its digest. Ordinary seeds are unaffected.
Engines that do not support this member MUST reject the manifest.
The manifest is invalid (invalid_type_pack) when upgrade_from appears on a
resource that is not a seed type, when it is an empty list, when a baseline lacks
digest or document, when a baseline's digest is not the SHA-256 of
its document, when two baselines share a digest, when a baseline's digest equals
the resource's own digest, when a baseline document's frontmatter kind or
name differs from the desired document's, or when a baseline's version
differs from the version its document's frontmatter declares.
A seed's origin is the publisher document its live target descends from.
The lock records it (see Pack Identity And Portable Provenance). When the target
exists, is not an intentionally preserved seed target, and the resource declares
upgrade_from, the engine plans exactly one of these, in order:
- The live bytes equal the desired document:
preserve. - The live bytes equal a baseline's document:
update, writing the desired document byte-for-byte. Byte equality proves the origin, whatever the lock records. - The origin is the desired document:
preserve. The seed was already upgraded and has been edited since. - The origin is a baseline:
updateby a three-way merge of that baseline, the live type, and the desired type. - Otherwise, because the origin is unknown or is not a listed baseline:
preserve, with areasonstating that no upgrade baseline applies. The type is left as it is; it does not make the pack conflict.
An engine MUST NOT choose a merge baseline any other way. In particular, it MUST
NOT merge against a baseline the seed is not known to descend from, because the
differences between that baseline and the seed's actual origin would be applied
as if they were the user's edits. The assessment reports, for every seed
update, the baseline used as upgrade_baseline: { digest, version? }.
A three-way merge is conservative. The type kind and name MUST match. Unchanged publisher settings retain live customizations; unchanged live settings accept publisher changes. Object settings merge recursively. Contract implementations merge by contract ID only when unambiguous, retaining customized field mappings and bindings while updating exact version references. Competing changes (including ambiguous implementation lists) MUST fail closed. Other arrays are indivisible. Missing values and explicit null are distinct. Top-level setting removal requires separate manual review. The live Markdown body and unrelated YAML nodes MUST be preserved. Reformatting a changed YAML node is permitted.
The merged resource digest MUST appear in the assessment. Normal assessment and collection revision guards still apply. The upgrade, contract resources, and provenance MUST publish atomically only after validating the staged collection. A remaining reference to a removed exact contract MUST block the whole upgrade; implementations MUST NOT rewrite additional types implicitly. Record documents MUST NOT be migrated by this mechanism. An intentionally preserved seed target MUST NOT be upgraded.
This is not version negotiation or grant migration. Callers coordinating applications must retain old contracts and implementations when supported applications still require them, or arrange a separately reviewed coordinated upgrade. Existing grants never acquire the new contract version implicitly.
Resource digests are SHA-256 over the exact resource bytes. Source and target paths are relative, forward-slash paths without traversal. A directory, archive, repository, or package that distributes a pack MUST preserve those bytes exactly, including line endings. Rewriting a text resource from LF to CRLF therefore creates a different resource and MUST fail digest verification. Distributors that store packs in version control SHOULD disable line-ending conversion for pack resources.
Type packs are installation units, not record types and not permission grants. A pack may include several contracts, several implementing or auxiliary types, and local JSON Schemas referenced by those artifacts.
Pack Identity And Portable Provenance
The pack digest is SHA-256 over RFC 8785 JSON Canonicalization Scheme bytes for
the complete validated manifest, prefixed with sha256:. Because every
resource digest is part of the manifest, the pack digest identifies the exact
manifest and exact resource bytes without incorporating their distribution
location.
Managed pack state is stored in mdbase.lock.yaml at the collection root and
validates against schemas/v0.3/type-pack-lock.schema.json. The lock is
portable collection source, not derived cache state. It records each installed
pack's exact ID, version, pack digest, stable installer identity, and the kind,
mode, canonical source, resolved target, and installed digest of every resource. Tools write it
deterministically and users MAY inspect or version it. Tools MUST NOT infer
ownership from filenames, application names, or x-* metadata.
A seed resource's entry also records origin_digest, the digest of the
publisher document its target descends from, when that is known. Because a seed
becomes user-owned, the installed digest of a seed describes the pack, not the
target, and MUST NOT be used as its origin. An apply sets origin_digest to the
desired resource digest when it creates the seed, when it upgrades it (by exact
replacement or by merge), and whenever the target's live bytes equal the desired
document, whatever else applies. Otherwise it carries the previous entry's
origin_digest for that target forward unchanged, or omits it when there is
none. A seed target that existed before the pack was installed, and an
intentionally preserved seed target, therefore have no origin unless their bytes
equal the desired document. A lock entry without origin_digest means the
origin is unknown. Locks written before origin_digest existed carry none, so
an edited seed under such a lock is preserved with a reason rather than merged
until it is upgraded or recreated; an unedited seed still upgrades, because its
bytes prove its origin. origin_digest is defined only for seed resources and
MUST NOT appear on a managed resource. An apply that changes only seed origins leaves the pack's
status current; it is not a reconfiguration.
Full collection snapshots, authority transfers, and unscoped synchronization
MUST carry mdbase.lock.yaml when it exists. A scoped application projection
MAY omit it to avoid disclosing unrelated pack metadata. A storage provider
MUST NOT reject the lock under a per-document limit that the collection owner
cannot remediate, because tools generate it.
An absent lock means no resource is pack-managed, including resources created by older one-shot installers. Adopting existing files into managed ownership is a separate explicit operation and MUST verify their exact digests.
Assessment And Transactional Apply
Pack evolution has two public phases: assess_type_pack is read-only and
apply_type_pack consumes a reviewed assessment. A caller supplies the desired
manifest, exact resources, and a stable reverse-domain installed_by
application or tool identity. Optional target_overrides map canonical manifest
targets to collection-specific paths without changing the publisher-owned pack
identity. Overrides are validated as safe collection paths, are included in the
assessment digest, and the resolved targets are written to the receipt.
Assessment reports one of current, install, upgrade, downgrade,
reconfigure, or conflict, the current and desired pack identities, and every resource in a
stable exact diff. Resource actions are create, update, delete,
adopt, unchanged, preserve, or conflict. adopt records an existing
byte-identical managed resource without rewriting it. preserve is used for seed resources
and for seed resources retired from a newer pack. A conflicting assessment is
not applicable. The assessment also reports the planned mdbase.lock.yaml
action (create, update, or unchanged) and its resulting digest, so storage
adapters can persist the complete authority transaction without inferring
hidden engine writes.
reconfigure means the exact same immutable pack is being resolved to different
collection targets. Managed resources are relocated only when their installed
bytes are unchanged; modified resources conflict. Seed resources are never
moved or deleted automatically. A caller may name seed targets in
preserve_seed_targets to record an intentional omission, such as when a user
maps the contract to an existing type instead of accepting the starter. Unknown
or non-seed preservation targets MUST be rejected.
When guided setup maps a provided contract to an existing user-owned type, the
reviewed contract_setups choices are part of the same assessment and apply.
The assessment reports their exact type-resource diff and binds the selected
type revisions, field mappings, and bindings into assessment_digest. Apply
MUST stage those edits together with the managed resources and lock, validate
the combined collection, and commit them in the same transaction. A stale type
revision therefore leaves both the user-owned type and every pack resource
unchanged. A connector MUST NOT install the pack first and apply the reviewed
mapping as a later independent mutation.
The assessment includes a deterministic assessment_digest over the desired
pack identity, current lock entry, and every relevant live target digest. Apply
MUST recompute it inside the collection mutation boundary and fail with
concurrent_modification before writing when it differs. Downgrades require an
explicit allow_downgrade decision.
An unmanaged target with different bytes is a conflict unless the assessment
request contains an explicit adoption decision naming that target and its exact
current digest. An accepted adoption may plan update, records the adopted
digest in the diff, and is covered by assessment_digest. This is the safe
upgrade bridge for collections created before pack receipts existed: tools can
show the files that will become package-managed, require informed approval, and
remain race-safe. Unknown targets, seed targets, and stale adoption digests MUST
be rejected. Adoption is never inferred from application access or install
intent.
A pack-aware apply implementation MUST:
- validate the manifest, safe paths, source bytes, and resource digests
- stage every resource and reviewed existing-type setup without changing the live collection
- resolve the complete staged contract and type registries
- validate every contract, implementation, type, reference, and affected existing record
- compute and present the exact resource diff and assessment digest
- acquire its collection mutation boundary and recheck the assessment
- commit all resource changes and the lock as one recoverable transaction
- reopen the collection and verify the committed registry and lock
An invalid resource aborts before any live write. A conflict or concurrent change aborts with the live collection unchanged. Implementations may use an atomic directory exchange or a durable backup journal with rollback. After a crash, recovery MUST restore either the complete pre-install state or the complete committed state before normal collection operations resume.
Revoking an application's access does not uninstall its type pack. Uninstall is a separate, explicitly requested operation because records may still depend on the installed types.
Applying a current pack is valid and reports every managed resource as
unchanged; it MUST NOT create a new logical collection revision. Applying an
upgrade deletes a retired managed target only when its live digest still
matches the lock. User-modified managed resources produce conflict and no
live write. Successful apply reports the committed receipt, exact resource
diff, and cleanup_deferred when transaction-journal cleanup must be retried.
Diagnostics
Data-contract-aware tools use these codes:
| Code | Meaning |
|---|---|
invalid_data_contract |
contract frontmatter or schema is invalid |
data_contract_not_found |
no registered contract has the referenced ID |
data_contract_conflict |
one ID and version resolve to different contract digests |
data_contract_version_mismatch |
no registered version satisfies an implementation or consumer requirement |
data_contract_binding_invalid |
implementation binding fails its binding schema |
data_contract_field_invalid |
a mapped contract or record field is missing or incompatible |
data_contract_record_invalid |
a projected contract view fails the contract schema |
invalid_type_pack |
a pack manifest, lock, resource, path, or digest is invalid |
type_pack_conflict |
ownership, live bytes, or an untracked target prevents a safe apply |
type_pack_apply_failed |
transactional commit or recovery did not complete normally |
Diagnostics use the canonical shape from Chapter 14 and identify the contract
ID, exact version, type name, and relevant field mapping in details.
06. JSON Schema Profile
Base Dialect
v0.3 uses JSON Schema 2020-12 for persisted frontmatter shape.
Every embedded schema SHOULD include:
$schema: "https://json-schema.org/draft/2020-12/schema"
The containing type file MUST declare:
schema:
dialect: json-schema-2020-12
Data Model
YAML frontmatter is converted into the JSON data model before schema validation:
- mapping -> object
- sequence -> array
- string -> string
- integer/float -> number, with integer constraints enforced by JSON Schema
- boolean -> boolean
- null -> null
Values that cannot be represented in the JSON data model MUST be rejected or normalized by the mdbase YAML profile before JSON Schema validation.
Required Support
Core v0.3 JSON Schema support includes:
typerequiredpropertiesadditionalPropertiesitemsenumconstoneOfanyOfallOfif,then,elseminimum,maximumexclusiveMinimum,exclusiveMaximummultipleOfminLength,maxLengthpatternminItems,maxItems,uniqueItems$defs- local
$ref title,description,default,examples, and other annotations
Tools MAY support more of JSON Schema 2020-12. The required profile defines the portable baseline for Core Read conformance.
Discriminated Unions
v0.3 uses ordinary JSON Schema constructs for discriminated unions.
Example:
type: object
required: [kind, input]
oneOf:
- properties:
kind: { const: notice.show }
input:
type: object
required: [message]
properties:
message: { type: string }
additionalProperties: false
required: [kind, input]
- properties:
kind: { const: mdbase.record.patch }
input:
type: object
required: [path, patch]
properties:
path: { type: string }
patch: { type: object }
additionalProperties: false
required: [kind, input]
Runtime workflow steps normally use step.action plus action contracts instead
of embedding every action union directly in the workflow schema.
Defaults
JSON Schema default is an annotation. Record mutation occurs through create,
lifecycle, or explicit default materialization, and required evaluates the raw
record during validation.
Tools MAY use default as a create/editor hint.
Effective read defaults belong in collection.read_defaults.
Dynamic defaults such as now, uuid, ulid, and slugify belong in
lifecycle.
Format
JSON Schema format is an annotation in the base dialect. The mdbase v0.3
profile requires assertion behavior for:
format: dateformat: date-timeformat: time
date, time, and date-time use the RFC 3339 productions referenced by JSON
Schema. date-time values MUST contain Z or a numeric UTC offset. Invalid
values produce format_invalid diagnostics.
Other formats such as email, uri, and uuid MAY be asserted by
implementations. Collections that require them depend on implementation-specific
behavior until a conformance profile defines their semantics.
References
Tools MUST support fragment-only $ref references within an embedded schema.
They MUST also support a type wrapper's schema.ref to a local JSON file.
The base URI for schema.ref is the directory containing the type file. The
resolved file MUST remain inside the collection root or inside the installed
pack root that supplied the type. Symlinks are resolved before this boundary
check. Escapes produce schema_ref_forbidden.
Nested file-to-file $ref references use the optional feature identifier
external_schema_refs. An implementation supporting the feature lists it under
optional_features in its claim document. Such references use the containing
schema document as their base URI, obey the same owning-root boundary, detect
cycles, and produce schema_ref_unresolved or schema_ref_cycle diagnostics.
Implementations that do not support the feature report unsupported_feature
before invoking a resolver.
Core tools MUST NOT fetch network references during collection validation.
Canonical https://mdbase.dev/schemas/... identifiers resolve only from the
tool's bundled schema registry. Other HTTP(S) references are
schema_ref_forbidden unless a non-portable extension explicitly enables them.
An embedded $id changes JSON Schema identifier resolution. The owning-root and
network boundaries continue to apply.
Canonical Schema Identity
Normative mdbase schemas have stable $id values under:
https://mdbase.dev/schemas/v0.3/
Companion profile schemas use their independently versioned namespaces:
https://mdbase.dev/schemas/interop/v0.1/
https://mdbase.dev/schemas/runtime/v0.2/
Implementations MUST validate against the canonical schema contents associated
with the exact declared spec/profile version. latest aliases are unsuitable
for validation.
Validation Result
JSON Schema validation errors are reported as mdbase validation issues. Each issue SHOULD include:
- path to the record
- JSON pointer or field path
- code
- severity
- message
- type name
- schema location when available
Tools SHOULD preserve native JSON Schema diagnostic detail when exposing machine-readable results.
JSON Schema validation diagnostics use schema_<keyword>, where the JSON
Schema keyword is converted to lower snake_case. For example, required
produces schema_required, additionalProperties produces
schema_additional_properties, and unevaluatedProperties produces
schema_unevaluated_properties. Implementations MUST NOT place camelCase JSON
Schema keyword names into the canonical diagnostic code field.
Additional Properties
Strictness is expressed with JSON Schema additionalProperties.
additionalProperties: false
A type that persists configured explicit keys such as type and types MUST
include them in its JSON Schema.
07. Collection Semantics
Purpose
match selects the records governed by a type. collection defines behavior
that depends on the surrounding Markdown collection.
match:
path_glob: "tasks/**/*.md"
collection:
display:
name_field: title
read_defaults:
status: open
unique:
- field: id
scope: type
links:
assignee:
target_type: person
validate_exists: true
Field References
Field references appear in collection sections, lifecycle set keys, update
unset lists, and implements field maps. A field reference has one of two
forms, distinguished by its first character.
Field path (the readable default). One or more object keys separated by
., where any segment may end in [] to select every item of an array, such
as title, metadata.owner, or blocks[]. A segment begins with an ASCII
letter or _ and contains only ASCII letters, digits, _, :, and -.
JSON Pointer (the exact form). A non-empty
RFC 6901 JSON Pointer beginning with
/, such as /@type, /metadata/owner, or /a~1b. Pointer tokens escape ~
as ~0 and / as ~1, so /a~1b selects the key a/b. Numeric tokens are
zero-based array indices. The URI-fragment form beginning with # and the
empty pointer are not accepted, because collection semantics always address a
field inside the frontmatter object.
Use a field path unless a key contains another character, such as @, $,
., /, or a space, or a single array item must be addressed. Only field paths expand arrays with [];
a pointer resolves exactly one value. An operation that accepts a link
collection applies its link rule to every item when the resolved value is an
array.
Lifecycle set creates missing intermediate objects. It MUST fail rather than
replace a non-object intermediate value. Assignment through an array index is
valid only when that array and index already exist.
Matching Decision Process
Matching uses the collection-relative record path, raw persisted frontmatter, the configured explicit type keys, and the loaded type registry.
For each record, an implementation follows this sequence:
- Inspect the configured explicit type keys in configuration order.
- If any configured key is present, read and validate its declarations, resolve those names against the type registry, and use the resulting explicit set.
- Otherwise, evaluate every type that has a
matchsection against the record. - Collect the matching types and order them by canonical lower-case name.
The explicit branch completes type selection. Inferred rules, including
match.expr, are skipped for that record. The selected types still perform
their normal schema and collection validation.
An explicit type value is a type-name string or a non-empty list of type-name strings. Declarations from several configured keys are concatenated in key order and case-insensitively de-duplicated while preserving the first occurrence. Invalid value shapes and unknown type names produce diagnostics.
The default keys are type and types. Configuring
settings.explicit_type_keys replaces that default list. An empty list selects
inferred matching for every record.
A type with no match section can be selected explicitly. It contributes no
inferred match.
Inferred Match Rules
All members present in one match object combine with AND:
match:
path_glob: "tasks/**/*.md"
fields_present: [title, "/@type"]
where:
status:
neq: done
This type matches a candidate only when its path, required fields, and
structured predicates all match. A list in path_glob combines its patterns
with OR. Every selector in fields_present MUST resolve to a raw, non-null
value.
Empty string, false, zero, and empty list count as present. A missing value or
explicit null does not.
Match rules read persisted frontmatter. Read defaults and projections are applied after type selection and therefore do not influence inferred matching.
Structured Predicates
match.where is the portable structured predicate language for basic matching.
A where mapping combines different field selectors with AND. A direct value
uses deep JSON equality. An operator mapping combines its operators with AND.
| Operator | Meaning |
|---|---|
eq, neq |
deep JSON equality or inequality |
gt, gte, lt, lte |
number, string, date, time, or date-time comparison |
contains |
string substring or array item equality |
containsAll, containsAny |
array contains all or any requested values |
startsWith, endsWith |
string prefix or suffix |
matches |
portable regular-expression match |
exists |
raw key presence, including an explicit null value |
Except for exists, an operator evaluates to false when its field is missing or
null. Ordering evaluates to false for incomparable values. neq also evaluates
to false for a missing field. A predicate with an operand of the wrong type
evaluates to false.
matches uses the same portable regular-expression subset as JSON Schema
pattern: Unicode-aware matching without backreferences or look-around. An
unsupported or invalid pattern is a type-file diagnostic.
CEL Matching
The CEL Match profile adds match.expr for inferred rules that need a portable
expression:
match:
path_glob: "tasks/**/*.md"
expr:
$expr: 'has(raw.tags) && tags.exists(t, t == "task")'
The expression combines with the other members of match using AND. It receives
the matching context defined in Chapter 10 and MUST evaluate to boolean true for
the type to match.
Implementations compile match.expr when loading the type. Parse and type
errors invalidate the type definition. A per-record evaluation error reports a
diagnostic for that record and expression and yields a non-match. False and null
also yield a non-match.
A type containing match.expr requires the cel_match conformance profile.
Loading it under a claim that omits cel_match produces
unsupported_profile.
Read Defaults
collection.read_defaults supplies effective read and query values for missing
fields.
collection:
read_defaults:
status: open
recurrenceAnchor: scheduled
For each defaulted field:
- a missing raw field receives the configured effective value
- an explicit
nullremains null - JSON Schema
requiredcontinues to evaluate the raw frontmatter - raw-frontmatter presence remains false
- read and query operations leave the persisted file unchanged
Create and editor tooling MAY mirror static values into JSON Schema default
annotations for presentation or scaffolding.
Three mechanisms supply default-like values, each with one purpose:
| Need | Mechanism | Persisted |
|---|---|---|
| a value readers and queries see when a field is missing | collection.read_defaults |
no |
| a value written into new records | lifecycle on_create with a literal, now, ulid, or other provider |
yes |
| a suggestion shown by editors and create forms | JSON Schema default |
only if the caller submits it |
JSON Schema default never changes validation, reads, or queries.
Links
JSON Schema validates a link field's local shape.
collection.links supplies its collection-level meaning.
collection:
links:
parent:
target_type: task
validate_exists: true
blocks[]:
target_type: task
validate_exists: false
"/relations":
target_type: task
validate_exists: true
blocks[] applies the rule to every item in the blocks array. Chapter 08
defines link parsing and resolution. /relations applies the same item-wise
rule when the exactly selected value is an array.
Cross-File Uniqueness
collection.unique declares collection-level uniqueness:
collection:
unique:
- field: id
scope: type
| Scope | Comparison set |
|---|---|
collection |
every record in the collection |
type |
every record matching the declaring type |
path_glob |
every record under the configured path glob |
Missing and null values are exempt from uniqueness comparison.
Path Policy
Path policy guides create and rename operations:
collection:
path:
pattern: "tasks/{id}.md"
The portable grammar uses {field} placeholders for top-level frontmatter
fields. Values are converted to strings without expression evaluation. A
missing or null value produces path_value_missing.
Generated paths MUST remain inside the collection root. Placeholder values that
produce /, \\, ., or .. path components are invalid. Runtime-owned path
logic and richer template languages belong under an x-* extension.
Display Metadata
collection.display contains advisory presentation metadata:
collection:
display:
name_field: title
description_field: summary
icon: check-circle
color_field: status
Editors, collection views, and generated documentation can use these values. Record validation ignores display metadata.
Projections
Collection projections are the optional feature collection_projections: an
effective-value feature declared outside JSON Schema and expressed in CEL:
collection:
projections:
is_overdue:
expr: 'due != null && due < today() && status != "done"'
Projection values are available to queries. Persistence occurs only through an explicit write operation or runtime workflow.
Collection projections enter the effective record and effective_frontmatter
under their declared field names, after read defaults are applied. They never
replace a persisted field: a record that persists a field with the same name as
a projection keeps its persisted value and reports a projection_shadowed
warning. A projection whose evaluation fails is absent from that record's
effective values and reports an expression diagnostic.
Projections evaluate in the query context of Chapter 10 without projection or
this. A projection MAY reference another collection projection by its field
name; implementations evaluate them in dependency order and reject cycles when
loading the type.
An implementation that does not advertise collection_projections still loads a
type that declares projections. It leaves the projection values absent and
reports a warning diagnostic with code unsupported_feature, the type name,
and details.feature: collection_projections, so that authors never lose
projections silently.
Query- and view-local named projections are separate, live under the
projection CEL namespace, and never replace a collection projection or
persisted field with the same name.
Private Domain Namespaces
Private annotations with no portable interoperability meaning use namespaced extension sections:
x-example-app:
fields:
status:
role: status
completed_values: [done, cancelled]
This keeps the JSON Schema reusable and gives each private extension an
explicit owner. Portable domain interfaces use the first-class data contracts
and type-local implements entries from Chapter 05A.
08. Links
Link Values
mdbase recognizes three link syntaxes:
| Syntax | Example |
|---|---|
| Wikilink | `[[people/alice |
| Markdown link | [Alice](people/alice.md) |
| Bare path | people/alice.md |
collection.links makes a field link-aware. JSON Schema validates its local
shape, usually string or an array of strings.
Link Components
Parsed links expose:
rawtargetaliasanchorformatis_relative
format is one of wikilink, markdown, or path.
Resolution
Resolution is performed against the collection root and the containing file.
General rules:
- Markdown links and bare relative paths resolve relative to the containing file's folder.
- Absolute collection paths use
/from the collection root. - Wikilinks beginning with
./or../resolve relative to the containing file's folder. Other wikilinks containing/, such as[[people/alice]], resolve from the collection root. - Simple wikilinks without path separators resolve by filename, matching the target against record filenames with or without their record extension.
- When
settings.id_fieldis configured, a simple wikilink first tries ID-based resolution against that field and falls back to filename resolution when no record has that ID.
After normalization, a link that escapes the collection root is invalid.
Ambiguity
If multiple records have the same configured ID, ID-based resolution is ambiguous and MUST fail without falling back to filename resolution.
If filename resolution finds multiple candidates, tools SHOULD apply stable tiebreakers:
- same directory as referring file
- shortest collection path
- alphabetical path
If ambiguity remains, resolution returns null and reports an ambiguous link warning.
Target Constraints
collection.links can require a target type:
collection:
links:
assignee:
target_type: person
validate_exists: true
When validate_exists is true, an unresolved link is a link_not_found
record validation issue whose severity follows the validation level in
Chapter 04.
When target_type is present, a resolved target is valid only if it matches the
target type.
Body Links
file.links includes, in this order and without de-duplication:
- values of frontmatter fields declared in
collection.links - every other frontmatter string, or string item of a frontmatter array, whose
complete value is a wikilink, such as
related: "[[alpha]]" - body wikilinks
- body Markdown links
Undeclared frontmatter values in Markdown-link or bare-path syntax are not links, because ordinary strings often contain paths.
Each entry is a link value that resolves exactly as the original link does,
without its alias or anchor: a wikilink target is written [[target]], and a
path that resolves from the containing folder, from a Markdown link, a bare
path, or a wikilink beginning with ./ or ../, is written with a leading
./ or ../. For example, [[people/alice|Alice]] becomes
[[people/alice]] and [notes](plan.md#goals) becomes ./plan.md.
file.embeds uses the same form. file.embeds includes
Markdown and wikilink embeds in the body.
Backlinks
file.backlinks is a list of link values, as produced by file.asLink(), for
the records whose file.links or file.embeds resolve to the current record,
ordered by referring record path. A referring
record appears once even when it links several times. A record that links to
itself appears in its own backlinks. Unresolved and ambiguous links create no
backlinks.
Backlinks are derived from the current collection state. After a successful write, subsequent reads and queries observe backlinks that reflect it.
Links and tags inside fenced code blocks and inline code MUST be ignored by body extraction.
Tags
file.tags includes:
- frontmatter tags from
tagswhen the field is string or list of strings - inline body tags beginning with
#
Inline tags must begin at the start of a line or after whitespace. URL fragments MUST NOT be treated as tags.
file.hasTag("project") uses complete tag-segment prefixes. It matches
#project and #project/alpha; its result for #projection is false.
Link Host Functions
The CEL profile defines host functions and methods for links:
link(value)file.hasLink(linkValue)file.hasTag(tag)file.asLink()linkValue.asFile()
asFile() returns null for a broken link. Selecting a field of null is a CEL
evaluation error, so traversal through a link that may be broken uses a null
check or optional selection, as shown in Chapter 10.
Round Trip
Write operations SHOULD preserve link format when updating references:
- wikilinks remain wikilinks
- Markdown links remain Markdown links
- bare paths remain bare paths
- aliases and anchors are preserved where possible
ID-based links SHOULD NOT be rewritten during rename if the target ID did not change.
09. Lifecycle
Purpose
lifecycle defines deterministic mutation-time behavior for managed fields.
Lifecycle is part of Core Write behavior and runs independently of the workflow runtime.
Events
Lifecycle policy runs on:
on_create, during a create operationon_update, during an update operation, including an update that only changes the body
Rename and delete do not run lifecycle policy. Behavior triggered by those operations belongs to workflows.
Actions
The value of each event is one action or a non-empty list of actions. An action
contains a set mapping and an optional CEL guard if:
lifecycle:
on_create:
- if: '!has(raw.id)'
set:
id: { ulid: true }
- set:
dateCreated: { now: true }
dateModified: { now: true }
on_update:
set:
dateModified: { now: true }
Each key of set is a field reference as defined in Chapter 07, and each value
is a value provider. set always assigns: it replaces a value that the caller
supplied or that already exists. A guard such as '!has(raw.id)' limits an
assignment to records where the field is missing.
Actions run in list order. Each guard evaluates against the draft as modified by
the preceding actions. All providers in one set read the draft as it was
before that action, and its assignments then apply together. When two actions
of one type assign the same field, the later assignment wins.
Standard Value Providers
Core lifecycle providers:
| Provider | Meaning |
|---|---|
{ now: true } |
current instant as an RFC 3339 date-time in UTC with Z |
{ today: true } |
current date in the operation timezone, as an RFC 3339 full-date |
{ uuid: true } |
random UUID in lower-case canonical form |
{ ulid: true } |
random ULID in upper-case Crockford Base32 |
{ slugify: fieldRef } |
slug of another field's string value |
{ copy: fieldRef } |
copy of another field's value |
{ literal: value } |
the literal value |
now and today use the operation's captured instant, so every provider in one
operation observes the same time. The operation timezone follows the same
precedence as the query timezone in Chapter 11.
slugify lowercases the value, transliterates it to ASCII where a
transliteration exists, replaces each run of other characters with -, and
trims leading and trailing -. A missing, null, or non-string source value
produces null. copy of a missing field removes the target key.
Guards
Lifecycle guards use the lifecycle CEL context from Chapter 10:
- current draft frontmatter fields at top level, and as
recordandraw oldfor the previous raw frontmatter on updatefilefor file metadataoperationfor operation metadata
old is a map, so a guard that reads a field that may be missing from the
previous record uses has() or optional selection:
lifecycle:
on_update:
- if: 'old.?status.orValue(null) != status && status == "done"'
set:
completedDate: { today: true }
A guard that fails to compile invalidates the type definition. A guard that
raises an evaluation error fails the operation with
lifecycle_expression_error. A guard that evaluates to anything other than
boolean true skips its action.
Validation Order
For create and update:
- parse input
- build a draft frontmatter object
- determine and freeze type membership from the draft and target path
- apply the lifecycle actions of every matched type
- re-evaluate membership and fail with
type_membership_changedif it differs - validate JSON Schema
- run collection validators
- write the file
Lifecycle MUST run before final validation so generated IDs and timestamps can satisfy required schema fields. Chapters 05 and 12 define membership freezing.
Read defaults MUST NOT run as lifecycle policy unless a write operation explicitly asks to materialize them.
Relationship To Workflows
Lifecycle is deterministic, local, and operation-scoped.
Workflows are event/action orchestration. They may read or write records, call agents, request approvals, run commands, or produce run state.
Generated IDs and timestamps belong in lifecycle. Cross-record automation and agent work belong in workflows.
Conflicts
When several matched types define lifecycle policy for the same event, each type's actions run as one unit and types run in matched-type order. The result MUST NOT depend on that order:
- identical normalized assignments to the same field from different types are allowed and execute once
- different assignments to the same field from different types are
type_conflicterrors before any write, whether or not their guards would run - diagnostics MUST report the type names and lifecycle paths involved
10. CEL Profile
Purpose
Portable v0.3 expressions use the Common Expression Language (CEL). This chapter defines the values an mdbase host binds, the mdbase host functions and types, limits, and how each embedding context handles evaluation errors.
mdbase does not change CEL's language semantics. Operators, macros, error propagation, and the standard function library behave as defined by the CEL specification, so conforming hosts can use an existing CEL engine.
The base cel conformance profile covers this shared expression model. Features
that embed CEL add their own context requirements: cel_match, cel_query,
Lifecycle, and the durable runtime.
Each containing object defines how source text is stored. Query where uses a
string, while match.expr and workflow expression values use an object such as:
$expr: 'status == "open"'
Plain strings in workflow inputs remain literal strings.
Language Requirements
Hosts MUST support:
- the CEL standard definitions, including
has(), the comprehension macros,timestamp,duration, and heterogeneous numeric comparison - the CEL optional types extension, including the
.?fieldand[?key]selection syntax,optional.of,optional.none,hasValue(),value(),or(), andorValue() - the mdbase host functions and bindings defined in this chapter
mdbase adds functions only under names that the CEL standard library does not define, so hosts never need to overload a standard function or operator.
Optional syntax is disabled by default in several CEL engines. A conforming host enables it for every mdbase expression context.
Expression Locations
CEL appears in:
match.expr- query filters and projections
- collection projections
- lifecycle guards
- workflow variables, conditions, inputs, iteration, and run policy
match.where uses the structured predicate language from Chapter 07.
Tools may translate another user-interface expression language to CEL before
writing portable records. A stored alternate dialect uses an x-* extension
whose owner defines its semantics.
Evaluation Contexts
Every expression location has a context contract. A host supplies exactly the system bindings listed for that context, along with the applicable top-level record fields.
| Context | Available values |
|---|---|
| inferred match | candidate raw fields at top level; record, raw, file |
| query or projection | effective candidate fields at top level; record, raw, file; projection; this as the invocation-context record or null |
| query summary | values; the fixed operation time and timezone |
| lifecycle guard | current draft fields at top level; record, raw, old, file, operation |
| workflow variable or trigger condition | event, workflow, trigger, vars |
| workflow step condition or input | event, workflow, trigger, steps, vars; item during iteration |
| workflow run-policy expression | event, workflow, trigger, vars |
An unavailable system binding is a compile or preflight diagnostic. For example,
steps is unavailable to a trigger condition because no step has run.
The system names record, raw, file, projection, this, values, old,
operation, event, workflow, trigger, steps, vars, and item are
reserved. A frontmatter field with one of those names remains available through
record.<field> and raw.<field>.
Query Context
In a query, top-level fields and record contain effective values. raw
contains persisted frontmatter.
!has(raw.status) && record.status == "open"
file supplies the candidate metadata and helpers defined by the collection,
query, and link profiles. projection contains named query projections after
their dependency-ordered evaluation.
this is reserved for the query invocation-context record. It is null when no
context is bound. A non-null context mirrors the candidate query namespaces:
this.<field>andthis.record.<field>expose effective context valuesthis.raw.<field>exposes persisted context frontmatterthis.fileexposes context-file metadata and the helpers available to the active profiles
When an effective context field conflicts with the reserved members record,
raw, or file, it remains available through this.record.<field> and
this.raw.<field>.
The host resolves and snapshots the context once before evaluating candidates.
All candidates see the same immutable context, operation time, and timezone.
Context link values and this.file helpers resolve relative to the context
record; candidate values and file helpers resolve relative to the candidate.
this is record-only and MUST NOT be repurposed as an arbitrary caller
parameter map. A feature that adds parameters uses a distinct binding and
declares its own context contract.
Chapter 11 defines portable context binding and saved-view invocation.
Matching Context
In match.expr, the candidate has not yet acquired a type. record, raw,
and top-level field names therefore refer to the same raw frontmatter object,
and frontmatter values are not typed from any schema.
file.inFolder("tasks") && has(raw.tags) && tags.exists(t, t == "task")
Read defaults and projections enter after matching and are absent from this context.
Lifecycle Context
Lifecycle expressions evaluate against the current write draft. Top-level
fields, record, and raw contain that draft. On update, old contains the
previous raw frontmatter. operation describes the active create or update and
file describes its target path and available metadata.
old.?status.orValue(null) != status
Workflow Context
Workflow expressions use the validated event envelope and workflow metadata.
Trigger and run-policy expressions execute before steps. Step expressions also
receive the standard results of completed steps. Iteration adds the current
item under the configured iteration name, with item as the default.
event.data.zone.id
steps.patch_status.output.path
The durable runtime profile defines when each workflow expression is evaluated.
Record Values
Frontmatter is converted to CEL values after the JSON data-model conversion in Chapter 06:
| JSON value | CEL value |
|---|---|
| object | map(string, dyn) |
| array | list(dyn) |
| string | string, or a timestamp as described below |
| integer-valued number written as a YAML integer | int |
| other number | double |
| boolean | bool |
| null | null_type |
CEL's heterogeneous numeric comparison makes priority >= 3 valid whether
priority is an int or a double. Arithmetic between int and double
follows CEL and requires an explicit conversion.
Missing Fields And Presence
mdbase preserves four observable record states:
| Raw state | has(raw.f) |
has(record.f) |
Top-level f |
|---|---|---|---|
| missing, no default | false |
false |
null |
| missing, read default | false |
true |
default value |
| explicit null | true |
true |
null |
| persisted value | true |
true |
persisted value |
A host binds every unreserved top-level identifier referenced by an expression.
An identifier naming a missing record field is bound to null. The host does not
bind other values for missing keys: raw.f and record.f on a missing key, and
field selection on null, are CEL evaluation errors.
Portable presence checks use CEL's has() macro: has(raw.f) tests persisted
presence and has(record.f) tests effective presence. Null-safe traversal uses
optional selection:
metadata.?owner.orValue("unassigned") == "alice"
Temporal Values
Representation
The profile adds no host types. Temporal values use CEL's standard types and strings:
- A date is an RFC 3339
full-datestring such as"2026-06-20". Because the format is fixed-width, CEL string comparison orders dates chronologically, sodue < today()needs no conversion. - A timestamp is CEL's standard
google.protobuf.Timestamp. - A duration is CEL's standard
google.protobuf.Duration.
Date fields therefore stay strings in every context. In query, projection, and
lifecycle contexts, a string at a location that every matched schema declares
format: date-time becomes a timestamp, because date-time offsets make string
comparison unreliable. A schema location is reachable from the root through
properties, items, and local $ref; locations inside allOf, anyOf,
oneOf, not, and conditional subschemas are not used for typing. A value
that fails its format stays a string and the record reports format_invalid.
Untyped date-times are converted explicitly with timestamp().
Functions
| Function | Result |
|---|---|
now() |
timestamp: the context's captured current instant |
today() |
date string: the calendar date of now() in the effective timezone |
date(string) |
the date string, validated as an RFC 3339 full-date |
date(timestamp) |
date string of the instant in the effective timezone |
startOfDay(string) |
timestamp of the start of that date in the effective timezone |
timestamp(string) |
CEL standard: RFC 3339 date-time with offset |
duration(string) |
CEL standard, for example duration("36h") |
These methods take a date string receiver:
| Method | Result |
|---|---|
d.addDays(int) |
date string |
d.addMonths(int) |
date string, clamped to the last day of the target month |
d.addYears(int) |
date string, clamping February 29 to February 28 |
d.daysUntil(string) |
int number of calendar days to another date, negative when earlier |
d.year(), d.month(), d.day() |
int; month() is 1 for January |
d.dayOfWeek() |
int ISO weekday, 1 for Monday through 7 for Sunday |
A date function or method whose date argument or receiver is not a valid
full-date raises an evaluation error.
Comparing a date string with a timestamp has no CEL overload and is an
evaluation error. Callers convert one side explicitly, for example
startOfDay(due) < now() or due < date(now()). Fixed spans use
timestamp ± duration; calendar spans use the date methods.
The effective timezone is the operation or query timezone defined in
Chapter 11 and MUST be declared by every operation or runtime context that
evaluates today(), date(timestamp), or startOfDay().
Serialization
Date results are already strings. A timestamp result serializes as an RFC 3339
date-time in UTC with a Z suffix. A duration result serializes as a CEL
duration string in seconds, such as "5400s". Typing never changes persisted
or effective_frontmatter values; it applies only to expression evaluation and
expression results.
Text Helpers
Standard CEL compares strings exactly. For case-insensitive matching, the profile adds two string methods:
| Method | Result |
|---|---|
s.lower() |
s with every character mapped to lowercase |
s.upper() |
s with every character mapped to uppercase |
Both use the Unicode default full case mappings without locale tailoring, so
"Éclair".lower() is "éclair" and "Straße".upper() is "STRASSE". A
case-insensitive search lowercases the text it searches:
file.body.lower().contains("mission body")
File And Link Helpers
Core Read supplies file metadata and:
file.inFolder(path)
The Links profile adds:
link(value)file.links,file.embeds,file.tags, andfile.backlinksfile.hasTag(tag)file.hasLink(linkValue)file.asLink()linkValue.asFile()
asFile() returns the resolved record in the same shape as a query candidate:
its effective fields at top level, plus record, raw, and file members. It
returns null for an unresolved link. A link value resolves relative to the
record it was read from: the candidate, the this context, or a record
returned by asFile(), so project.asFile().lead.asFile() resolves lead
from the project's folder. Because field selection on null
is an error, traversal through a possibly broken link uses a guard or an
optional:
assignee != null && assignee.asFile() != null &&
assignee.asFile().team == "engineering"
Traversal depth is bounded as described below. Expressions can use helpers supplied by every profile in the implementation's conformance claim.
Limits
Implementations MUST bound expression source size, AST depth, evaluation work, and link traversal. The portable minimum supported limits are:
| Limit | Minimum supported value |
|---|---|
| expression source | 64 KiB |
| AST depth | 100 |
| link traversal depth | 10 |
Hosts SHOULD also bound list iteration, elapsed evaluation time, and memory. Operational limits are reported in conformance claims. Exceeding a limit produces a diagnostic identifying the limit and configured value.
Compilation And Evaluation Errors
All stored expressions MUST compile during preflight of their containing type,
query, lifecycle policy, workflow, or runtime object. Parse errors and
references to unavailable system bindings invalidate that containing object.
Hosts MAY type-check expressions with record fields declared as dyn; a
type-check error is a compilation diagnostic only when the expression could not
evaluate successfully for any record.
CEL evaluation errors propagate as defined by the CEL specification, including
its commutative absorption by && and ||. The containing context decides what
a top-level error means:
| Context | Top-level evaluation error |
|---|---|
| inferred match | report a diagnostic and treat the candidate as a non-match |
| query filter | exclude the candidate and report a diagnostic |
| query or collection projection, selection, or ordering value | use null for that value and report a diagnostic |
| lifecycle guard | fail the write operation with lifecycle_expression_error |
| workflow trigger condition | create a failed-run diagnostic |
| workflow step or run policy | fail the step or run with a runtime diagnostic |
A condition that evaluates to anything other than boolean true does not match
or execute.
Diagnostics SHOULD include the source, code, message, context name, and line,
column, or byte range when available. Compilation diagnostics use
expression_compile_error; evaluation diagnostics use
expression_evaluation_error. A context that evaluates an expression once per
candidate MAY aggregate identical evaluation diagnostics as defined in
Chapter 11.
11. Querying
Query Object
A query selects records from a collection. The canonical query-object schema is
schemas/v0.3/query.schema.json.
types: [task]
timezone: Australia/Melbourne
context:
this:
path: projects/alpha.md
projections:
is_overdue:
expr: 'due != null && due < today() && status != "done"'
where: 'status != "done" && priority >= 3'
select:
- title
- due
- projection.is_overdue
order_by:
- field: due
direction: asc
limit: 20
offset: 0
include_body: false
Unknown query members are invalid. x-* members MAY carry adapter metadata but
MUST NOT change the meaning of portable members.
Schema violations and semantic preflight failures such as cyclic projection
dependencies or duplicate result names produce invalid_query and abort before
candidate evaluation.
Temporal Execution Context
A query MAY supply timezone as an IANA timezone identifier. It is ephemeral
execution context: it applies only to this invocation and is not collection or
saved-view state. UTC is the canonical identifier for Coordinated Universal
Time. Numeric offsets and ambient aliases such as local are not valid query
timezone identifiers because they cannot model daylight-saving transitions or
identify a durable calendar authority.
The effective query timezone is resolved in this order:
- the invocation's
timezone - the collection's configured timezone
- the implementation's documented local-runtime default
An invalid invocation timezone produces invalid_timezone and aborts before
candidate evaluation. An implementation MUST NOT silently substitute another
timezone for a supplied value.
The execution authority captures the current instant once per query. now(),
today(), conversion of datetimes to calendar dates, and calendar arithmetic
MUST all use that captured instant and the same effective timezone throughout
the execution. Callers do not supply the current instant through this field.
Interactive callers SHOULD send their current IANA timezone on every temporal query. Headless callers MAY omit it to use the durable collection default.
Types
types is an OR filter. A record is included if it matches at least one listed
type.
If types is omitted, all records are candidates.
Invocation Context And `this`
A query MAY bind one collection record as its invocation context:
context:
this:
path: projects/alpha.md
The path is collection-relative and MUST resolve to an ordinary record in the
same collection. The implementation reads the context through the same raw and
effective-value pipeline as any other record, snapshots it once before
candidate evaluation, and binds it to this as defined in Chapter 10.
When no context is supplied, this is null. Supplying an unresolved context
produces context_not_found and aborts the query before candidate evaluation.
An invalid context is handled according to the collection validation level.
Query time, effective timezone, and collection state are fixed for the context and all candidates in one execution. A caller MUST NOT replace or mutate the context between candidate evaluations.
Named Projections
Queries MAY define named projections evaluated for every candidate before the filter:
projections:
is_overdue:
expr: 'due != null && due < today() && status != "done"'
urgency:
expr: 'priority + (projection.is_overdue ? 10 : 0)'
Projection names are available under projection.<name> in subsequent
projection expressions, where, selection expressions, ordering, grouping,
summaries, and presentation mappings.
Implementations MUST resolve projection dependencies deterministically and reject direct or indirect cycles before evaluating candidates. A projection evaluation error produces null for that candidate's projection value and a per-record diagnostic; it does not abort evaluation of other candidates.
Named query projections are effective query values. They are not persisted and do not replace raw or effective frontmatter fields with the same name.
Where
where is a CEL expression evaluated against the effective candidate and its
named projections. The expression result includes the record only when it is
boolean true.
An evaluation error MUST exclude that record and report a diagnostic. It does not abort evaluation of other candidates.
Selection
Queries MAY request a logical result shape:
select:
- title
- file.path
- projection.is_overdue
- name: display_due
expr: 'due == null ? "Unscheduled" : string(due)'
label: Due
A string selects an effective field, file value, or named projection. An object
defines a named CEL result value. Selection expressions run after filtering and
may use the candidate, named projections, and this.
For a string selector, the output name is the effective-field name or the final
member of a file.<name> or projection.<name> selector. An object uses its
required name. Two selections that produce the same output name make the
query invalid; callers use an object selection to assign an unambiguous name.
Computed selection values belong to the query result. Persistence requires an
explicit write operation. Every result includes file.path in its file
object even when file.path is not selected.
Ordering
order_by sorts by one or more effective fields, file values, named
projections, or named selection values.
order_by:
- field: due
direction: asc
- field: projection.urgency
direction: desc
Null values sort last in ascending order and first in descending order.
When all ordering fields compare equal, tools MUST tie-break by ascending
file.path for deterministic results.
Grouping And Summaries
Queries MAY group the complete filtered and ordered result set by one or more values:
group_by:
- field: status
direction: asc
Each distinct ordered tuple of grouping values creates one group. Null and
missing values form the same null group. Group tuple ordering applies the same
direction and null rules as order_by. Results within a group retain query
order.
Queries MAY define custom summary functions and apply built-in or custom functions to values:
summary_functions:
completion_rate:
expr: 'values.size() == 0 ? 0 : values.filter(v, v).size() * 100 / values.size()'
summaries:
- field: estimate
function: sum
name: total_estimate
- field: projection.is_completed
function: completion_rate
name: completion_rate
A custom summary expression receives values, an ordered list containing one
value per matching result in result order. Missing fields contribute null.
When grouping is active, the function is evaluated independently for each
group; otherwise it is evaluated once for the complete match set.
A summary's result key is its explicit name, or its function identifier when
name is omitted. Duplicate result keys make the query invalid.
The portable built-in summary identifiers are count, sum, average,
minimum, maximum, earliest, latest, empty, and filled. count
counts all values. empty and filled count empty and non-empty values using
the CEL missing/null contract. Other built-ins ignore null values and produce
null when no compatible non-null value exists. Incompatible non-null values
produce a summary diagnostic and a null summary result.
Grouping and summaries are calculated before pagination and therefore describe
the complete filtered match set. Group metadata does not replace the flat
results page.
Pagination
limit and offset apply after filtering and sorting.
limit: 0 returns an empty result page. Total count, grouping, and summary
metadata still describe the complete match set.
Body Search
file.body MAY be used in filters even when include_body is false. In that
case the body is available for filtering but not returned in results.
Tools MAY report that body filtering requires a profile or index when they cannot read bodies on demand.
Frontmatter Mode
frontmatter_mode controls which fixed-semantics frontmatter members appear in
each result:
effective(the default) returnseffective_frontmatterpersistedreturnsfrontmatterbothreturns bothfrontmatterandeffective_frontmatter
The mode changes result serialization only. Filtering, projections, selection, ordering, grouping, and summaries continue to use effective values unless an expression explicitly reads the persisted namespace. Unlike a complete record document returned by Core Read or a mutation, a query result is a summary and MAY omit the member excluded by the requested mode.
Result Envelope
Query results MUST use this envelope:
results:
- file:
path: tasks/fix-login.md
effective_frontmatter:
title: Fix login
status: open
values:
title: Fix login
is_overdue: true
meta:
total_count: 1
has_more: false
context:
path: projects/alpha.md
groups:
- values:
status: open
count: 1
summaries:
total_estimate: 30
diagnostics: []
Each result MUST include file.path. meta.total_count is the count before
pagination, and meta.has_more is true when additional matching records remain.
Diagnostics use the canonical diagnostic envelope from the conformance chapter.
Read defaults are included in effective_frontmatter. values contains
requested selection values and is omitted when select is omitted.
meta.context.path identifies the bound invocation context and is omitted when
this is null. meta.groups is present only when grouping or summaries were
requested. Without grouping, summaries appear in one group whose values is an
empty object.
Evaluation Diagnostics
Per-candidate evaluation errors are reported in diagnostics with severity
warning, code expression_evaluation_error, and the expression location in
details.expression. An implementation MAY aggregate diagnostics that share
code, expression location, and message into one diagnostic whose details
contains count and up to 10 sample record paths. Aggregation never hides a
distinct message or expression location.
Saved View Records
A saved view is an ordinary Markdown record whose matched type implements the
mdbase.view record contract (Chapter 05). The canonical query object above
supplies its execution semantics, and the view record provides a portable
persisted container. The contract's record schema is
schemas/v0.3/view.schema.json. The example below uses the canonical view
type, which maps each contract field to the same-named frontmatter field.
One view record contains a shared canonical query fragment and one or more
named views. The shared fragment is nested under query so its types member
cannot be mistaken for the record-level explicit type declaration:
---
type: view
id: tasknotes.tasks
version: 1
name: Task views
query:
types: [task]
projections:
urgency:
expr: 'priority + (due != null && due < today() ? 10 : 0)'
views:
- id: today
name: Today
where: 'due == today() || scheduled == today()'
select: [title, due, projection.urgency]
order_by:
- field: projection.urgency
direction: desc
presentation:
type: tasknotes.task-list
fallback: mdbase.table
---
# Task views
Reusable task views for editors, CLIs, and agents.
Named-view resolution
A named view is addressed by the view record path or stable record ID plus the named-view ID. Human-readable names are not identifiers. Duplicate named-view IDs make the view record invalid.
A tool resolves a view record through its contract view:
- the record MUST match exactly one type that implements
mdbase.view; a record that matches none is not a view and producesview_not_found, and a record that matches several producesinvalid_view - the tool constructs the
mdbase.viewcontract view through that type's field mapping and validates it as Chapter 05A requires; an invalid contract view producesinvalid_view
All remaining steps read the contract view, not raw frontmatter. To derive an executable query:
- inherit
query.typesunless the named view suppliestypes - combine
query.whereand named-viewwherewith AND - merge
query.projectionsand named-view projections; the same name may appear in both only when its parsed definition is structurally equal, ignoring mapping key order - inherit
query.contextunless the named view supplies context - copy the named view's selection, ordering, grouping, summaries, pagination, and body options
- bind the invocation context using the rules below
Record-level property metadata and summary functions remain available to every
named view. Resolving an unknown view record or named-view ID produces
view_not_found.
View execution returns the query envelope and adds
meta.view: { path, id }, using the resolved view-record path and named-view
ID.
A saved-view execution invocation MAY supply timezone with the same syntax,
precedence, validation, and fixed-clock semantics as a direct query. The
invocation timezone is applied after named-view resolution and is not written
into the saved view. This lets one portable today() view follow its caller's
calendar day without local post-filtering or daily view rewrites.
Property-metadata keys MAY name effective fields, file.* values,
projection.* values, or selection outputs. They provide labels, descriptions,
format hints, and visibility hints. select remains the source of result
values.
View invocation context
View records declare what to do when the caller supplies no context:
query:
context:
this:
on_missing: view
types: [project]
on_missing values are:
| Value | Behavior |
|---|---|
view |
bind the view-definition record; default |
null |
bind this to null |
error |
abort with context_required |
An explicitly supplied context always wins. If types is present, a non-null
context must match at least one listed type or execution aborts with
context_type_mismatch. A named-view context declaration replaces, rather than
partially merges with, the shared query.context declaration.
An embedding host supplies the embedding record. A headless caller supplies a collection-relative record path. An editor may map an active record to the explicit invocation context, but ambient concepts such as workspace leaves or sidebars are not part of portable execution.
Presentation
presentation.type and presentation.fallback are open renderer identifiers,
not a closed enumeration. select defines the portable logical result shape;
presentation mappings and options describe how a supporting tool may render
that result.
Each presentation.mappings key is a renderer-defined role and its value names
an output produced by select. Selected named projections are therefore
available to presentation without giving presentation its own projection
language. A named view with no presentation is a complete headless saved
query.
Saved-view discovery exposes the selected outputs as ordered property descriptors. A renderer uses those descriptors to preserve selection order and labels while reading the corresponding values from each result row.
Presentation metadata is advisory. Filtering, projection, ordering, grouping,
summaries, pagination, and the headless result envelope retain their canonical
query semantics. Headless execution succeeds for every renderer identifier. A
request for rendered output reports unsupported_presentation when neither the
requested renderer nor its declared fallback is available.
Source syntax, renderer configuration, and round-trip data use x-*
extensions. The Obsidian Bases adapter defines
the adapter contract for Obsidian .base sources.
Optional support
Core Read implementations treat a view record as an ordinary typed record. A
tool advertises view_records in its optional_features claim when it
resolves, lists, and executes named views with the semantics in this chapter.
Because views are identified through the mdbase.view contract, a tool that
advertises view_records also claims data_contracts.
12. Operations
Operation Model
Write-capable tools operate on Markdown files while preserving the collection contract.
Core operations:
- read
- create
- update
- delete
- rename
- batch
Optional saved-view operations:
- list_views
- execute_view
- read_view_source
- create_view_source
- update_view_source
- delete_view_source
Read
Read returns a record by path, including:
- persisted
frontmatter - derived
effective_frontmatter - body
- file metadata
- matched type names
- diagnostics
Read MUST NOT write defaults or lifecycle values to disk. Every successful read returns the complete record document defined in Chapter 03.
Read accepts optional include_document. When true, the returned record MUST
include the exact document source defined in Chapter 03. Providers that
cannot supply exact source MUST reject the request rather than silently return
a reconstructed serialization.
Create
Create builds a new record.
Pipeline:
- determine target type or types from explicit input or path/match policy
- build draft frontmatter
- freeze pre-lifecycle type membership
- apply lifecycle
on_create - verify type membership did not change as a lifecycle side effect
- validate JSON Schema
- run collection validators
- choose or validate path policy; the final path's extension fixes the record's format (Chapter 03), and a body is rejected when that format is a YAML document
- write Markdown file
- update derived indexes
- emit watch/runtime events after state is consistent
Static JSON Schema defaults MAY be used by editor and create interfaces. Validation-time mutation occurs when the create operation explicitly copies a default into the draft.
Selected types and membership representation
A request-level type selector identifies the type the caller intends to create; it is not itself a persisted frontmatter declaration. Contract selection MUST resolve and validate its designated implementing type before writing.
When explicit declaration keys are configured, implementations MUST persist a
selected type using those keys, preserving existing valid memberships. They
MUST NOT hard-code type or types when those keys are not configured. A field
outside the configured declaration keys remains ordinary application data.
When settings.explicit_type_keys is empty, implementations MUST NOT add a type
declaration field and MUST NOT reject a create solely because the list is empty.
The selected type guides schema, lifecycle, and path policy. Membership is
inferred from persisted frontmatter and the final canonical path. The selected
(or contract-designated) type MUST be included in the final inferred membership;
otherwise the operation MUST fail without writing a record. All applicable
types participate in validation, not only the requested type. Matching errors
MUST NOT be silently treated as non-matches.
The pre-lifecycle membership freeze and final membership check still apply. Read defaults and projections are not persisted evidence of membership. A successful create guarantees membership for the written record under the current collection rules, not permanent membership after later user edits.
Update
Update modifies an existing record.
Pipeline:
- read existing raw frontmatter
- apply the requested
patchandunset - re-match and freeze types when type-affecting fields or path changed
- apply lifecycle
on_update - verify type membership did not change as a lifecycle side effect
- validate JSON Schema
- run collection validators
- write frontmatter and preserve body
- update derived indexes
- emit watch/runtime events
A structured update accepts:
patch: an object whose top-level keys are set to the supplied values, replacing any existing value; a null value persists an explicit nullunset: a list of field references, as defined in Chapter 07, whose keys are removed from persisted frontmatterbody: optional replacement Markdown body; a non-empty body for a YAML document record (Chapter 03) isinvalid_request
Unsetting a key that is already missing is not an error. A request that names
the same field in patch and unset, names a field inside a key that patch
replaces, or uses an unset reference that selects an array item, is invalid
and produces invalid_request before any write. When unset removes the last key of a
nested object, the now-empty object remains.
As an alternative to a frontmatter patch and body replacement, Update accepts
document containing the complete candidate source in the record's format:
Markdown source for a Markdown record, or the whole YAML document for a YAML
document record. A document
replacement MUST NOT be combined with patch, unset, or body. The
candidate is parsed and passes through the same type matching, lifecycle,
validation, concurrency, and atomic-write pipeline as a structured update. When lifecycle policy does not alter the candidate, the exact supplied
source MUST be preserved. If policy changes persisted values, the authoritative
post-policy source MAY be reserialized and is returned when source was
requested.
Update accepts optional include_document. A document replacement implies
include_document: true for its successful result.
Delete
Delete removes a record.
Tools supporting link/reference profiles SHOULD optionally report broken backlinks before deleting.
Delete is a Core Write operation and MAY emit an event for workflow runtimes.
Rename
Rename moves a record within the collection. It accepts from and to
collection-relative paths, optional if_revision, and optional update_refs.
Tools MUST reject target paths that escape the collection root.
If reference updating is enabled, link updates SHOULD preserve link style, alias, and anchor where possible. ID-based links SHOULD not be rewritten if the target ID did not change.
An implementation MAY commit a rename and its reference updates as one atomic batch. Otherwise reference updates are applied after the rename, and failed reference updates are reported per file.
Batch
Batch applies a list of create, update, delete, and rename operations as one request:
operations:
- kind: update
input:
path: tasks/a.md
patch: { status: done }
if_revision: sha256:opaque
- kind: rename
input:
from: tasks/b.md
to: archive/b.md
dry_run: false
allow_partial: false
Each item names its operation kind and that operation's input. A request
that names one record path more than once, through path, from, or to,
fails with duplicate_batch_path before any operation is prepared, so the
items of one batch never depend on each other.
Atomic execution is the default. The implementation prepares every
operation, including lifecycle, type membership, schema and collection
validation, path policy, and if_revision, against a staged copy of the
collection. If any operation fails, nothing is written. Otherwise all changes
commit as one recoverable transaction: after a failure or crash, recovery
restores either the complete pre-batch state or the complete committed state
before normal collection operations resume.
Partial execution is selected with allow_partial: true. Each operation is
prepared and committed independently in request order, and a failed operation
does not prevent the others. A host that can commit only one atomic transaction
per request, such as a durable runtime action, MAY reject allow_partial with
invalid_request.
Dry runs with dry_run: true prepare every operation and report the
results without changing files, indexes, runtime state, or revisions.
The result lists every operation in request order:
operations:
- index: 0
kind: update
valid: true
result: {}
diagnostics: []
succeeded: 1
failed: 0
preflight: false
dry_run: false
result holds the operation's own result on success. preflight is true when
the operations ran only against the staged copy, which happens for a dry run or
a failed atomic batch. The envelope's valid is false when any operation
failed.
Watch notifications and runtime events for an atomic batch are delivered after the whole batch commits. For a partial batch they follow each committed operation.
Saved Views
list_views discovers the saved-view sources available through the collection
provider. Its result has this shape:
valid: true
result:
views:
- id: task.views
name: Task views
source:
path: views/tasks.md
format: mdbase.view
revision: opaque-source-revision
writable: true
views:
- id: today
name: Today
properties:
- key: title
label: Task
- key: urgency
label: Urgency
presentation:
type: tasknotes.task-list
meta:
total_count: 1
diagnostics: []
source.path is a collection-relative path. source.format is a stable source
format identifier. source.revision is an opaque token for the source content.
source.writable describes whether the provider accepts writes for that source
format. Each nested descriptor exposes the stable named-view ID, its display
name, and its optional presentation metadata. Discovery order is ascending by
source path and then source-defined named-view order.
properties lists the named view's result values in display order. Each entry
has the result key and may include label, description, format, and
hidden metadata. Canonical view records derive this list from select.
Compatible external sources derive it from their ordered property list and
property metadata. Projection and formula results use the same descriptors as
persisted fields.
Malformed configured sources are omitted from result.views and reported as
warning diagnostics. Reading a malformed source explicitly produces
invalid_view.
execute_view accepts:
path: views/tasks.md
view: today
context:
path: projects/alpha.md
limit: 50
offset: 0
render: false
path and view select a descriptor returned by list_views. context binds
the invocation context defined in Chapter 11. limit and offset override the
named view's pagination for this invocation. The provider evaluates the
source's declared expression dialect and returns the query result envelope with
meta.view.
render: false requests the headless result and is the default. render: true
requests renderer output using the selected presentation metadata.
Saved-view source operations
A provider that advertises a source as writable: true MUST support the four
saved-view source operations. These operations exchange the complete source
document so format-aware editors can preserve source data they do not
interpret.
read_view_source accepts a path from list_views and returns:
path: TaskNotes/Views/tasks.base
format: obsidian.base
revision: sha256:opaque
document: |
views:
- type: tasknotesTaskList
name: Tasks
create_view_source accepts document and may accept path, format, and
name. When path is absent, the provider selects a collection-relative path
using the requested format and the collection's format configuration. Creation
MUST validate the complete document and MUST fail with path_conflict rather
than replace an existing source.
update_view_source accepts path, document, and optional if_revision.
The complete candidate document MUST be valid before the current source is
atomically replaced. delete_view_source accepts path and optional
if_revision.
All source operations apply the collection's path-boundary and symlink rules.
Update and delete use the concurrency behavior defined below. A successful
create or update returns the same fields as read_view_source; a successful
delete returns path and deleted: true.
Concurrency
Write-capable tools SHOULD detect external modification between read and write using mtime, content hash, version token, or platform-specific file identity.
On conflict, tools MUST preserve the current file and report a concurrency diagnostic.
Successful reads MUST return a stable revision token derived from the raw
file state. Write operations MUST accept an optional if_revision token and
fail with concurrent_modification when it no longer matches. The token format
is implementation-defined and opaque to callers.
Operation Result Envelope
Every operation returns a mapping with:
valid: true
result: {}
diagnostics: []
valid is false when any error-severity diagnostic applies. Successful
create, update, and rename operations MUST return the complete,
authoritative post-write record document defined in Chapter 03. rename adds
its rename-specific metadata to that document.
The document MUST be derived from the bytes actually persisted after lifecycle hooks, normalization, validation, and the atomic write complete. Clients and transport adapters MUST NOT reconstruct missing document members from the request or from another response member.
Create and rename accept optional include_document; when true, their
successful record document includes the exact post-write source. Update follows
the source rules above.
Dry runs return operation-specific preflight results rather than record documents. They use the same envelope and MUST NOT change files, indexes, runtime state, or revisions.
Events
After a successful mutation, tools MAY emit watch/runtime events. Events MUST be delivered after the derived read/query state is consistent. Watch consumers and workflow runtimes may subscribe to the same stream.
13. Migrations And Compatibility
Migration Philosophy
v0.3 uses the source model defined by Chapters 01–12. Migration translates v0.2.x collections into that model and reports features that need adapter-owned handling.
Migration tooling should produce:
- a readable diff
- a machine-readable report
- explicit unsupported-feature notes
- generated output only with user approval or generated-file detection
Migration analyzes the complete collection. Before a write, tooling MUST validate every existing record against the proposed target types and include incompatible records in the report.
Configuration
Configuration migration preserves which files are records and how they validate and resolve:
spec_versionbecomes0.3.0.settings.default_validation, or a top-leveldefault_validation, becomessettings.validation. When neither is set, migration writesvalidation: warn, the v0.2 default.- A top-level
id_fieldmoves undersettings. When neither is set, migration writesid_field: id, because v0.2 resolved wikilinks by ID by default. settings.extensionsbecomessettings.record_extensions, without leading dots and includingmd.settings.include_subfolders: falseadds the exclusion*/**, and the setting is removed.- Each
settings.excludepattern becomes a portable glob that excludes the same paths. A bare name without/,*,?, or[excluded that root path and everything below it, soarchivebecomesarchive/**. A pattern without/that contains a wildcard matched file names at any depth, so*.draft.mdbecomes**/*.draft.md. A pattern containing/is kept. Patterns that only repeat the built-in exclusions from Chapter 02 or name the types or contracts folder may be dropped. A pattern that is not a portable glob is reported for review. - Other v0.2 settings, such as
default_strictand the write options, have no v0.3 meaning. Migration moves them underx-legacy-v0.2in the configuration and migrates strictness into each type'sadditionalProperties.
Type Mapping
| v0.2.x feature | v0.3 destination |
|---|---|
fields |
JSON Schema properties |
field required |
JSON Schema root required |
type: string |
{ type: "string" } |
type: integer |
{ type: "integer" } |
type: number |
{ type: "number" } |
type: boolean |
{ type: "boolean" } |
type: date |
{ type: "string", format: "date" } |
type: datetime |
{ type: "string", format: "date-time" } |
type: time |
{ type: "string", format: "time" } |
type: enum, values |
JSON Schema enum |
type: list |
JSON Schema array |
type: object |
JSON Schema object |
type: link |
JSON Schema string/array plus collection.links |
default |
JSON Schema default and/or collection.read_defaults |
strict |
additionalProperties |
unique |
collection.unique |
generated |
lifecycle |
computed |
query projection, collection projection, or workflow |
path_pattern |
collection.path.pattern |
display_name_key |
collection.display.name_field |
extends |
JSON Schema $ref/allOf or explicit duplication |
Defaults
Migration should distinguish:
- creation/editor hints: JSON Schema
default - effective read/query defaults:
collection.read_defaults - dynamic values:
lifecycle
For current mdbase field defaults, the safest migration is to emit both JSON
Schema default and collection.read_defaults for static scalar defaults, with
a report explaining the difference.
Generated Fields
Generated fields migrate to lifecycle. v0.2 generated values apply only when the
field is missing, while lifecycle set always assigns, so migration adds a
!has(raw.field) guard to preserve that rule:
| v0.2.x generated | v0.3 lifecycle |
|---|---|
now |
on_create: [{ if: '!has(raw.field)', set: { field: { now: true } } }] |
now_on_write |
unguarded on_create and on_update actions setting { now: true } |
uuid |
guarded on_create action setting { uuid: true } |
ulid |
guarded on_create action setting { ulid: true } |
slugify from field |
guarded on_create action setting { slugify: source } |
from field without a transform |
guarded on_create action setting { copy: source } |
Computed Fields
Computed fields migrate to one of:
- query projection suggestion
collection.projectionswhen the target tool supports it- workflow/runtime policy when the computed value should be materialized
- unsupported note when the computation has side effects or depends on non-portable functions
Expressions
Current mdbase expressions migrate to CEL where possible.
Tool-specific expression dialects are adapter concerns. Tools may translate them to CEL for portable storage and translate them back for user interfaces or exports.
Runtime Workflows
Current generated-field and tool-conforming behavior that causes mutation should be reviewed as lifecycle or workflow behavior.
Generated IDs and timestamps usually become lifecycle.
Cross-record behavior, agent work, approval flows, external APIs, and scheduled checks become workflows and action/event contracts.
Data Contract Implementations
Portable application interfaces migrate to a local data contract plus a
type-local implements entry.
Example:
implements:
- contract: example.task
version: 1.0.0
fields:
status: status
binding:
completed_values: [done, cancelled]
The migration bundle MUST include the exact mdbase.contract artifact required
by the implementation. Existing convention-based x-* objects do not become
contracts merely because they contain keys named contract or version.
Private annotations with no portable contract meaning remain under a
namespaced x-* section. They SHOULD NOT become JSON Schema custom keywords.
Version Detection
A v0.2.x type file usually has name and fields without kind: mdbase.type.
A v0.3 type file has kind: mdbase.type and schema.
Migration tooling SHOULD refuse ambiguous files unless the user supplies an explicit source version.
Safe Migration Protocol
A conforming migration command has separate analyze and apply phases:
- discover source config, type files, extension metadata, and records
- generate proposed config/types without modifying the collection
- validate generated schemas and type files
- validate all existing records against the proposed target
- emit a human-readable diff and machine-readable report
- require explicit approval unless every target is recognized as generated
- create a backup manifest containing hashes and original paths
- write through temporary files and atomic renames where supported
- re-open and validate the migrated collection
Apply MUST abort before writes when analysis has unsupported features or invalid target records unless the caller explicitly selects a documented partial mode. A failed apply restores files from the backup manifest when restoration is possible and reports any paths requiring manual recovery.
Apply SHOULD durably journal the currently attempted path and completed paths inside the backup before each replacement. Tooling MUST provide a recovery path that can resume or restore an interrupted apply. Restoration MUST validate the backup hashes from the manifest before replacing current files and MUST require explicit approval when invoked as a separate command.
Repeated analysis of unchanged inputs MUST produce the same report. Re-applying an already migrated type MUST fail without writing.
Unknown source metadata is preserved under x-legacy-v0.2 with its original
key paths unless a named migration adapter handles it. YAML comments and style
preservation are best effort. Markdown bodies MUST be preserved byte for byte.
The report includes source and target hashes, generated-file evidence, exact mappings, warnings, unsupported constructs, invalid record paths, proposed file operations, backup location, and post-apply validation status.
14. Conformance
Conformance Profiles
A v0.3 conformance claim names the atomic behavior sets an implementation has verified. Profiles let readers distinguish collection reading, matching, queries, writes, runtime preflight, workflow execution, and watching.
| Profile | Purpose |
|---|---|
| Core Read | discover collections, parse records, load types, match records, and validate JSON Schema |
| Collection Semantics | apply defaults, uniqueness, path policy, multi-type composition, and collection diagnostics |
| Data Contracts | load contracts, resolve type implementations, compute digests, and project contract views |
| CEL | compile and evaluate the shared mdbase CEL language and host contract |
| CEL Match | evaluate match.expr against raw candidate records |
| Query | evaluate contextual CEL filters, projections, grouping, summaries, and query envelopes |
| Links | parse, resolve, validate, and traverse links |
| Core Write | create, update, delete, rename, and batch records |
| Type Packs | assess and transactionally apply managed type packs |
| Lifecycle | apply standard managed-field policy during writes |
| Event/Action Interoperability | exchange CloudEvents and admitted action invocations through independently claimable roles |
| Durable Runtime 0.2 | validate standard runtime records, admit exact plans through interoperability declarations, and execute/recover them durably |
| Watch | report ordered collection changes after consistent state |
Normative profile IDs and dependencies are:
| Profile ID | Requires |
|---|---|
core_read |
none |
collection_semantics |
core_read |
data_contracts |
core_read |
cel |
none |
cel_match |
core_read, cel |
cel_query |
collection_semantics, cel |
links |
collection_semantics, cel |
core_write |
collection_semantics |
type_packs |
data_contracts, core_write |
lifecycle |
core_write, cel |
event_action_interop/0.1 |
none |
runtime/0.2 |
data_contracts, event_action_interop/0.1, cel |
watch |
collection_semantics |
An implementation claims a profile after passing every required behavior and
test for that profile. optional_features records additional work outside the
verified profile list.
Conformance Claim Documents
Conformance claims MUST validate against
schemas/v0.3/conformance-claim.schema.json. A claim names:
- the implementation and exact implementation version
- the exact mdbase specification version
- every supported profile ID, including dependency profiles
- the runtime profile version when a runtime profile is claimed
- the supported JSON Schema keywords and formats
- operational limits that affect portable behavior
- evidence commands and the time and environment in which they passed
A general compatibility label is supported by this validated profile list. Profile claims derive from verified behavior independently of product roles such as LSP, CLI, plugin, and server.
v0_2_read and v0_2_migrate are compatibility declarations for transition
behavior. They appear separately from v0.3 profiles.
The canonical claim schema enforces profile dependencies. Claim verification tools SHOULD reject evidence produced for a different implementation artifact or specification version and SHOULD report stale evidence.
Portable Interoperability Testbed
The spec-owned interoperability testbed in testbed/v0.1/ supplements the
profile suites with executable, implementation-neutral integration scenarios.
It has three rings:
| Ring | Boundary under test |
|---|---|
| Contract | record contracts, type implements declarations, projections, and multiple independent consumers |
| Interop | live event sources/consumers and action callers/providers connected through the interoperability profile |
| Runtime | durable admission, action attempts, crash recovery, leases, and fencing |
The testbed is deliberately role-based. An application MAY consume the same type or contract as any number of other applications. An event is delivered to every compatible authorized subscriber. An action still resolves to exactly one admitted provider; multiple eligible providers remain an error until a caller or policy selects one. The testbed does not turn an application name into a contract owner.
Every testbed scenario MUST validate against
schemas/testbed/v0.1/scenario.schema.json, use only fixtures from the validated
neutral catalog, and include its canonical expected transcript. An adapter MUST
run behind the describe/run process boundary defined by testbed protocol
0.1. A runner MUST start a fresh adapter process for each scenario, validate
the run request and returned transcript, and compare the complete ordered
transcript entries. Adapters MUST derive entries from behavior observed through
public or documented implementation boundaries; copying the scenario's
expected entries is not a conforming adapter.
Transcripts intentionally contain stable facts rather than private state. Generated IDs, database keys, wall-clock values, stack traces, scheduling jitter, and implementation-specific objects MUST be normalized or omitted unless a scenario explicitly makes them observable. Scenario order, actor, operation, outcome, and facts are all significant.
Portable testbed evidence validates against
schemas/testbed/v0.1/evidence.schema.json and binds every scenario result to a
canonical transcript digest. A conformance claim records it with evidence kind
testbed_transcript, the protocol version, scenario IDs, artifact path, and
evidence digest. evidence_digest MUST be the SHA-256 digest of the evidence
artifact's recursively key-sorted, whitespace-free JSON form, prefixed with
sha256:. Passing the testbed does not replace the complete profile suite: it
is integration evidence for the normative requirements named by each scenario.
Conformance never grants authority. A testbed adapter may be able to construct a compatible payload or declaration while the real host correctly rejects the operation as unauthorized.
Canonical Diagnostics
Every diagnostic contains:
severity: error
code: type_conflict
message: Human-readable context
path: tasks/example.md
field: status
type: task
schema_location: "https://mdbase.dev/schemas/v0.3/type-file.schema.json#/..."
details: {}
severity, code, and message are required. Paths use collection-relative
forward-slash form. field uses JSON Pointer or an explicitly identified
frontmatter selector. Implementations MAY add fields under x-*.
The v0.3 core codes include invalid_request, duplicate_batch_path,
unsupported_profile, unsupported_feature, invalid_frontmatter,
expression_compile_error, expression_evaluation_error,
projection_shadowed, link_not_found, type_conflict,
type_membership_changed, path_value_missing, schema_ref_forbidden,
schema_ref_unresolved, schema_ref_cycle, format_invalid,
lifecycle_expression_error, concurrent_modification, invalid_query,
context_not_found, context_required, context_type_mismatch,
view_not_found, invalid_view, unsupported_presentation,
invalid_data_contract, data_contract_not_found, data_contract_conflict,
data_contract_version_mismatch, data_contract_binding_invalid,
data_contract_field_invalid, data_contract_record_invalid,
invalid_type_pack, type_pack_conflict, type_pack_apply_failed,
invalid_timezone, and the JSON Schema schema_<keyword> codes from Chapter
06. Runtime profile 0.2 reuses interoperability codes such as
unknown_contract, contract_digest_conflict, no_provider,
ambiguous_provider, and capability_denied, and additionally defines
event_source_unavailable, idempotency_unavailable, cursor_expired,
stale_lease, invalid_run_transition, outcome_indeterminate, and
stale_timer_generation.
Core Read Requirements
Core Read implementations MUST:
- identify a collection by
mdbase.yaml - scan records using collection-relative forward-slash paths
- load and validate v0.3 type files
- skip the contracts folder and ignore type
implementssections whendata_contractsis not claimed - validate embedded JSON Schema against the v0.3 profile
- select explicit types and evaluate structured inferred match rules
- validate raw frontmatter independently against every matched schema
- reject a type that requires an unsupported optional profile with
unsupported_profile - report diagnostics in the canonical machine-readable shape
Data Contracts Requirements
Data Contracts implementations MUST:
- load contract files and register them by exact ID and version
- report
data_contract_conflictfor one ID and version with different digests - resolve every type
implementsversion requirement with the portable grammar - validate field mappings and bindings against the resolved contract
- expose deterministic contract and implementation digests
- project and validate contract views when a record is accessed through an implementation
- return every conforming implementation of a contract rather than selecting one
Type Packs Requirements
Type Packs implementations MUST:
- validate pack manifests, safe resource paths, and resource digests
- read and write
mdbase.lock.yamldeterministically - produce a read-only assessment with an exact resource diff and
assessment_digest - honor managed and seed resource ownership, explicit adoption, and target overrides
- recheck the assessment inside the mutation boundary and fail with
concurrent_modificationwhen it changed - commit resources, reviewed type setups, and the lock as one recoverable transaction
- treat reapplying a current pack as a no-op
Collection Semantics Requirements
Collection Semantics implementations MUST:
- apply
collection.read_defaultsto effective reads - preserve missing, null, raw, and effective distinctions
- validate every
collection.uniquerule in its declared scope - validate portable
collection.pathpolicies for write-capable tools - expose display metadata as advisory values
- compose compatible behavior from multiple matched types
- report
type_conflictfor incompatible matched behavior
CEL Requirements
CEL implementations MUST:
- compile and evaluate portable CEL source used by a claimed feature context with standard CEL semantics
- support the CEL optional types extension
- supply the system bindings defined for that context in Chapter 10
- preserve the mdbase missing, null, raw, and effective-value contract
- type
format: date-timevalues as timestamps and providenow(),today(), the date conversions, and the date-string methods with declared timezone behavior - provide the
lower()andupper()text helpers with Unicode default case mappings - enforce and report expression, evaluation, and traversal limits
- distinguish compilation diagnostics from evaluation diagnostics
The CEL profile supplies the shared language capability. Each embedding profile defines its context and operational outcome.
CEL Match Requirements
CEL Match implementations MUST:
- compile
match.exprwhile loading its type definition - evaluate it against raw candidate frontmatter and file metadata
- combine it with other members of
matchusing AND - match only a boolean true result
- report per-record evaluation errors and treat that candidate as a non-match
Query Requirements
Query implementations MUST:
- validate portable query objects against the canonical query schema
- resolve and snapshot an optional same-collection invocation context
- expose the complete
thiscontext contract, binding it to null when absent - evaluate named query projections in dependency order before filtering
- reject cyclic projection dependencies and duplicate result names with
invalid_query - evaluate
wherefilters against the effective query context - evaluate requested CEL projections
- support OR-based type filtering
- support deterministic ordering and pagination
- support deterministic grouping and built-in and custom summaries
- return total-count and has-more metadata
- return context, grouping, and summary metadata when requested
- expose raw and effective frontmatter when requested
- report per-record evaluation errors and continue evaluating remaining records
Collection Projection Optional Feature
An implementation advertises collection_projections through
optional_features when it:
- evaluates
collection.projectionsin the query context after read defaults - exposes projection values in effective reads,
effective_frontmatter, and queries under their declared field names - keeps persisted fields and reports
projection_shadowedfor a projection with the same name - evaluates projections that reference each other in dependency order and rejects cycles when loading the type
- leaves a failed projection absent and reports an expression diagnostic
An implementation that does not advertise it reports unsupported_feature as
described in Chapter 07.
View Record Optional Feature
An implementation advertises view_records through optional_features when it
claims data_contracts and:
- identifies view records only through types implementing the
mdbase.viewrecord contract, never by type name, path, or raw frontmatter value - reports
invalid_viewfor a record that matches several implementing types - validates the
mdbase.viewcontract view against the canonical view schema - lists view records with stable source and named-view descriptors
- resolves a stable view-record ID or path plus a stable named-view ID
- rejects duplicate named-view IDs with
invalid_view - derives the executable query using the inheritance and merge rules in Chapter 11
- exposes selected result properties in display order with their metadata
- applies
context.this.on_missingand context type constraints before query execution - reports the selected view and resolved context in query result metadata
- applies presentation metadata as advisory input while preserving canonical headless results
- keeps alternate dialect and renderer-specific data under
x-*extensions
A tool MAY advertise supported presentation identifiers separately in
optional_features.
An implementation advertises obsidian_bases_views through
optional_features when it meets the requirements of the
Obsidian Bases adapter.
An implementation advertises writable_view_sources through
optional_features when it:
- marks only writable source formats with
source.writable: true - reads complete source documents with stable opaque revisions
- validates complete candidate documents before create or update
- creates sources without replacing an existing path
- applies
if_revisionto update and delete - writes source replacements atomically
- preserves source-format extension data supplied by the caller
- makes successful mutations visible to subsequent list and execute operations
Links Requirements
Links implementations MUST:
- parse wikilinks, Markdown links, and bare path link values
- resolve collection-relative and file-relative paths safely
- enforce
collection.links.target_typeandvalidate_exists - resolve simple wikilinks by filename, using ID resolution only when
settings.id_fieldis configured - expose
file.links,file.embeds,file.tags, andfile.backlinks - provide the CEL link helpers from Chapter 10
- bound
asFile()traversal
Core Write Requirements
Core Write implementations MUST:
- validate a complete draft before writing
- preserve unrelated Markdown body content where possible
- reject paths that escape the collection root
- enforce
if_revisionand report common concurrency conflicts - persist null patch values as explicit null and remove
unsetkeys - apply the validation level to record validation issues
- execute batches atomically by default, support
allow_partialanddry_run, reject duplicate batch paths, and report per-operation results - return the canonical operation envelope and final record revision
- update derived state before reporting a successful mutation
Lifecycle Requirements
Lifecycle implementations MUST:
- support
on_createandon_updatewith single-action and action-list forms - support
now,today,uuid,ulid,slugify,copy, andliteralvalue providers - apply actions in order with the assignment semantics of Chapter 09
- evaluate lifecycle guards in the lifecycle CEL context
- run lifecycle before final record validation
- evaluate membership once after lifecycle and report
type_membership_changed - report conflicts between matched lifecycle policies
Event/Action Interoperability Requirements
The event_action_interop/0.1 profile, its independently claimable roles, and
its required scenarios are defined by the
event/action interoperability profile. Its claims validate
against schemas/interop/v0.1/conformance-claim.schema.json.
Durable Runtime Requirements
The runtime/0.2 profile's requirements are defined by the
durable runtime companion profile.
Watch Requirements
A watch notification MUST identify a change kind, unique event ID, observation time, and affected collection-relative path or contract identity. A record notification has this portable shape:
kind: record_modified
id: watch_01J...
observed_at: "2026-07-19T10:30:00Z"
path: tasks/example.md
changed_fields: [status]
frontmatter:
type: task
title: Example
status: done
Record notifications use these change kinds:
record_createdrecord_modifiedrecord_deletedrecord_renamed
Implementations also report config_changed and type_changed notifications.
An implementation that also claims data_contracts reports contract_changed
with the contract file path after the contract registry and type
implementations have been re-resolved. Implementations MAY also report
schema_changed, view_changed, and lock_changed, each with a path, for
referenced local schema files, saved-view sources, and mdbase.lock.yaml. A rename may be
reported as record_deleted followed by record_created when the host cannot
establish file identity.
record_created and record_modified include current effective frontmatter.
record_renamed includes path, previous_path, and current effective
frontmatter. record_modified.changed_fields contains the top-level raw
frontmatter keys whose persisted values changed; it is empty for a body-only
change. A deleted notification may include the last observed frontmatter.
Watch implementations MUST:
- observe the same record extensions, exclusions, type folder, and nested collection boundaries as Core Read
- report logical changes caused by external file updates and core write operations
- publish record, config, type, and contract notifications after derived read and query state reflects the change
- include both paths in a detected rename
- preserve notification order for the same record or configuration subject
- coalesce duplicate host notifications for one logical change and report the final observed state
- isolate listener failures so later notifications continue to be delivered
mdbase Event and Action Interoperability Profile 0.1
1. Purpose
This optional profile lets independently developed applications exchange typed events and invoke typed actions without adopting one workflow engine or durable runtime.
The profile defines:
- event and action contract kinds built on
mdbase.contract; - a CloudEvents 1.0 event envelope;
- action request, admitted invocation, cancellation, and outcome envelopes;
- event-source and action-provider declarations;
- discovery, version resolution, validation, authorization boundaries, and duplicate semantics;
- role-specific conformance;
- transport capabilities and the baseline bridge behavior.
The profile does not define workflows, schedules, retries, leases, persistence, or a universal authorization policy.
The profile identifier is event_action_interop/0.1. Envelope
profile_version values use 0.1; event envelopes carry the CloudEvents
extension mdbaseprofile: "0.1".
The normative schemas are in schemas/interop/v0.1/.
2. Layering
The layers have separate responsibilities:
| Layer | Responsibility |
|---|---|
| Contract | identity, exact semantic version, canonical digest, schema, and meaning |
| Interoperability profile | message envelopes, roles, resolution, and boundary validation |
| Transport binding | physical delivery and declared delivery capabilities |
| Application runtime | application-specific behavior |
| Durable runtime | optional workflow state, retries, timers, leases, and recovery |
A contract demonstrates data compatibility. It does not grant authority. Registration, application installation, and conformance claims do not grant authority either.
3. Conformance Roles
Implementations claim only roles they implement:
| Role | Required behavior |
|---|---|
event_source |
register exact event artifacts and publish valid events |
event_consumer |
subscribe by compatible requirement and validate delivered events |
action_caller |
create requests and handle portable outcomes |
action_provider |
register exact action artifacts and execute selected invocations |
bridge |
resolve, authorize, validate, route, and expose execution evidence |
Transport bindings may define additional roles. A role claim never implies another role.
4. First-Class Contract Artifacts
All passive records, events, and actions use the same control-file envelope:
kind: mdbase.contract
contract_type: event
id: tasknotes.task.completed
version: 1.0.0
name: Task completed
description: A TaskNotes task transitioned from incomplete to complete.
data_schema:
dialect: json-schema-2020-12
value:
type: object
required: [task_id, completed_at]
additionalProperties: false
properties:
task_id: { type: string, minLength: 1 }
completed_at: { type: string, format: date-time }
contract_type is one of:
record, withrecord_schemaand optionalbinding_schema;event, withdata_schemaand optionalsource_schema;action, withinput_schema, optionaloutput_schema, optionalerror_schema, optionalprovider_schema, and optionalbehavior.
Every schema uses the mdbase JSON Schema 2020-12 profile. Event data, action
input, successful action output, declared action error details, and
implementation binding values validate against the relevant exact artifact.
Record implements entries remain in type files. Event sources and action
providers make runtime implementation declarations because their
implementations are executable and instance-specific.
4.1 Digest
The contract digest is SHA-256 over RFC 8785 canonical JSON containing:
kind,contract_type,id, andversion;- the fully resolved subject-specific schemas;
- action
behaviorwhen present.
Names, descriptions, Markdown bodies, x-* metadata, wrapper dialect keys, and
local schema reference paths do not affect the digest.
The digest input uses the schema field names defined for the selected
contract_type and omits absent optional fields. A bridge MUST fail with
contract_digest_conflict when one (id, version) pair is presented with
different contract bytes.
5. Requirements and Exact References
An author-facing contract requirement contains:
{ "id": "canvas.card.create", "version": "^1.0.0" }
version is a version requirement using the portable grammar in
Data Contracts. digest may
additionally pin one artifact.
Execution evidence contains an exact reference:
{
"id": "canvas.card.create",
"version": "1.2.0",
"digest": "sha256:..."
}
A bridge resolves a requirement to one exact artifact before event delivery or action admission. It MUST NOT change a pinned artifact or selected provider after admission.
6. Implementation Identity
Portable implementation identity has four distinct layers:
application: canvas-bases
implementation: canvas-bases.obsidian
version: 1.4.0
instance_id: vault-01
applicationnames the product or package;implementationnames a concrete implementation line;versionnames its build version;instance_id, when present, distinguishes a live installation.
Declarations and outcomes additionally expose a declaration digest. A transport binding owns the authenticated principal and authorization evidence. The portable identity is not itself authentication.
7. Events
7.1 Event Contracts and Source Declarations
An event contract defines the schema and meaning of data. It does not name
one owner. Any number of authorized sources may implement the same exact event
contract.
An event-source declaration identifies one implementation, its author-declared
compatible requirement, its exact resolved artifact, optional binding data,
and ordering capabilities. Binding data validates against source_schema when
the contract supplies one.
7.2 CloudEvents Envelope
Events use CloudEvents 1.0 structured JSON. The standard attributes retain their CloudEvents meanings:
{
"specversion": "1.0",
"id": "evt_01...",
"source": "urn:mdbase:app:tasknotes:tasknotes.obsidian",
"type": "tasknotes.task.completed",
"time": "2026-07-28T11:30:00.000Z",
"subject": "urn:mdbase:record:vault-01:task-123",
"datacontenttype": "application/json",
"dataschema": "urn:mdbase:contract:tasknotes.task.completed:1.0.0:sha256:...",
"mdbaseprofile": "0.1",
"mdbasecontractversion": "1.0.0",
"mdbasecontractdigest": "sha256:...",
"mdbaseapplication": "tasknotes",
"mdbaseimplementation": "tasknotes.obsidian",
"mdbaseimplementationversion": "5.0.0",
"correlationid": "flow_01...",
"causationid": "req_01...",
"data": {
"task_id": "task-123",
"completed_at": "2026-07-28T11:30:00.000Z"
}
}
source and id together identify a logical event across redelivery.
time is when the domain occurrence happened. Transport receipt time remains
transport metadata. subject is an optional stable URI reference.
type, mdbasecontractversion, and mdbasecontractdigest identify the exact
event artifact. dataschema is its canonical URN. Bridges MUST reject an
inconsistent combination.
correlationid groups related activity. causationid identifies the event,
request, or invocation that directly caused this event.
Consumers subscribe by contract ID and compatible version range. Every authorized matching consumer receives the same logical event. Adding a consumer does not alter another consumer's subscription.
Events have no global ordering guarantee. Bindings declare none, source,
or subject ordering scopes.
8. Actions
Events multicast. Actions select exactly one provider.
8.1 Action Contracts and Provider Declarations
An action contract defines the requested effect and its input, successful output, declared portable errors, provider binding, and behavior. It does not name or authorize a provider.
An action-provider declaration identifies:
- the provider implementation;
- a stable handler ID;
- an author-declared compatible contract requirement;
- the exact resolved artifact;
- optional provider binding;
- cancellation and request-deduplication support;
- optional operational limits.
Provider binding validates against provider_schema.
8.2 Request
A request is logical caller intent:
kind: mdbase.action.request
profile_version: "0.1"
request_id: req_01...
contract:
id: canvas.card.create
version: ^1.0.0
caller:
application: tasknotes-workflows
implementation: tasknotes-workflows.obsidian
version: 1.0.0
created_at: 2026-07-28T11:30:01Z
correlation_id: flow_01...
causation_id: evt_01...
idempotency_key: flow_01:canvas-card
input:
canvas: Projects/Report.canvas
title: Prepare report
An optional requested-provider selector may pin an application,
implementation, or instance. An optional deadline is an RFC 3339 timestamp.
An authorization-context reference is bridge supplied and opaque.
The request is not evidence that a provider was selected or that an effect occurred.
8.3 Admitted Invocation
Before dispatch, a bridge records an admitted invocation containing:
request_id, preserving logical intent;- a new
invocation_id; - an exact contract version and digest;
- the selected provider identity and declaration digest;
- the selected handler ID;
- an
attempt_id; - admitted input and trace context;
- admission time and applicable deadline.
The bridge validates the action input and authorization before admission. Provider selection and the exact contract MUST NOT change after admission.
The base bridge creates one attempt. A durable binding or runtime may create
later attempts but preserves the request and invocation identities and gives
each concrete handler attempt a distinct attempt_id.
8.4 Outcome
Every terminal result has a distinct outcome_id and identifies its request,
invocation, attempt, exact contract, provider, and completion time.
Statuses are:
succeeded;rejected;failed;cancelled;outcome_indeterminate.
Successful output validates against output_schema when present. Every
unsuccessful outcome carries a portable error. A bridge MUST turn an invalid
provider output into failed with invalid_action_output; it MUST NOT deliver
invalid output as success.
Transport acceptance is not an action outcome.
8.5 Selection
A bridge:
- finds exact action artifacts satisfying the request;
- applies an optional requested-provider selector;
- removes unauthorized or unavailable providers;
- selects the one remaining provider; or
- returns
no_provider,requested_provider_unavailable, orambiguous_provider.
Providers are never unioned or broadcast. A bridge MUST NOT silently select the first registered provider. Adding a provider cannot redirect an admitted request or an explicitly pinned caller.
9. Validation and Authorization
At every boundary, a bridge validates:
- the portable envelope;
- the exact contract identity and digest;
- source or caller identity attributed by the active binding;
- implementation declarations and bindings;
- event data, action input, output, or declared error details;
- active authorization.
Authorization is separate from compatibility. A bridge may authorize by principal, role, contract, exact implementation, collection/subject scope, capability, approval, or resource limit. Credentials and sensitive grants stay in transport-private state; portable envelopes carry only an opaque reference when necessary.
Unauthorized publish, subscribe, provider registration, and invocation fail closed.
10. Duplicate, Idempotency, and Indeterminate Outcomes
The profile does not claim universal exactly-once behavior.
- Consumers deduplicate events by
(source, id)when duplicate processing is harmful. - Requests preserve
request_idacross transport retry. - An idempotency key is meaningful only when the action contract permits it and the selected provider declares request deduplication.
- A duplicate request to such a provider returns its recorded terminal outcome during the declared retention window.
- Without declared deduplication, the bridge rejects a duplicate request rather than inventing exactly-once behavior.
- If the caller cannot know whether an external effect occurred, the terminal
status is
outcome_indeterminate.
Retry policy belongs to the caller, binding, or durable runtime.
11. Cancellation and Deadlines
A caller may request cancellation by request_id. Cancellation is
cooperative. The bridge verifies caller ownership and selected-provider
support, then signals the current attempt.
Possible races are represented honestly:
- cancellation before admission returns
cancelled; - a cooperatively cancelled attempt returns
cancelled; - a completed action keeps its completed outcome;
- an effect whose completion cannot be determined returns
outcome_indeterminate; - a provider without cancellation support returns
cancellation_unsupported.
A bridge supporting deadlines rejects admission after the deadline and signals
a cooperative provider when the deadline expires during an attempt. A binding
that cannot enforce a required deadline fails
unsupported_transport_capability.
12. Transport Capabilities
A binding describes:
- delivery:
ephemeral,at_least_once,durable_cursor,offline_queue; - ordering:
none,source,subject; - cancellation and deadline support;
- provider discovery;
- maximum payload size;
- outcome retention;
- request deduplication;
- cross-process identity.
An application that requires an unavailable capability fails clearly before exchange.
The baseline Obsidian in-process binding is ephemeral, process-local, and cooperatively attributed. Obsidian does not isolate plugins as security principals, so this binding cannot defend against a malicious plugin running in the same process. It still prevents accidental cross-application registration through opaque client handles and validates host-attributed identities at each boundary.
13. Baseline Bridge API
The behavior-based API is:
connect one implementation identity
describe contracts and implementations
register event source
publish event
subscribe to events
register action provider
invoke action
receive action outcome
cancel action
dispose client registration
Language SDKs may use idiomatic names. A client handle scopes every subscription and provider registration to one implementation and removes them on disposal.
14. Standard Errors
Portable error codes are defined by
schemas/interop/v0.1/profile.schema.json#/$defs/portableError.
Transport-private diagnostics may be nested in safe details, but they do not
replace the portable category. Error messages MUST NOT expose credentials,
private authorization policy, or provider-internal stack traces.
15. Relationship to Runtime and Hosted Bindings
The mdbase durable runtime profile consumes these contract artifacts and envelopes. It does not define a second event or action system. Workflow triggers subscribe to event requirements; workflow steps create action requests; durable dispatch records the admitted invocation and outcome.
A hosted service may implement a durable authorized binding using the same envelopes. Its control-plane wake-up signals may remain opaque: private event data can stay at the collection authority and be retrieved only by an authorized application.
16. Conformance
Required scenarios are enumerated in
tests/v0.3/event-action-interop/event-action-interop.yaml. Claims validate
against schemas/interop/v0.1/conformance-claim.schema.json.
The profile is behavior based. Implementations may use any package structure or programming language.
mdbase Durable Runtime Companion Profile 0.2
Purpose
The durable runtime profile turns passive, portable definitions into recoverable automation. It is deliberately a companion to two simpler specifications:
- the core mdbase contract system defines contract identity, semantic versions, JSON Schemas, digests, record implementations, resolution, and packs;
- the event/action interoperability profile defines CloudEvents, action requests and outcomes, event-source and action-provider declarations, provider selection, and baseline exchange behavior;
- this profile adds durable admission, authorization, execution state, retries, timers, cancellation, and recovery.
The separation is practical. An application can consume portable records or exchange events and actions without implementing a workflow engine.
This document defines runtime companion profile 0.2. The profile is versioned
independently from mdbase collection specification 0.3.0.
The Short Version
contract artifact what an event, action, or record means
type implements how a Markdown record stores a record contract
source/provider declares how live code implements an event/action contract
runtime policy whether that exact code may act here
admitted plan exact contract digests and implementations for one run
durable runtime safely executes and recovers that admitted plan
Installing a contract, type, or pack never runs code and grants no authority. Markdown provider-registration records are audit evidence only. A host must verify and admit a live interoperability declaration before code is eligible.
No Second Contract System
Runtime profile 0.2 has no runtime-specific contract record, registry, provider-owned contract definition, event envelope, or contract mode.
- Event and action interfaces are ordinary
mdbase.contractartifacts. - Runtime state is stored in ordinary record types that implement standard record contracts.
- Live event sources and action providers use interoperability declarations.
- Runtime indexes are derived caches of those verified registries.
- Bundled artifacts, installed artifacts, and exported copies have identical identity and conflict semantics.
Private x-* metadata, filenames, and folders have no discovery,
authorization, or activation meaning.
The superseded type: action, type: event, type: provider,
contract_version, schemas.payload, runtime.contract_mode, and
provider-supplied contract arrays are not runtime profile 0.2.
Standard Runtime Pack
The official mdbase.runtime.standard pack is at
`standard-packs/mdbase-runtime/0.2.0`.
It installs atomically and contains:
| Record contract | Canonical record type | Purpose |
|---|---|---|
mdbase.runtime.workflow |
runtime_workflow |
portable workflow definition |
mdbase.runtime.policy |
runtime_policy |
local authority and provider selection |
mdbase.runtime.provider-registration |
runtime_provider_registration |
optional declaration audit snapshot |
mdbase.runtime.capability-grant |
runtime_capability_grant |
optional materialized grant |
mdbase.runtime.run |
runtime_run |
durable run and admitted plan |
mdbase.runtime.action-attempt |
runtime_action_attempt |
invocation intent and outcome evidence |
mdbase.runtime.checkpoint |
runtime_checkpoint |
durable continuation |
mdbase.runtime.timer |
runtime_timer |
generation-safe one-shot timer |
mdbase.runtime.diagnostic |
runtime_diagnostic |
machine-readable runtime issue |
mdbase.runtime.dead-letter |
runtime_dead_letter |
retained unprocessable evidence |
It also contains inspectable event/action artifacts for the four canonical
record-change events (mdbase.record.created, mdbase.record.modified,
mdbase.record.deleted, and mdbase.record.renamed),
mdbase.runtime.timer.fired, and mdbase.runtime.run.cancel. The event data
schemas make a contract definition portable across every source and consuming
application; a source declaration says only who publishes an exact artifact.
These artifacts are a standard library, not privileged built-ins. A host may bundle the pack for offline use without copying it into each collection. Exporting a bundled artifact is an inspection feature; it does not create a new definition or alter precedence.
Runtime Records Are Ordinary Implementations
The canonical workflow type illustrates the model:
kind: mdbase.type
name: runtime_workflow
version: 1
match:
where:
type: runtime_workflow
schema:
dialect: json-schema-2020-12
ref: ../schemas/mdbase.runtime.workflow/1.0.0.schema.json
implements:
- contract: mdbase.runtime.workflow
version: 1.0.0
fields:
type: type
id: id
version: version
name: name
enabled: enabled
triggers: triggers
steps: steps
The core collection engine discovers and validates this exactly like any other
record contract. A durable runtime asks the core engine for the verified
descriptor and projected view. It does not scan for workflows/, special-case
the filename, or reconstruct a separate registry.
Internal database rows and indexes do not need Markdown record contracts. Materialize runtime state when it is intended to be portable, inspectable, exportable, or shared through mdbase. Private storage tables remain implementation details but must preserve the same protocol evidence.
Event Sources And Action Providers
Live implementations use the interoperability profile unchanged.
An event source declaration binds a verified implementation identity to one or more exact event artifacts. An action provider declaration binds handler IDs to exact action artifacts. Declarations include their own canonical digest.
A provider does not return an action or event definition from executable code. Registration supplies the already-resolved artifact to the interoperability bridge, which validates the implementation binding and publishes a declaration.
Several sources may publish one event contract. Several providers may implement one action contract. Events remain multicast. Actions select exactly one provider:
- an explicit provider selector in the workflow wins;
- otherwise a single matching runtime-policy selection may apply;
- otherwise one eligible provider is unambiguous;
- zero providers fails with
no_provider; - multiple providers fail with
ambiguous_provider.
Executable providers are never unioned.
Authorization Is Separate
Conformance answers “can this implementation speak this interface?” Authorization answers “may it act here, for this caller, on this resource?”
A host checks, independently:
- selected and enabled runtime policy;
- caller and executor identity;
- named capabilities;
- collection and record scope;
- action contract and exact provider implementation;
- operational limits;
- required user approval;
- transport and host authority.
Deny takes precedence. The standard pack ships no permissive policy. A contract, type, workflow, provider declaration, or audit record is passive until a host admits it under current authority.
Workflow Requirements
Authors use a contract ID and SemVer requirement:
type: runtime_workflow
id: canvas.zone.set-status
version: 1.0.0
name: Set status from canvas zone
enabled: true
triggers:
- id: drop
event:
id: canvas.drop
version: ^1.0.0
steps:
- id: patch-status
action:
id: mdbase.record.patch
version: ^2.0.0
requires:
capabilities: [mdbase.record.write]
input:
path:
$expr: event.data.file.path
patch:
status:
$expr: event.data.zone.id
The readable workflow does not contain a digest for every normal dependency. An author may add a digest to a requirement when deliberately pinning it.
Workflows normally depend on behavior, not an application name. Add a provider selector only when the workflow intentionally needs a provider-specific implementation:
provider:
application: tasknotes
implementation: tasknotes-v5
Preflight And Admission
Preflight is derived and repeatable. It:
- validates the workflow through
mdbase.runtime.workflow; - checks unique trigger and step IDs and compiles expressions;
- resolves each contract requirement through the core contract registry;
- uses verified interoperability declarations to find implementations;
- applies explicit or policy provider selection;
- evaluates capability and operational policy;
- reports every missing, conflicting, ambiguous, or denied dependency.
Preflight does not make an execution durable.
Admission occurs for one validated event delivery. In one durable transaction the authority:
- journals and deduplicates the CloudEvent;
- identifies the exact event-source declaration;
- evaluates trigger and admission policy;
- resolves every action provider;
- pins the workflow revision;
- pins exact event/action contract ID, version, and digest;
- pins exact source/provider identities and declaration digests;
- records handler IDs, idempotency decisions, and deadlines;
- reserves the run idempotency key and concurrency position;
- writes the run and immutable admitted plan.
Registry changes after this transaction do not alter the plan. Changing an admitted dependency requires explicit migration or re-admission.
Shared Envelopes
Runtime event intake is a structured CloudEvents 1.0 envelope from the interoperability profile. Runtime action dispatch persists and exchanges the standard:
mdbase.action.request;mdbase.action.invocation;mdbase.action.outcome;mdbase.action.cancel.
The runtime does not wrap these in another envelope or rename data to
payload. Runtime-specific run, attempt, cursor, and lease records refer to
the shared envelope evidence.
Runtime Configuration
The core mdbase.yaml schema does not define runtime.contract_mode.
Runtime enablement, storage, worker identity, policy selection, and transport
binding are host configuration. A host may store portable policy records in the
collection, but loading the collection remains safe and does not activate them.
Conformance Boundary
A conformant core engine supports contract artifacts, type implementations, record projection, conflict detection, and packs. It need not execute workflows.
A conformant interoperability implementation supports its claimed event/action roles. It need not persist runs.
A conformant durable runtime supports admission and the execution and recovery rules in this document. It uses core and interoperability evidence rather than publishing a second contract model.
Durable Workflow Execution
The sections above define the companion-profile boundary, standard records, policy, and admission. This part defines the durable protocol after a workflow and event have been admitted.
JSON Schema proves that persisted evidence has the right shape. The transition, transaction, lease, replay, and recovery rules below are behavioral requirements and cannot be replaced by schema validation.
Execution Inputs
A worker executes only an immutable admitted plan. The plan contains:
- workflow content revision;
- exact triggering event contract and source declaration;
- exact action contracts;
- one exact provider identity, declaration digest, and handler ID per step;
- selected runtime policy revision;
- evaluated limits and authority evidence;
- run idempotency and concurrency decisions.
The worker may confirm that a live declaration still matches the pin. It must not silently resolve a replacement.
Deterministic Evaluation
Workflow variables, conditions, templates, and iteration use the expression
rules and workflow contexts in the
CEL profile. Runtime activation exposes the
shared CloudEvent as event; portable payload values therefore appear below
event.data.
- variables are evaluated once in dependency order;
- cycles fail preflight;
- trigger and step conditions must evaluate to boolean true to continue;
- object values are literals unless they have exactly one
$exprkey; - steps execute in document order;
for_eachvisits list items in list order;- evaluated action input is validated by the interoperability bridge before provider invocation;
- action output or declared error data is validated before it becomes durable step state.
Every execution limit is the stricter of the workflow and current policy.
Durable Event Admission
Event delivery may be repeated. The data authority therefore commits the following atomically:
- validated CloudEvent and exact source evidence;
- duplicate/tombstone state;
- monotonically increasing delivery cursor;
- every run admitted from the event;
- run idempotency reservations;
- concurrency queue changes;
- immutable admitted plans.
A crash cannot expose the event without its derived runs or a run without its event. Retrying the same event ID and contract/source evidence returns the original cursor and does not create another logical run. Reuse of an event ID with different canonical content fails closed.
Pruning may remove event bodies after the documented retention period but
keeps sufficient tombstone evidence through the replay horizon. Reading from a
cursor older than retained history reports cursor_expired and the earliest
available cursor; it never silently skips the gap.
Run State Machine
Portable run states and transitions are:
| From | To | Cause |
|---|---|---|
| admitted | queued |
concurrency delays execution |
admitted or queued |
running |
worker obtains current lease |
running |
waiting |
durable checkpoint suspends execution |
waiting |
queued |
checkpoint becomes ready |
running |
succeeded |
all required steps complete |
running |
failed |
deterministic failure or deadline |
queued, running, waiting |
cancelled |
cancellation completes without ambiguous effects |
running |
indeterminate |
non-replayable action outcome is unknown |
succeeded, failed, cancelled, and indeterminate are terminal. A host
rejects every transition from a terminal state.
Leases
A worker claims a run with a bounded lease containing owner, opaque token, and expiry. Every state-changing write compares the current lease token.
- workers renew before expiry;
- an expired lease may be recovered by another worker;
- a stale worker cannot commit a step, checkpoint, or terminal state;
- handler execution is never treated as proof that its outcome was committed.
Lease duration and renewal strategy are host configuration. The observable stale-write rejection is portable.
Action Attempts
Before invoking a provider, the runtime durably writes an action-attempt intent containing the shared request ID, invocation ID, attempt ID, exact contract, provider identity, provider-declaration digest, handler ID, idempotency key, deadline, and input.
It then sends the ordinary interoperability request. The bridge produces the ordinary invocation and outcome evidence. The runtime stores those envelopes without redefining them.
Attempt states are:
admitted → dispatching → succeeded
↘ rejected
↘ failed
↘ cancelled
↘ indeterminate
Only an outcome with matching request, invocation, attempt, contract, and provider evidence completes the attempt.
Idempotency And Crash Recovery
The workflow run has an idempotency key. If the author does not supply one, the host derives:
workflow.id + ":" + event.id + ":" + trigger.id
The key is reserved during admission, before any dispatch.
Action replay follows the action artifact and provider declaration:
- for request-idempotent actions, recovery reuses the same request and invocation identity as required by the binding and obtains the retained outcome or safely retries;
- for non-idempotent actions, a crash after dispatch but before durable outcome
creates
indeterminate; the runtime does not guess or replay; - conflicting reuse of a request or idempotency key fails closed;
- an action marked as requiring idempotency is ineligible when the provider or transport cannot supply it.
Exactly-once external side effects are not promised. The profile provides durable intent, duplicate suppression, explicit replay rules, and honest indeterminate outcomes.
Concurrency
The optional workflow concurrency policy applies to an evaluated group:
| Policy | New run while group is active |
|---|---|
skip |
record the admission decision and do not execute |
queue |
preserve event-cursor order |
replace |
request cancellation, then wait for a terminal state |
allow |
run concurrently |
When no group expression is supplied, the workflow ID is the group. Completed
effects are never rolled back by replace. A replacement waits when the active
action cannot be cancelled safely.
Cancellation And Deadlines
Cancellation is cooperative and uses the interoperability cancellation envelope. The runtime records the request before delivery.
- not-yet-started attempts are not dispatched;
- cooperative providers receive cancellation through the bridge;
- a confirmed cancelled outcome permits
cancelled; - a completed action outcome wins a race with cancellation;
- loss of a non-idempotent outcome becomes
indeterminate; - terminal runs ignore repeated cancellation requests idempotently.
A run deadline prevents all new dispatches after expiry. A deterministic
deadline failure becomes failed; an already-dispatched action whose outcome
cannot be known follows the indeterminate rule.
Checkpoints
A checkpoint stores enough state to resume deterministically:
- run ID and current generation;
- next step/item position;
- immutable variables and completed step outputs;
- pending request/invocation evidence;
- wait condition;
- current workflow/admitted-plan revision;
- lease-safe updated time.
Checkpoint generation only increases. A stale generation or stale lease write is rejected. Completion is recorded before the worker advances.
Timers
A durable timer is generation-safe and one-shot.
- scheduling writes generation
n, fire time, and an event contract requirement plus event data; - rescheduling writes generation
n + 1and makes older generations stale; - a worker atomically claims the current due generation;
- firing publishes
mdbase.runtime.timer.firedor the requested event through an interoperability event source; - the resulting CloudEvent is admitted through the ordinary event journal;
- the timer becomes
firedonly with matching event evidence.
After restart, an overdue current generation fires once under
missed_run_policy: fire_once. Polling or racing workers cannot create two
logical events.
Policy Rechecks
Admission pins compatibility and selection. Authority is checked again immediately before each dispatch because grants, scopes, or approval may have changed.
A new denial prevents dispatch and fails the attempt with
capability_denied. A policy change does not substitute another provider or
contract in an admitted plan. That requires re-admission.
Diagnostics And Dead Letters
Protocol failures use stable codes and durable evidence. Important examples include:
unknown_contract;contract_digest_conflict;event_source_unavailable;no_provider;ambiguous_provider;capability_denied;idempotency_unavailable;invalid_run_transition;stale_lease;cursor_expired;outcome_indeterminate.
Unprocessable event/request/outcome evidence may be retained as
mdbase.runtime.dead-letter. A dead-letter record is passive. Requeue is an
explicit authorized operation that creates new admission evidence.
Minimum Durable Conformance
A runtime claiming profile 0.2 must demonstrate:
- ordinary record-contract validation of runtime records;
- exact contract/source/provider pinning through interoperability declarations;
- authorization separate from conformance;
- atomic event/run admission and duplicate suppression;
- legal run and attempt transitions;
- stale-lease rejection and crash recovery;
- idempotent versus non-idempotent recovery;
- generation-safe timers;
- cursor expiry behavior;
- cancellation and deadline races;
- machine-readable diagnostics.
Conformance Requirements
Durable runtime profile runtime/0.2 implementations MUST:
- validate standard runtime records as ordinary record-contract
implementations from the
mdbase.runtime.standardpack - resolve workflow event and action requirements through the core contract registry and verified interoperability declarations
- select exactly one provider per action step, failing with
no_providerorambiguous_providerrather than choosing arbitrarily - reject duplicate trigger and step IDs within a workflow
- evaluate workflow variables in dependency order and fail preflight on cycles
- evaluate trigger, step, and run-policy expressions in the contexts defined in the CEL profile
- evaluate step inputs and
for_eachiteration in deterministic order - fail preflight for denied required capabilities and recheck authorization before each dispatch
- atomically journal, deduplicate, and admit delivered events together with their runs and immutable admitted plans
- apply trigger
debounceandminimum_intervaladmission - pin exact workflow revision, contract digests, source and provider declarations, and policy revision in the admitted plan
- reserve run idempotency keys during admission
- apply concurrency policy, run limits, and
on_error - enforce the legal run and action-attempt state transitions above
- claim work with bounded leases and reject stale lease writes
- persist action-attempt intent before provider invocation
- recover request-idempotent attempts and mark ambiguous non-idempotent
attempts
indeterminate - validate action inputs, outputs, and emitted events
- provide generation-safe one-shot timers and missed-run behavior
- report
cursor_expiredrather than skipping retained-history gaps - resolve cancellation and deadline races as defined above
- report runtime failures through the diagnostics listed above
A claim lists its tested action handlers under optional_features or
associated evidence. The profile ID is runtime/0.2; its dependencies are
listed in the conformance chapter.
Obsidian Bases Adapter
Purpose
This companion document defines how an mdbase collection provider exposes
Obsidian .base files as saved-view sources. It builds on the saved-view model
in Querying and the saved-view operations in
Operations. Implementations that do not advertise the
obsidian_bases_views optional feature ignore .base files and the
x-obsidian configuration section.
Bases as records
A collection that lists base in settings.record_extensions stores each
.base file as a YAML document record (Chapter 03). A Base is a saved-view
source when its matched type implements the obsidian.base record contract
(_contracts/obsidian.base/1.0.0.md), exactly as a canonical view is one when
its type implements mdbase.view:
kind: mdbase.type
name: obsidian_base
version: 1
match: { path_glob: 'TaskNotes/Views/**/*.base' }
implements:
- contract: obsidian.base
version: 1.0.0
fields: { filters: filters, formulas: formulas, properties: properties, views: views }
list_views discovers such records alongside mdbase.view records and reports
source.format: obsidian.base. execute_view selects the evaluator from the
contract the record's type implements: the Query profile for mdbase.view, the
Obsidian Bases dialect below for obsidian.base. A Base is created, replaced,
renamed, and deleted with the ordinary record operations under ordinary record
grants. Because Obsidian owns the format, writers SHOULD replace a Base with a
whole-document update so comments and layout survive; structured patches
remain valid but re-emit the YAML.
The type's path match replaces x-obsidian.bases.include, and the record
operations replace the saved-view source operations for Bases. Both remain
defined below for collections that do not list base as a record extension.
They are transitional: they are removed in the release that removes the
saved-view source operations for canonical views, and no new consumer should
adopt them.
Sources
Obsidian .base files are external saved-view sources. A collection provider
discovers configured sources and exposes them through the saved-view operations
in Operations. Core Read continues to discover records
through the collection's record extensions.
Collections enable discovery with a namespaced configuration section:
x-obsidian:
bases:
include:
- TaskNotes/Views/**/*.base
create_folder: TaskNotes/Views
default_for_new_views: true
include contains collection-relative globs as defined in
Collection Layout. The provider MUST apply
the same path-boundary and symlink protections used for record discovery.
create_folder identifies the preferred location for new Obsidian sources.
default_for_new_views makes that source format the collection's default when
a view-creation interface offers no explicit format. Providers advertising
write support use these values when creating a source.
Write-capable providers validate the complete .base document before a
source operation commits it. They preserve unknown top-level keys, view keys,
property metadata, formulas, and presentation options supplied in the
document. A source editor can therefore modify the structures it understands
while round-tripping the remainder.
The .base file remains authoritative for a discovered Obsidian source.
list_views returns source.format: obsidian.base, a revision derived from the
source bytes, and a stable named-view ID for each contained view. Stable IDs are
derived deterministically from view names when the source format supplies no
ID. Collisions receive deterministic source-order suffixes.
The structural mapping is:
| Obsidian Bases | mdbase view record |
|---|---|
global filters |
query.where |
view filters |
named-view where, combined with the shared filter |
formulas |
query.projections |
formula.name |
projection.name |
properties |
property metadata |
view order |
select order |
view sort |
order_by |
groupBy |
group_by |
| custom and property summaries | summary_functions and summaries |
view type |
presentation.type |
| plugin view keys | presentation options or x-* extension data |
Executing an Obsidian source evaluates its filters and formulas with Obsidian
Bases expression semantics. The adapter parses the source dialect into an
inspectable syntax tree and applies the source dialect's value coercion, date,
link, file, formula, and error behavior. Translation to canonical CEL is an
export operation and succeeds only when behavior is preserved. Translation
diagnostics identify unsupported expressions, functions, coercions, or
renderer features. Lossless source and round-trip metadata may be retained
under x-obsidian.
Execution returns the headless result envelope from
Operations. Selected and
presentation-mapped values appear under each row's values; renderer metadata
may approximate layout while retaining the source's filtering, formula,
ordering, grouping, and pagination semantics.
Obsidian placement state maps to the portable invocation context rather than to query semantics: opening a Base directly supplies the view definition, an embed supplies its embedding record, and an active-file interface supplies its active record. The adapter resolves that host state into an explicit invocation context before calling the query or view executor.
Optional Feature Requirements
An implementation advertises obsidian_bases_views through
optional_features when it:
- discovers
.basesources selected byx-obsidian.bases.include - assigns deterministic named-view IDs and source revisions
- parses filters and formulas before candidate evaluation
- evaluates the Obsidian Bases expression dialect, including formula dependencies, file and link values, date and duration behavior, methods, and coercions
- combines source and named-view filters with AND
- applies source order, sort, group, limit, and presentation metadata
- exposes the source's ordered properties and display names
- returns the saved-view headless result envelope
- keeps
.basesources authoritative throughout discovery and execution
Conformance suites for obsidian_bases_views MUST include an oracle corpus
captured from the supported Obsidian expression environment. Each case records
the expression, evaluation context, expected value or error, and source
environment version. Known upstream divergences are identified individually in
the corpus.