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:

  1. Recognizes collections by the presence of an mdbase.yaml configuration file.
  2. Loads type definitions from Markdown files that wrap JSON Schema.
  3. Matches records to types through explicit declarations or configured match rules.
  4. Validates frontmatter against matched schemas and collection-aware rules.
  5. Resolves links between records and exposes link-aware metadata.
  6. Executes queries using CEL expressions for filtering, ordering, and projection.
  7. Executes saved view records when it advertises the optional view_records feature.
  8. Performs record operations with validation, reference handling, and lifecycle-managed values.
  9. 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.

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.

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.

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.yaml
  • mdbase.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.yaml and mdbase.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:

  1. discovers candidate type files under the configured types folder
  2. validates each file against the built-in type-file schema
  3. canonicalizes type names for lookup and detects name conflicts
  4. resolves the embedded or referenced JSON Schema
  5. validates and compiles that schema against the v0.3 JSON Schema profile
  6. resolves and validates every implements entry 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:

  1. parses the record path and raw persisted frontmatter
  2. determines its matched types using Chapter 07
  3. validates the raw frontmatter against every matched JSON Schema
  4. evaluates every matched type's collection validators
  5. 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.type
  • name
  • schema

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:

  1. An mdbase.contract artifact defines a versioned subject-specific interface using JSON Schema 2020-12.
  2. For a record contract, a type's implements entry maps that interface to the type and supplies contract-specific binding data.
  3. An optional managed mdbase.type-pack groups 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_schema for 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:

  1. scans the configured contracts folder recursively
  2. validates every candidate against the built-in data-contract schema
  3. resolves and compiles the schemas selected by contract_type
  4. registers each contract by the exact pair (id, version)
  5. computes its contract digest
  6. resolves every type implements requirement to one exact contract version
  7. validates every type implements entry 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_schema validates 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:

  1. The live bytes equal the desired document: preserve.
  2. 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.
  3. The origin is the desired document: preserve. The seed was already upgraded and has been edited since.
  4. The origin is a baseline: update by a three-way merge of that baseline, the live type, and the desired type.
  5. Otherwise, because the origin is unknown or is not a listed baseline: preserve, with a reason stating 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:

  1. validate the manifest, safe paths, source bytes, and resource digests
  2. stage every resource and reviewed existing-type setup without changing the live collection
  3. resolve the complete staged contract and type registries
  4. validate every contract, implementation, type, reference, and affected existing record
  5. compute and present the exact resource diff and assessment digest
  6. acquire its collection mutation boundary and recheck the assessment
  7. commit all resource changes and the lock as one recoverable transaction
  8. 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:

  • type
  • required
  • properties
  • additionalProperties
  • items
  • enum
  • const
  • oneOf
  • anyOf
  • allOf
  • if, then, else
  • minimum, maximum
  • exclusiveMinimum, exclusiveMaximum
  • multipleOf
  • minLength, maxLength
  • pattern
  • minItems, 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: date
  • format: date-time
  • format: 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:

  1. Inspect the configured explicit type keys in configuration order.
  2. If any configured key is present, read and validate its declarations, resolve those names against the type registry, and use the resulting explicit set.
  3. Otherwise, evaluate every type that has a match section against the record.
  4. 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 null remains null
  • JSON Schema required continues 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.

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

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.

Parsed links expose:

  • raw
  • target
  • alias
  • anchor
  • format
  • is_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_field is 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:

  1. same directory as referring file
  2. shortest collection path
  3. 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.

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.

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 tags when 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.

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 operation
  • on_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 record and raw
  • old for the previous raw frontmatter on update
  • file for file metadata
  • operation for 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:

  1. parse input
  2. build a draft frontmatter object
  3. determine and freeze type membership from the draft and target path
  4. apply the lifecycle actions of every matched type
  5. re-evaluate membership and fail with type_membership_changed if it differs
  6. validate JSON Schema
  7. run collection validators
  8. 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_conflict errors 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 .?field and [?key] selection syntax, optional.of, optional.none, hasValue(), value(), or(), and orValue()
  • 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> and this.record.<field> expose effective context values
  • this.raw.<field> exposes persisted context frontmatter
  • this.file exposes 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-date string such as "2026-06-20". Because the format is fixed-width, CEL string comparison orders dates chronologically, so due < 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")

Core Read supplies file metadata and:

  • file.inFolder(path)

The Links profile adds:

  • link(value)
  • file.links, file.embeds, file.tags, and file.backlinks
  • file.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:

  1. the invocation's timezone
  2. the collection's configured timezone
  3. 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.

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) returns effective_frontmatter
  • persisted returns frontmatter
  • both returns both frontmatter and effective_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:

  1. the record MUST match exactly one type that implements mdbase.view; a record that matches none is not a view and produces view_not_found, and a record that matches several produces invalid_view
  2. the tool constructs the mdbase.view contract view through that type's field mapping and validates it as Chapter 05A requires; an invalid contract view produces invalid_view

All remaining steps read the contract view, not raw frontmatter. To derive an executable query:

  1. inherit query.types unless the named view supplies types
  2. combine query.where and named-view where with AND
  3. merge query.projections and named-view projections; the same name may appear in both only when its parsed definition is structurally equal, ignoring mapping key order
  4. inherit query.context unless the named view supplies context
  5. copy the named view's selection, ordering, grouping, summaries, pagination, and body options
  6. 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:

  1. determine target type or types from explicit input or path/match policy
  2. build draft frontmatter
  3. freeze pre-lifecycle type membership
  4. apply lifecycle on_create
  5. verify type membership did not change as a lifecycle side effect
  6. validate JSON Schema
  7. run collection validators
  8. 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
  9. write Markdown file
  10. update derived indexes
  11. 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:

  1. read existing raw frontmatter
  2. apply the requested patch and unset
  3. re-match and freeze types when type-affecting fields or path changed
  4. apply lifecycle on_update
  5. verify type membership did not change as a lifecycle side effect
  6. validate JSON Schema
  7. run collection validators
  8. write frontmatter and preserve body
  9. update derived indexes
  10. 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 null
  • unset: a list of field references, as defined in Chapter 07, whose keys are removed from persisted frontmatter
  • body: optional replacement Markdown body; a non-empty body for a YAML document record (Chapter 03) is invalid_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_version becomes 0.3.0.
  • settings.default_validation, or a top-level default_validation, becomes settings.validation. When neither is set, migration writes validation: warn, the v0.2 default.
  • A top-level id_field moves under settings. When neither is set, migration writes id_field: id, because v0.2 resolved wikilinks by ID by default.
  • settings.extensions becomes settings.record_extensions, without leading dots and including md.
  • settings.include_subfolders: false adds the exclusion */**, and the setting is removed.
  • Each settings.exclude pattern becomes a portable glob that excludes the same paths. A bare name without /, *, ?, or [ excluded that root path and everything below it, so archive becomes archive/**. A pattern without / that contains a wildcard matched file names at any depth, so *.draft.md becomes **/*.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_strict and the write options, have no v0.3 meaning. Migration moves them under x-legacy-v0.2 in the configuration and migrates strictness into each type's additionalProperties.

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.projections when 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:

  1. discover source config, type files, extension metadata, and records
  2. generate proposed config/types without modifying the collection
  3. validate generated schemas and type files
  4. validate all existing records against the proposed target
  5. emit a human-readable diff and machine-readable report
  6. require explicit approval unless every target is recognized as generated
  7. create a backup manifest containing hashes and original paths
  8. write through temporary files and atomic renames where supported
  9. 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 implements sections when data_contracts is 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_conflict for one ID and version with different digests
  • resolve every type implements version 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.yaml deterministically
  • 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_modification when 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_defaults to effective reads
  • preserve missing, null, raw, and effective distinctions
  • validate every collection.unique rule in its declared scope
  • validate portable collection.path policies for write-capable tools
  • expose display metadata as advisory values
  • compose compatible behavior from multiple matched types
  • report type_conflict for 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-time values as timestamps and provide now(), today(), the date conversions, and the date-string methods with declared timezone behavior
  • provide the lower() and upper() 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.expr while loading its type definition
  • evaluate it against raw candidate frontmatter and file metadata
  • combine it with other members of match using 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 this context 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 where filters 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.projections in 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_shadowed for 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.view record contract, never by type name, path, or raw frontmatter value
  • reports invalid_view for a record that matches several implementing types
  • validates the mdbase.view contract 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_missing and 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_revision to 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 implementations MUST:

  • parse wikilinks, Markdown links, and bare path link values
  • resolve collection-relative and file-relative paths safely
  • enforce collection.links.target_type and validate_exists
  • resolve simple wikilinks by filename, using ID resolution only when settings.id_field is configured
  • expose file.links, file.embeds, file.tags, and file.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_revision and report common concurrency conflicts
  • persist null patch values as explicit null and remove unset keys
  • apply the validation level to record validation issues
  • execute batches atomically by default, support allow_partial and dry_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_create and on_update with single-action and action-list forms
  • support now, today, uuid, ulid, slugify, copy, and literal value 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_created
  • record_modified
  • record_deleted
  • record_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, with record_schema and optional binding_schema;
  • event, with data_schema and optional source_schema;
  • action, with input_schema, optional output_schema, optional error_schema, optional provider_schema, and optional behavior.

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, and version;
  • the fully resolved subject-specific schemas;
  • action behavior when 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
  • application names the product or package;
  • implementation names a concrete implementation line;
  • version names 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:

  1. finds exact action artifacts satisfying the request;
  2. applies an optional requested-provider selector;
  3. removes unauthorized or unavailable providers;
  4. selects the one remaining provider; or
  5. returns no_provider, requested_provider_unavailable, or ambiguous_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:

  1. the portable envelope;
  2. the exact contract identity and digest;
  3. source or caller identity attributed by the active binding;
  4. implementation declarations and bindings;
  5. event data, action input, output, or declared error details;
  6. 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_id across 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:

  1. the core mdbase contract system defines contract identity, semantic versions, JSON Schemas, digests, record implementations, resolution, and packs;
  2. the event/action interoperability profile defines CloudEvents, action requests and outcomes, event-source and action-provider declarations, provider selection, and baseline exchange behavior;
  3. 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.contract artifacts.
  • 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:

  1. validates the workflow through mdbase.runtime.workflow;
  2. checks unique trigger and step IDs and compiles expressions;
  3. resolves each contract requirement through the core contract registry;
  4. uses verified interoperability declarations to find implementations;
  5. applies explicit or policy provider selection;
  6. evaluates capability and operational policy;
  7. 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:

  1. journals and deduplicates the CloudEvent;
  2. identifies the exact event-source declaration;
  3. evaluates trigger and admission policy;
  4. resolves every action provider;
  5. pins the workflow revision;
  6. pins exact event/action contract ID, version, and digest;
  7. pins exact source/provider identities and declaration digests;
  8. records handler IDs, idempotency decisions, and deadlines;
  9. reserves the run idempotency key and concurrency position;
  10. 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 $expr key;
  • steps execute in document order;
  • for_each visits 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.

  1. scheduling writes generation n, fire time, and an event contract requirement plus event data;
  2. rescheduling writes generation n + 1 and makes older generations stale;
  3. a worker atomically claims the current due generation;
  4. firing publishes mdbase.runtime.timer.fired or the requested event through an interoperability event source;
  5. the resulting CloudEvent is admitted through the ordinary event journal;
  6. the timer becomes fired only 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.standard pack
  • 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_provider or ambiguous_provider rather 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_each iteration 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 debounce and minimum_interval admission
  • 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_expired rather 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 .base sources selected by x-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 .base sources 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.