00. Overview
Abstract
This specification defines the behavior of tools that treat folders of Markdown files as typed, queryable, link-aware data collections. It covers collection discovery, JSON Schema types, validation, links, CEL queries, ordinary records that save named views, record operations, lifecycle policy, and optional runtime workflows.
Motivation
Markdown files with YAML frontmatter are a common way to store structured content. The pattern appears in static site generators, knowledge management tools such as Obsidian, documentation systems, agent workspaces, and AI agent frameworks that use Markdown for persistent state.
These ecosystems need consistent conventions for frontmatter structure, validation, querying, links, and updates. This specification defines one coherent set of behaviors so that:
- a CLI tool and an editor plugin can operate on the same files with consistent semantics
- an AI agent can read and write Markdown files that a human can inspect and edit
- tool authors can implement a shared behavior contract
- collections can move between conforming tools
Intended implementers
CLI tools for querying, validating, and manipulating Markdown collections from the command line.
Editor plugins for applications such as Obsidian and VS Code that provide validation, completion, navigation, and query interfaces.
Libraries in different languages that applications can use to work with typed Markdown.
AI agent frameworks that need structured, human-readable persistent storage.
Runtime hosts that connect collection events to declared actions and workflows.
What a conforming tool does
Depending on its conformance profiles and optional features, a tool implementing this specification:
- Recognizes collections by the presence of an
mdbase.yamlconfiguration file. - Loads type definitions from Markdown files that wrap JSON Schema.
- Matches records to types through explicit declarations or configured match rules.
- Validates frontmatter against matched schemas and collection-aware rules.
- Resolves links between records and exposes link-aware metadata.
- Executes queries using CEL expressions for filtering, ordering, and projection.
- Executes saved view records when it advertises the optional
view_recordsfeature. - Performs record operations with validation, reference handling, and lifecycle-managed values.
- Loads runtime contracts and workflows when it supports active behavior.
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
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:
status == "open" && "urgent" in tags
due_date < today()
assignee.asFile().team == "engineering"
View records save reusable queries
A collection can define the ordinary view type and store one or more named
queries in a Markdown record. Shared query scope, named-view filters,
projections, ordering, grouping, and summaries remain machine-readable, while
the Markdown body documents the view for people. Optional presentation metadata
can select a renderer without changing query results.
Validation is progressive
Files in a collection can remain untyped records. Types can be added
incrementally, and validation severity is configurable as off, warn, or
error. JSON Schema controls field shape and unknown-property handling.
Collection rules add checks that depend on other records or paths.
Links connect records across the collection
Records can reference each other with wikilinks such as [[alice]], Markdown
links such as [Alice](../people/alice.md), or declared path values. Link-aware
tools resolve targets, extract links and tags from record bodies, traverse
links in queries, and can update references during rename operations.
Lifecycle policy manages values during writes
Lifecycle policy can assign IDs, timestamps, slugs, copied values, and literals during create and update operations. Lifecycle runs before final validation, so managed fields participate in the same schema and collection checks as supplied frontmatter.
Runtime contracts describe active behavior
Optional runtime records describe providers, events, actions, capabilities, policies, workflows, runs, and checkpoints. A runtime host loads these contracts, validates their data, and connects declared workflows to available event and action implementations.
Conformance is profile-based
Implementations claim the profiles they support, such as Core Read, Collection Semantics, Links, Query, Core Write, Lifecycle, Runtime Contracts, Workflow, 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 |
| 05-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-runtime-contracts.md | Providers, events, actions, capabilities, and policy |
| 14-workflows.md | Workflow records and execution semantics |
| 15-migrations-and-compatibility.md | Migration from earlier versions and compatibility |
| 16-conformance.md | Profiles, claims, fixtures, and runners |
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.
The optional runtime-contract and workflow vocabulary has an independent profile version. The runtime profile defined by this specification is 0.2.
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 runtime 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 is view. It stores
shared query scope and one or more named queries, with optional advisory
presentation metadata.
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 is not a runtime contract.
Link
A link is a frontmatter value or body reference that can resolve to another record. mdbase recognizes wikilinks, Markdown links, and bare path strings where a field is declared as link-aware.
JSON Schema validates the local string or array shape. collection.links
declares link meaning, target type, and existence requirements.
Runtime
A runtime is a process, plugin, daemon, CLI, CI job, or agent that executes runtime-profile behavior for a collection.
The core collection model is runtime-neutral. Runtime records make active behavior portable and inspectable without making every implementation a runtime.
Runtime Contract
A runtime contract is a typed record or virtual registry entry describing the interface of a provider, event, action, capability, policy, run, checkpoint, diagnostic, or workflow.
Runtime contracts describe the interfaces used by action handlers, event sources, watchers, schedulers, agents, and provider APIs supplied by a runtime.
Explicit And Implicit Runtime Contracts
Explicit contracts are ordinary Markdown records in a collection or installed pack.
Implicit contracts are supplied by a conforming runtime, for example built-in file events or core record actions. A runtime may materialize implicit contracts as Markdown records for inspection or offline tooling.
02. Collection Layout
Identification
A directory is an mdbase v0.3 collection when it contains mdbase.yaml with a
supported v0.3 spec_version.
spec_version: "0.3.0"
Tools MUST NOT treat a parent directory as owning records below a nested
directory that has its own mdbase.yaml.
Recommended Layout
collection/
mdbase.yaml
_types/
meta.md
task.md
workflow.md
action.md
event.md
_contracts/
example.task.md
actions/
mdbase.record.patch.md
events/
file.created.md
workflows/
route-new-file.md
policies/
local-runtime-policy.md
tasks/
example.md
Only mdbase.yaml is required. Untyped records form a valid collection.
View records are ordinary records 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- the configured types folder, default
_types/ - the configured contracts folder, default
_contracts/ .mdbase/for derived implementation state- nested collection roots
Runtime folders such as providers/, actions/, events/, workflows/,
capabilities/, policies/, runs/, and checkpoints/ contain ordinary
records unless excluded by configuration. Their meaning comes from their type
files and runtime contracts.
Record Discovery
Tools discover records by recursively scanning the collection root for files with configured record extensions. 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/ - stop scanning at nested collection roots
- ignore non-record extensions unless configured otherwise
Tools SHOULD exclude common derived directories by default, including .git/
and node_modules/.
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
Runtime records are discovered like ordinary records. A workflow record is a
record whose effective type is workflow. An action contract is a record whose
effective type is action. The effective type is authoritative; folder names
are conventions.
Tools SHOULD preserve common folder names for portability:
actions/events/providers/workflows/capabilities/policies/runs/checkpoints/
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
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.
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 is invalid at validation level error. At validation level warn, tools
SHOULD treat it as empty frontmatter and report a warning.
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 is the Markdown content after the closing frontmatter delimiter. 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.
When serializing frontmatter, tools SHOULD:
- omit missing values; bare nulls represent explicit null values
- quote empty strings
- preserve array/object structure
- produce deterministic key ordering when the operation rewrites a generated file
When updating a field to null, tools MAY either persist explicit null or remove the key depending on operation policy. The operation result MUST make the chosen behavior explicit.
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:
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.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 | id |
field used for ID-based link and contract resolution |
settings.include_subfolders |
boolean | true |
whether record scanning recurses |
settings.exclude |
list of globs | implementation default | excluded paths |
settings.explicit_type_keys replaces the default key list. An empty list makes
all type membership inferred.
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.
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 three parts of the mdbase model:
- a rule for selecting records
- a JSON Schema for validating their persisted frontmatter
- collection behavior such as defaults, links, lifecycle assignments, and path policy
- portable data-contract implementations
The YAML frontmatter is the machine-readable definition. The Markdown body can document the type for people and tools.
Minimal Type
---
kind: mdbase.type
name: task
version: 1
match:
path_glob: "tasks/**/*.md"
schema:
dialect: json-schema-2020-12
value:
$schema: "https://json-schema.org/draft/2020-12/schema"
type: object
required: [title]
additionalProperties: false
properties:
type:
const: task
title:
type: string
minLength: 1
---
# Task
Task records live under `tasks/`.
Type Evaluation Model
Type processing has a collection-load phase and a record-evaluation phase.
During collection load, an implementation:
- discovers candidate type files under the configured types folder
- validates each file against the built-in type-file schema
- canonicalizes type names for lookup and detects name conflicts
- resolves the embedded or referenced JSON Schema
- validates and compiles that schema against the v0.3 JSON Schema profile
- resolves and validates every
implementsentry against the local data contract registry
Each valid definition then enters the collection's type registry. Diagnostics for an invalid definition identify the type-file path and the failing section.
For each record, an implementation:
- parses the record path and raw persisted frontmatter
- determines its matched types using Chapter 07
- validates the raw frontmatter against every matched JSON Schema
- evaluates every matched type's collection validators
- constructs effective read values, including compatible read defaults and projections supported by the implementation
Type selection and record validation are separate operations. An explicit type declaration selects a type and still subjects the record to that type's schema and collection rules.
Write operations use the same model around lifecycle processing: determine membership from the requested draft, run the applicable lifecycle assignments, then confirm membership once before final validation. The complete write order is defined in Chapters 09 and 12.
Required Frontmatter
A type file MUST include:
kind: mdbase.typenameschema
version SHOULD be present and SHOULD be a positive integer.
Type Names
Type names are stable identifiers. They SHOULD use lower-case ASCII names with
letters, numbers, _, and -.
Tools MUST compare type names case-insensitively for matching and conflict detection. Displays SHOULD preserve the casing written by the author.
Two type files whose names differ only by case are conflicting definitions.
Schema Section
schema.dialect identifies the schema profile. v0.3 core defines:
schema:
dialect: json-schema-2020-12
The schema can be embedded:
schema:
dialect: json-schema-2020-12
value:
type: object
properties:
title: { type: string }
or referenced:
schema:
dialect: json-schema-2020-12
ref: "../schemas/task.schema.json"
External references resolve relative to the type-file path. Resolution MUST remain within the collection or installed pack root that owns the schema.
Exactly one of value and ref is required. A type containing both is invalid.
The JSON Schema validates raw persisted frontmatter. Read defaults, projections, and lifecycle values enter at the stages defined by their own chapters.
mdbase Sections
The following top-level sections are defined by v0.3:
| Section | Purpose |
|---|---|
match |
select records for inferred type membership |
collection |
define Markdown-aware collection semantics |
lifecycle |
assign managed values during mutations |
runtime |
attach runtime annotations to the type |
migrations |
declare explicit type-version migration steps |
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 use the ordinary view type defined by
schemas/v0.3/view.schema.json. A collection that stores portable view records
SHOULD materialize _types/view.md with match.where.type: view and a local
reference to that schema. The repository's _types/view.md is the canonical
materialization.
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. 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. First-Class Contracts
Why Data Contracts Exist
A contract describes one portable interface independently from the local type or application that implements it.
For example, personal_task, work_task, and task can all implement
tasknotes.task. Their filenames, additional fields, matching rules, and local
presentation can differ. An application can discover the shared contract,
understand each type's field mapping, and operate without requiring every
collection to use one canonical type name.
mdbase.contract is the shared identity and JSON Schema substrate for passive
record views, events, and actions. contract_type discriminates their
subject-specific schema fields. This chapter defines the shared artifact and
record implementation rules. The optional
event/action interoperability profile defines executable
source/provider declarations and message exchange. Contracts never grant
permission.
Three Portable Artifacts
The complete collection contract model has three intentionally small parts:
- An
mdbase.contractartifact defines a versioned subject-specific interface using JSON Schema 2020-12. - For a
recordcontract, a type'simplementsentry maps that interface to the type and supplies contract-specific binding data. - An optional
mdbase.type-packmanifest groups contracts, types, and their referenced schemas for transactional installation.
Event sources and action providers make runtime declarations because they are executable, instance-specific implementations rather than record types. Requirements, authorization grants, and transports are not collection contract artifacts.
Designing Record Contracts
A record contract SHOULD describe a compact, application-facing semantic interface. Its properties name values that independent consumers can rely on, while an implementing type remains free to choose local field names, matching rules, additional fields, and presentation.
Record contracts SHOULD:
- keep the unconditionally required surface as small as the shared behavior permits
- use optional properties for semantics that not every implementing type can provide
- use
binding_schemafor implementation-specific vocabularies and behavior, such as which local task statuses count as completed - be shared by applications that need the same semantics rather than duplicated under application-specific IDs
A record contract SHOULD NOT reproduce an external interchange or storage format merely so applications can request that serialization. An importer, exporter, or application adapter can translate a semantic record view to JSContact, vCard, or another wire format. A wire-format-shaped record contract remains valid when the implementing type intentionally stores and exposes that exact shape.
Event and action contracts are different: their schemas describe complete messages at an interoperability boundary, so a transport-shaped schema is usually appropriate.
Contract Files
Contract files are Markdown files under the configured contracts folder,
default _contracts/. Their frontmatter has kind: mdbase.contract and
validates against schemas/v0.3/data-contract.schema.json.
---
kind: mdbase.contract
contract_type: record
id: example.task
version: 1.0.0
name: Example task
description: A small portable task interface.
record_schema:
dialect: json-schema-2020-12
value:
$schema: "https://json-schema.org/draft/2020-12/schema"
type: object
required: [title, status]
additionalProperties: false
properties:
title: { type: string, minLength: 1 }
status: { type: string, minLength: 1 }
due: { type: string, format: date }
binding_schema:
dialect: json-schema-2020-12
value:
$schema: "https://json-schema.org/draft/2020-12/schema"
type: object
required: [completed_values]
additionalProperties: false
properties:
completed_values:
type: array
minItems: 1
uniqueItems: true
items: { type: string }
---
# Example task
The body explains the interface to people. Portable behavior is defined by the
frontmatter schemas.
id is a lower-case namespaced identifier. version is an exact semantic
version. A type implementation never names a version range.
record_schema validates the normalized contract view produced from a record.
binding_schema, when present, validates implementation-specific semantic
configuration. Both use the same JSON Schema profile and reference rules as
type schemas.
An event contract instead requires data_schema and may declare
source_schema. An action contract requires input_schema and may declare
output_schema, error_schema, provider_schema, and behavior. Subject
fields belonging to another contract_type are invalid.
Contract files are control files, not records. They do not participate in ordinary record scans, queries, links, or runtime workflow discovery.
Contract Registry
During collection load, a data-contract-aware implementation:
- scans the configured contracts folder recursively
- validates every candidate against the built-in data-contract schema
- resolves and compiles the schemas selected by
contract_type - registers each contract by the exact pair
(id, version) - computes its contract digest
- validates every type
implementsentry against the resulting registry
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 |
exact contract semantic version |
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 and
version. Field mappings MUST address fields declared by the resolved
record_schema and the resolved type schema. Every unconditional top-level
field named by the contract's record_schema.required array MUST be mapped,
either by the matching one-segment field path or by the matching one-token JSON
Pointer.
When a contract has a binding_schema, the implementation's binding value, or
an empty object when omitted, MUST validate against it. When a contract has no
binding_schema, binding MUST be absent or empty.
Private application metadata may remain under x-*, but an x-* object has no
contract-discovery, conformance, or authorization meaning.
Contract Views And Record Validation
To construct a contract view, a tool starts with a record's effective
frontmatter and copies every mapped value to its contract field reference. Missing
optional values remain missing. The resulting object is validated against the
contract's record_schema.
Contract validation complements rather than replaces type validation:
- the type schema validates raw persisted frontmatter
- collection semantics construct the effective record
- the field map constructs a normalized contract view
- the contract's
record_schemavalidates that view
A record can therefore satisfy its type schema and still produce
data_contract_record_invalid for one declared implementation. Implementations
MUST surface that diagnostic whenever they expose the record through that
contract.
Static checks at collection load SHOULD diagnose obviously incompatible mapped schema types early. Runtime contract-view validation remains authoritative when JSON Schema composition makes static implication impractical.
Stable Digests
Digests let a consumer distinguish an approved implementation from a later change that happens to retain the same name.
The contract digest is SHA-256 over RFC 8785 JSON Canonicalization Scheme bytes for this object, using the fully resolved JSON Schema values rather than their storage wrappers or reference paths:
{
"kind": "mdbase.contract",
"contract_type": "record",
"id": "...",
"version": "...",
"record_schema": {},
"binding_schema": {}
}
The digest object contains the subject-specific schema keys selected by
contract_type: record_schema and binding_schema; data_schema and
source_schema; or input_schema, output_schema, error_schema,
provider_schema, and behavior. Absent optional members are omitted.
Human-facing name, description, Markdown body, x-* metadata, schema
wrapper dialects, and local ref paths do not affect portable identity.
Consequently, an inline schema and a local referenced schema with identical
resolved JSON values have the same contract digest, while changing the bytes
at a stable reference path changes the digest.
The implementation digest is SHA-256 over RFC 8785 bytes for:
{
"contract_digest": "sha256:...",
"type": {
"name": "...",
"version": 1,
"match": {},
"schema": {},
"collection": {},
"lifecycle": {}
},
"implementation": {}
}
Absent optional members are omitted. This deliberately includes membership, shape, defaults, links, paths, and lifecycle behavior. A consumer that pinned an implementation can detect any portable change that may alter the records or values it observes.
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
source: contracts/example.task.md
target: _contracts/example.task.md
digest: sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
- kind: type
source: types/task.md
target: _types/task.md
digest: sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789
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. This repository fixes text checkouts to LF so its example pack has identical bytes on every supported platform.
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.
Transactional Pack Installation
A pack-aware installer MUST:
- validate the manifest, safe paths, source bytes, and resource digests
- stage every resource without changing the live collection
- resolve the complete staged contract and type registries
- validate every contract, implementation, type, reference, and affected existing record
- compute and present the exact create, replace, and unchanged diff
- acquire its collection mutation boundary and recheck overwritten hashes
- commit all resources as one recoverable transaction
- reopen the collection and verify the committed registry
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.
A pack install or dry-run result reports the pack id, exact version, and
every resource in manifest order as { target, action, digest }, where
action is create, replace, or unchanged. It also reports
cleanup_deferred when committed state is valid but transaction-journal cleanup
must be retried. Reinstalling identical bytes is valid and reports every
resource as unchanged; it MUST NOT create a new logical collection revision.
Diagnostics
Data-contract-aware tools use these codes:
| Code | Meaning |
|---|---|
invalid_data_contract |
contract frontmatter or schema is invalid |
data_contract_not_found |
an implementation references no local exact contract |
data_contract_conflict |
one ID and version resolve to different contract digests |
data_contract_version_mismatch |
a consumer requirement has no compatible exact version |
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, resource, path, or digest is invalid |
type_pack_conflict |
a target differs from live state and replacement was not approved |
type_pack_apply_failed |
transactional commit or recovery did not complete normally |
Diagnostics use the canonical shape from Chapter 16 and identify the contract
ID, exact version, type name, and relevant field mapping in details.
mdbase Event and Action Interoperability Profile 0.1
1. Purpose
This optional profile lets independently developed applications exchange typed events and invoke typed actions without adopting one workflow engine or durable runtime.
The profile defines:
- event and action contract kinds built on
mdbase.contract; - a CloudEvents 1.0 event envelope;
- action request, admitted invocation, cancellation, and outcome envelopes;
- event-source and action-provider declarations;
- discovery, version resolution, validation, authorization boundaries, and duplicate semantics;
- role-specific conformance;
- transport capabilities and the baseline bridge behavior.
The profile does not define workflows, schedules, retries, leases, persistence, or a universal authorization policy.
The profile identifier is event_action_interop/0.1. Envelope
profile_version values use 0.1; event envelopes carry the CloudEvents
extension mdbaseprofile: "0.1".
The normative schemas are in schemas/interop/v0.1/.
2. Layering
The layers have separate responsibilities:
| Layer | Responsibility |
|---|---|
| Contract | identity, exact semantic version, canonical digest, schema, and meaning |
| Interoperability profile | message envelopes, roles, resolution, and boundary validation |
| Transport binding | physical delivery and declared delivery capabilities |
| Application runtime | application-specific behavior |
| Durable runtime | optional workflow state, retries, timers, leases, and recovery |
A contract demonstrates data compatibility. It does not grant authority. Registration, application installation, and conformance claims do not grant authority either.
3. Conformance Roles
Implementations claim only roles they implement:
| Role | Required behavior |
|---|---|
event_source |
register exact event artifacts and publish valid events |
event_consumer |
subscribe by compatible requirement and validate delivered events |
action_caller |
create requests and handle portable outcomes |
action_provider |
register exact action artifacts and execute selected invocations |
bridge |
resolve, authorize, validate, route, and expose execution evidence |
Transport bindings may define additional roles. A role claim never implies another role.
4. First-Class Contract Artifacts
All passive records, events, and actions use the same control-file envelope:
kind: mdbase.contract
contract_type: event
id: tasknotes.task.completed
version: 1.0.0
name: Task completed
description: A TaskNotes task transitioned from incomplete to complete.
data_schema:
dialect: json-schema-2020-12
value:
type: object
required: [task_id, completed_at]
additionalProperties: false
properties:
task_id: { type: string, minLength: 1 }
completed_at: { type: string, format: date-time }
contract_type is one of:
record, withrecord_schemaand optionalbinding_schema;event, withdata_schemaand optionalsource_schema;action, withinput_schema, optionaloutput_schema, optionalerror_schema, optionalprovider_schema, and optionalbehavior.
Every schema uses the mdbase JSON Schema 2020-12 profile. Event data, action
input, successful action output, declared action error details, and
implementation binding values validate against the relevant exact artifact.
Record implements entries remain in type files. Event sources and action
providers make runtime implementation declarations because their
implementations are executable and instance-specific.
4.1 Digest
The contract digest is SHA-256 over RFC 8785 canonical JSON containing:
kind,contract_type,id, andversion;- the fully resolved subject-specific schemas;
- action
behaviorwhen present.
Names, descriptions, Markdown bodies, x-* metadata, wrapper dialect keys, and
local schema reference paths do not affect the digest.
The digest input uses the schema field names defined for the selected
contract_type and omits absent optional fields. A bridge MUST fail with
contract_digest_conflict when one (id, version) pair is presented with
different contract bytes.
5. Requirements and Exact References
An author-facing contract requirement contains:
{ "id": "canvas.card.create", "version": "^1.0.0" }
version is a SemVer range. digest may additionally pin one artifact.
Execution evidence contains an exact reference:
{
"id": "canvas.card.create",
"version": "1.2.0",
"digest": "sha256:..."
}
A bridge resolves a requirement to one exact artifact before event delivery or action admission. It MUST NOT change a pinned artifact or selected provider after admission.
6. Implementation Identity
Portable implementation identity has four distinct layers:
application: canvas-bases
implementation: canvas-bases.obsidian
version: 1.4.0
instance_id: vault-01
applicationnames the product or package;implementationnames a concrete implementation line;versionnames its build version;instance_id, when present, distinguishes a live installation.
Declarations and outcomes additionally expose a declaration digest. A transport binding owns the authenticated principal and authorization evidence. The portable identity is not itself authentication.
7. Events
7.1 Event Contracts and Source Declarations
An event contract defines the schema and meaning of data. It does not name
one owner. Any number of authorized sources may implement the same exact event
contract.
An event-source declaration identifies one implementation, its author-declared
compatible requirement, its exact resolved artifact, optional binding data,
and ordering capabilities. Binding data validates against source_schema when
the contract supplies one.
7.2 CloudEvents Envelope
Events use CloudEvents 1.0 structured JSON. The standard attributes retain their CloudEvents meanings:
{
"specversion": "1.0",
"id": "evt_01...",
"source": "urn:mdbase:app:tasknotes:tasknotes.obsidian",
"type": "tasknotes.task.completed",
"time": "2026-07-28T11:30:00.000Z",
"subject": "urn:mdbase:record:vault-01:task-123",
"datacontenttype": "application/json",
"dataschema": "urn:mdbase:contract:tasknotes.task.completed:1.0.0:sha256:...",
"mdbaseprofile": "0.1",
"mdbasecontractversion": "1.0.0",
"mdbasecontractdigest": "sha256:...",
"mdbaseapplication": "tasknotes",
"mdbaseimplementation": "tasknotes.obsidian",
"mdbaseimplementationversion": "5.0.0",
"correlationid": "flow_01...",
"causationid": "req_01...",
"data": {
"task_id": "task-123",
"completed_at": "2026-07-28T11:30:00.000Z"
}
}
source and id together identify a logical event across redelivery.
time is when the domain occurrence happened. Transport receipt time remains
transport metadata. subject is an optional stable URI reference.
type, mdbasecontractversion, and mdbasecontractdigest identify the exact
event artifact. dataschema is its canonical URN. Bridges MUST reject an
inconsistent combination.
correlationid groups related activity. causationid identifies the event,
request, or invocation that directly caused this event.
Consumers subscribe by contract ID and compatible version range. Every authorized matching consumer receives the same logical event. Adding a consumer does not alter another consumer's subscription.
Events have no global ordering guarantee. Bindings declare none, source,
or subject ordering scopes.
8. Actions
Events multicast. Actions select exactly one provider.
8.1 Action Contracts and Provider Declarations
An action contract defines the requested effect and its input, successful output, declared portable errors, provider binding, and behavior. It does not name or authorize a provider.
An action-provider declaration identifies:
- the provider implementation;
- a stable handler ID;
- an author-declared compatible contract requirement;
- the exact resolved artifact;
- optional provider binding;
- cancellation and request-deduplication support;
- optional operational limits.
Provider binding validates against provider_schema.
8.2 Request
A request is logical caller intent:
kind: mdbase.action.request
profile_version: "0.1"
request_id: req_01...
contract:
id: canvas.card.create
version: ^1.0.0
caller:
application: tasknotes-workflows
implementation: tasknotes-workflows.obsidian
version: 1.0.0
created_at: 2026-07-28T11:30:01Z
correlation_id: flow_01...
causation_id: evt_01...
idempotency_key: flow_01:canvas-card
input:
canvas: Projects/Report.canvas
title: Prepare report
An optional requested-provider selector may pin an application,
implementation, or instance. An optional deadline is an RFC 3339 timestamp.
An authorization-context reference is bridge supplied and opaque.
The request is not evidence that a provider was selected or that an effect occurred.
8.3 Admitted Invocation
Before dispatch, a bridge records an admitted invocation containing:
request_id, preserving logical intent;- a new
invocation_id; - an exact contract version and digest;
- the selected provider identity and declaration digest;
- the selected handler ID;
- an
attempt_id; - admitted input and trace context;
- admission time and applicable deadline.
The bridge validates the action input and authorization before admission. Provider selection and the exact contract MUST NOT change after admission.
The base bridge creates one attempt. A durable binding or runtime may create
later attempts but preserves the request and invocation identities and gives
each concrete handler attempt a distinct attempt_id.
8.4 Outcome
Every terminal result has a distinct outcome_id and identifies its request,
invocation, attempt, exact contract, provider, and completion time.
Statuses are:
succeeded;rejected;failed;cancelled;outcome_indeterminate.
Successful output validates against output_schema when present. Every
unsuccessful outcome carries a portable error. A bridge MUST turn an invalid
provider output into failed with invalid_action_output; it MUST NOT deliver
invalid output as success.
Transport acceptance is not an action outcome.
8.5 Selection
A bridge:
- finds exact action artifacts satisfying the request;
- applies an optional requested-provider selector;
- removes unauthorized or unavailable providers;
- selects the one remaining provider; or
- returns
no_provider,requested_provider_unavailable, orambiguous_provider.
Providers are never unioned or broadcast. A bridge MUST NOT silently select the first registered provider. Adding a provider cannot redirect an admitted request or an explicitly pinned caller.
9. Validation and Authorization
At every boundary, a bridge validates:
- the portable envelope;
- the exact contract identity and digest;
- source or caller identity attributed by the active binding;
- implementation declarations and bindings;
- event data, action input, output, or declared error details;
- active authorization.
Authorization is separate from compatibility. A bridge may authorize by principal, role, contract, exact implementation, collection/subject scope, capability, approval, or resource limit. Credentials and sensitive grants stay in transport-private state; portable envelopes carry only an opaque reference when necessary.
Unauthorized publish, subscribe, provider registration, and invocation fail closed.
10. Duplicate, Idempotency, and Indeterminate Outcomes
The profile does not claim universal exactly-once behavior.
- Consumers deduplicate events by
(source, id)when duplicate processing is harmful. - Requests preserve
request_idacross transport retry. - An idempotency key is meaningful only when the action contract permits it and the selected provider declares request deduplication.
- A duplicate request to such a provider returns its recorded terminal outcome during the declared retention window.
- Without declared deduplication, the bridge rejects a duplicate request rather than inventing exactly-once behavior.
- If the caller cannot know whether an external effect occurred, the terminal
status is
outcome_indeterminate.
Retry policy belongs to the caller, binding, or durable runtime.
11. Cancellation and Deadlines
A caller may request cancellation by request_id. Cancellation is
cooperative. The bridge verifies caller ownership and selected-provider
support, then signals the current attempt.
Possible races are represented honestly:
- cancellation before admission returns
cancelled; - a cooperatively cancelled attempt returns
cancelled; - a completed action keeps its completed outcome;
- an effect whose completion cannot be determined returns
outcome_indeterminate; - a provider without cancellation support returns
cancellation_unsupported.
A bridge supporting deadlines rejects admission after the deadline and signals
a cooperative provider when the deadline expires during an attempt. A binding
that cannot enforce a required deadline fails
unsupported_transport_capability.
12. Transport Capabilities
A binding describes:
- delivery:
ephemeral,at_least_once,durable_cursor,offline_queue; - ordering:
none,source,subject; - cancellation and deadline support;
- provider discovery;
- maximum payload size;
- outcome retention;
- request deduplication;
- cross-process identity.
An application that requires an unavailable capability fails clearly before exchange.
The baseline Obsidian in-process binding is ephemeral, process-local, and cooperatively attributed. Obsidian does not isolate plugins as security principals, so this binding cannot defend against a malicious plugin running in the same process. It still prevents accidental cross-application registration through opaque client handles and validates host-attributed identities at each boundary.
13. Baseline Bridge API
The behavior-based API is:
connect one implementation identity
describe contracts and implementations
register event source
publish event
subscribe to events
register action provider
invoke action
receive action outcome
cancel action
dispose client registration
Language SDKs may use idiomatic names. A client handle scopes every subscription and provider registration to one implementation and removes them on disposal.
14. Standard Errors
Portable error codes are defined by
schemas/interop/v0.1/profile.schema.json#/$defs/portableError.
Transport-private diagnostics may be nested in safe details, but they do not
replace the portable category. Error messages MUST NOT expose credentials,
private authorization policy, or provider-internal stack traces.
15. Relationship to Runtime and Connect
The mdbase 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.
Connect may implement a durable authorized binding using the same envelopes. Connect 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.
06. JSON Schema Profile
Base Dialect
v0.3 uses JSON Schema 2020-12 for persisted frontmatter shape.
Every embedded schema SHOULD include:
$schema: "https://json-schema.org/draft/2020-12/schema"
The containing type file MUST declare:
schema:
dialect: json-schema-2020-12
Data Model
YAML frontmatter is converted into the JSON data model before schema validation:
- mapping -> object
- sequence -> array
- string -> string
- integer/float -> number, with integer constraints enforced by JSON Schema
- boolean -> boolean
- null -> null
Values that cannot be represented in the JSON data model MUST be rejected or normalized by the mdbase YAML profile before JSON Schema validation.
Required Support
Core v0.3 JSON Schema support includes:
typerequiredpropertiesadditionalPropertiesitemsenumconstoneOfanyOfallOfif,then,elseminimum,maximumexclusiveMinimum,exclusiveMaximummultipleOfminLength,maxLengthpatternminItems,maxItems,uniqueItems$defs- local
$ref title,description,default,examples, and other annotations
Tools MAY support more of JSON Schema 2020-12. The required profile defines the portable baseline for Core Read conformance.
Discriminated Unions
v0.3 uses ordinary JSON Schema constructs for discriminated unions.
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.
Example:
type: object
required: [kind, input]
oneOf:
- properties:
kind: { const: notice.show }
input:
type: object
required: [message]
properties:
message: { type: string }
additionalProperties: false
required: [kind, input]
- properties:
kind: { const: mdbase.record.patch }
input:
type: object
required: [path, patch]
properties:
path: { type: string }
patch: { type: object }
additionalProperties: false
required: [kind, input]
Runtime workflow steps normally use step.action plus action contracts instead
of embedding every action union directly in the workflow schema.
Defaults
JSON Schema default is an annotation. Record mutation occurs through create,
lifecycle, or explicit default materialization, and required evaluates the raw
record during validation.
Tools MAY use default as a create/editor hint.
Effective read defaults belong in collection.read_defaults.
Dynamic defaults such as now, uuid, ulid, and slugify belong in
lifecycle.
Format
JSON Schema format is an annotation in the base dialect. The mdbase v0.3
profile requires assertion behavior for:
format: dateformat: date-timeformat: time
date, time, and date-time use the RFC 3339 productions referenced by JSON
Schema. date-time values MUST contain Z or a numeric UTC offset. Invalid
values produce format_invalid diagnostics.
Other formats such as email, uri, and uuid MAY be asserted by
implementations. Collections that require them depend on implementation-specific
behavior until a conformance profile defines their semantics.
References
Tools MUST support fragment-only $ref references within an embedded schema.
They MUST also support a type wrapper's schema.ref to a local JSON file.
The base URI for schema.ref is the directory containing the type file. The
resolved file MUST remain inside the collection root or inside the installed
pack root that supplied the type. Symlinks are resolved before this boundary
check. Escapes produce schema_ref_forbidden.
Nested file-to-file $ref references use the optional feature identifier
external_schema_refs. An implementation supporting the feature lists it under
optional_features in its claim document. Such references use the containing
schema document as their base URI, obey the same owning-root boundary, detect
cycles, and produce schema_ref_unresolved or schema_ref_cycle diagnostics.
Implementations that do not support the feature report unsupported_profile
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/
Runtime profile schemas use their independently versioned namespace:
https://mdbase.dev/schemas/runtime/v0.1/
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.
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
Every collection-semantic selector uses one field-reference syntax. A field reference is either:
- the existing mdbase field-path form, such as
title,metadata.owner, orblocks[] - a non-root RFC 6901 JSON Pointer,
such as
/@type,/metadata/owner, or/schema/$id
Field paths remain supported for compatibility and for the [] array-item
selector. JSON Pointer is the exact, standards-based form for every JSON object
key, including keys that contain ., /, ~, @, or $. Pointer tokens
escape ~ as ~0 and / as ~1; for example /a~1b selects the key a/b.
The URI-fragment form beginning with # is not accepted.
The empty JSON Pointer denotes the whole document in RFC 6901, but it is not a valid mdbase field reference. Collection semantics always address a field inside the frontmatter object.
JSON Pointer resolves one exact value. Array tokens are zero-based indices.
[] expansion belongs only to the field-path form. An operation that accepts a
link collection applies its link rule to every item when the resolved value is
an array.
Lifecycle set creates missing intermediate objects. It MUST fail rather than
replace a non-object intermediate value. Assignment through an array index is
valid only when that array and index already exist.
Matching Decision Process
Matching uses the collection-relative record path, raw persisted frontmatter, the configured explicit type keys, and the loaded type registry.
For each record, an implementation follows this sequence:
- Inspect the configured explicit type keys in configuration order.
- If any configured key is present, read and validate its declarations, resolve those names against the type registry, and use the resulting explicit set.
- Otherwise, evaluate every type that has a
matchsection against the record. - Collect the matching types and order them by canonical lower-case name.
The explicit branch completes type selection. Inferred rules, including
match.expr, are skipped for that record. The selected types still perform
their normal schema and collection validation.
An explicit type value is a type-name string or a non-empty list of type-name strings. Declarations from several configured keys are concatenated in key order and case-insensitively de-duplicated while preserving the first occurrence. Invalid value shapes and unknown type names produce diagnostics.
The default keys are type and types. Configuring
settings.explicit_type_keys replaces that default list. An empty list selects
inferred matching for every record.
A type with no match section can be selected explicitly. It contributes no
inferred match.
Inferred Match Rules
All members present in one match object combine with AND:
match:
path_glob: "tasks/**/*.md"
fields_present: [title, "/@type"]
where:
status:
neq: done
This type matches a candidate only when its path, required fields, and
structured predicates all match. A list in path_glob combines its patterns
with OR. Every selector in fields_present MUST resolve to a raw, non-null
value.
Empty string, false, zero, and empty list count as present. A missing value or
explicit null does not.
Match rules read persisted frontmatter. Read defaults and projections are applied after type selection and therefore do not influence inferred matching.
Structured Predicates
match.where is the portable structured predicate language for basic matching.
A where mapping combines different field selectors with AND. A direct value
uses deep JSON equality. An operator mapping combines its operators with AND.
| Operator | Meaning |
|---|---|
eq, neq |
deep JSON equality or inequality |
gt, gte, lt, lte |
number, string, date, time, or date-time comparison |
contains |
string substring or array item equality |
containsAll, containsAny |
array contains all or any requested values |
startsWith, endsWith |
string prefix or suffix |
matches |
portable regular-expression match |
exists |
raw key presence, including an explicit null value |
Except for exists, an operator evaluates to false when its field is missing or
null. Ordering evaluates to false for incomparable values. neq also evaluates
to false for a missing field. A predicate with an operand of the wrong type
evaluates to false.
matches uses the same portable regular-expression subset as JSON Schema
pattern: Unicode-aware matching without backreferences or look-around. An
unsupported or invalid pattern is a type-file diagnostic.
CEL Matching
The CEL Match profile adds match.expr for inferred rules that need a portable
expression:
match:
path_glob: "tasks/**/*.md"
expr:
$expr: 'present.raw.tags && tags.exists(t, t == "task")'
The expression combines with the other members of match using AND. It receives
the matching context defined in Chapter 10 and MUST evaluate to boolean true for
the type to match.
Implementations compile match.expr when loading the type. Parse and type
errors invalidate the type definition. A per-record evaluation error reports a
diagnostic for that record and expression and yields a non-match. False and null
also yield a non-match.
A type containing match.expr requires the cel_match conformance profile.
Loading it under a claim that omits cel_match produces
unsupported_profile.
Read Defaults
collection.read_defaults supplies effective read and query values for missing
fields.
collection:
read_defaults:
status: open
recurrenceAnchor: scheduled
For each defaulted field:
- a missing raw field receives the configured effective value
- an explicit
nullremains null - JSON Schema
requiredcontinues to evaluate the raw frontmatter - raw-frontmatter presence remains false
- read and query operations leave the persisted file unchanged
Create and editor tooling MAY mirror static values into JSON Schema default
annotations for presentation or scaffolding.
Links
JSON Schema validates a link field's local shape.
collection.links supplies its collection-level meaning.
collection:
links:
parent:
target_type: task
validate_exists: true
blocks[]:
target_type: task
validate_exists: false
"/relations":
target_type: task
validate_exists: true
blocks[] applies the rule to every item in the blocks array. Chapter 08
defines link parsing and resolution. /relations applies the same item-wise
rule when the exactly selected value is an array.
Cross-File Uniqueness
collection.unique declares collection-level uniqueness:
collection:
unique:
- field: id
scope: type
| Scope | Comparison set |
|---|---|
collection |
every record in the collection |
type |
every record matching the declaring type |
path_glob |
every record under the configured path glob |
Missing and null values are exempt from uniqueness comparison.
Path Policy
Path policy guides create and rename operations:
collection:
path:
pattern: "tasks/{id}.md"
The portable grammar uses {field} placeholders for top-level frontmatter
fields. Values are converted to strings without expression evaluation. A
missing or null value produces path_value_missing.
Generated paths MUST remain inside the collection root. Placeholder values that
produce /, \\, ., or .. path components are invalid. Runtime-owned path
logic and richer template languages belong under an x-* extension.
Display Metadata
collection.display contains advisory presentation metadata:
collection:
display:
name_field: title
description_field: summary
icon: check-circle
color_field: status
Editors, collection views, and generated documentation can use these values. Record validation ignores display metadata.
Projections
Collection projections are an optional effective-value feature declared outside JSON Schema and expressed in CEL:
collection:
projections:
is_overdue:
expr: 'present.record.due && 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 remain available through
their declared field names. Query- and view-local named projections are
separate, live under the projection CEL namespace, and never replace a
collection projection or persisted field with the same name.
Private Domain Namespaces
Private annotations with no portable interoperability meaning use namespaced extension sections:
x-example-app:
fields:
status:
role: status
completed_values: [done, cancelled]
This keeps the JSON Schema reusable and gives each private extension an
explicit owner. Portable domain interfaces use the first-class data contracts
and type-local implements entries from Chapter 05A.
08. Links
Link Values
mdbase recognizes three link syntaxes:
| Syntax | Example |
|---|---|
| Wikilink | `[[people/alice |
| Markdown link | [Alice](people/alice.md) |
| Bare path | people/alice.md |
collection.links makes a field link-aware. JSON Schema validates its local
shape, usually string or an array of strings.
Link Components
Parsed links expose:
rawtargetaliasanchorformatis_relative
format is one of wikilink, markdown, or path.
Resolution
Resolution is performed against the collection root and the containing file.
General rules:
- Markdown links and bare relative paths resolve relative to the containing file's folder.
- Absolute collection paths use
/from the collection root. - Wikilinks with
/,./, or../use path-style resolution. - Simple wikilinks without path separators first try ID-based resolution using
the configured
id_field, then filename resolution.
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.
If filename resolution finds multiple candidates, tools SHOULD apply stable tiebreakers:
- same directory as referring file
- shortest collection path
- alphabetical path
If ambiguity remains, resolution returns null and reports an ambiguous link warning.
Target Constraints
collection.links can require a target type:
collection:
links:
assignee:
target_type: person
validate_exists: true
When validate_exists is true, unresolved links are validation errors at
validation level error.
When target_type is present, a resolved target is valid only if it matches the
target type.
Body Links
file.links includes:
- frontmatter link fields declared in
collection.links - body wikilinks
- body Markdown links
file.embeds includes Markdown and wikilink embeds.
Links and tags inside fenced code blocks and inline code MUST be ignored by body extraction.
Tags
file.tags includes:
- frontmatter tags from
tagswhen the field is string or list of strings - inline body tags beginning with
#
Inline tags must begin at the start of a line or after whitespace. URL fragments MUST NOT be treated as tags.
file.hasTag("project") uses complete tag-segment prefixes. It matches
#project and #project/alpha; its result for #projection is false.
Link Host Functions
The CEL profile defines host functions and methods for links:
link(value)file.hasLink(linkValue)file.hasTag(tag)file.asLink()linkValue.asFile()
Broken links resolve to null. Property access through null returns null under the mdbase CEL profile.
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 may run on:
on_createon_updateon_deleteon_rename
v0.3 core standardizes on_create and on_update. Other lifecycle hooks are
optional until a profile defines them.
Example
lifecycle:
on_create:
set:
id: { ulid: true }
dateCreated: { now: true }
dateModified: { now: true }
on_update:
set:
dateModified: { now: true }
Standard Value Providers
Core lifecycle providers:
| Provider | Meaning |
|---|---|
{ now: true } |
current timestamp |
{ today: true } |
current date |
{ uuid: true } |
random UUID |
{ ulid: true } |
random ULID |
{ slugify: fieldName } |
slugified value of another field |
{ copy: fieldName } |
copy another field |
{ literal: value } |
set a literal value |
Tools MUST document timestamp precision and timezone behavior.
Guards
Lifecycle actions MAY have CEL guards:
lifecycle:
on_update:
- if: 'old.status != status'
set:
dateModified: { now: true }
Lifecycle guards use the mdbase CEL profile with these bindings:
- current draft frontmatter fields
oldfor the previous raw frontmatter on updatefilefor file metadataoperationfor operation metadata
Validation Order
For mutating operations:
- parse input
- match target types
- build a draft frontmatter object
- apply lifecycle policy
- validate JSON Schema
- run collection validators
- write the file
Lifecycle MUST run before final validation so generated IDs and timestamps can satisfy required schema fields.
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
If multiple matched types define lifecycle policy for the same field and event, the operation MUST be deterministic.
Normative rule:
- identical normalized assignments are allowed and execute once
- conflicting assignments are
type_conflicterrors before write - diagnostics MUST report the type names and lifecycle paths involved
v0.3 core MUST report the conflict. Future pack composition may define explicit precedence.
A lifecycle guard that fails to compile or raises an evaluation error fails the
operation with lifecycle_expression_error. A guard that evaluates normally to
false or null skips that action.
10. CEL Profile
Purpose
Portable v0.3 expressions use CEL. This chapter defines the data available to an expression, mdbase host functions, missing-value behavior, limits, and error handling.
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 Workflow.
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.
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, present, file, note |
| query or projection | effective candidate fields at top level; record, raw, present, file, note; 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, present, old, file, operation |
| workflow variable, trigger, or workflow 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, present, file, note, 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. note is an alias for record.
present.raw.status == false && 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>,this.record.<field>, andthis.note.<field>expose effective context valuesthis.raw.<field>exposes persisted context frontmatterthis.present.record.<field>andthis.present.raw.<field>expose presencethis.fileexposes context-file metadata and the helpers available to the active profiles
When an effective context field conflicts with the reserved members record,
note, raw, present, 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,
note, and top-level field names therefore refer to the same raw frontmatter
object. present.record and present.raw contain the same values.
file.inFolder("tasks") && present.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 != 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
Chapter 14 defines when each workflow expression is evaluated.
Missing And Null
mdbase preserves four observable record states:
| Raw state | Effective state | present.raw |
present.record |
Top-level query value |
|---|---|---|---|---|
| missing, no default | missing | false |
false |
null |
| missing, read default | default value | false |
true |
default value |
| explicit null | null | true |
true |
null |
| persisted value | persisted value | true |
true |
persisted value |
Hosts MUST make missing record fields evaluate to null and presence entries
evaluate to false. present includes schema-known, persisted, and defaulted field
names so these checks do not depend on host map-access behavior.
Property access through null returns null. Type errors in query evaluation also yield null and produce an expression diagnostic; a query filter includes only a record whose condition evaluates to boolean true.
Portable frontmatter-presence checks use present.raw.<field>.
present.record.<field> tests effective presence. CEL's has() macro remains
available for objects whose host representation has ordinary CEL presence
semantics, such as event payload objects.
Date And Duration
The profile defines these host functions:
now()today()duration(string)
now() returns an RFC 3339 UTC date-time. today() returns the calendar date in
the runtime's declared timezone. The operation or runtime context MUST declare
that timezone. duration(string) accepts ISO 8601 durations in portable stored
expressions.
Timestamp comparison uses the represented instant. Date comparison uses the calendar date. Date, time, timestamp, and duration values have stable serialization across reads and query results.
File And Link Helpers
Core Read supplies file metadata and:
file.inFolder(path)
The Links profile adds:
link(value)file.hasTag(tag)file.hasLink(linkValue)file.asLink()linkValue.asFile()
asFile() returns null for an unresolved link. 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 and type errors invalidate that containing object.
Evaluation outcomes depend on context:
| Context | Evaluation error |
|---|---|
| inferred match | report a diagnostic and treat the candidate as a non-match |
| query filter or projection | yield null for that record and report a diagnostic |
| lifecycle guard | fail the write operation with lifecycle_expression_error |
| workflow trigger or workflow condition | create a failed-run diagnostic |
| workflow step or run policy | fail the step or run with a runtime diagnostic |
A condition that evaluates normally to false or null does not match or execute.
Diagnostics SHOULD include the source, code, message, context name, and line, column, or byte range when available. Parse/type diagnostics and runtime evaluation diagnostics use distinct codes.
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]
context:
this:
path: projects/alpha.md
projections:
is_overdue:
expr: 'present.record.due && 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.
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, 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: 'present.record.due && 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 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.
Evaluation errors MUST produce null for that record, exclude it, and report a diagnostic according to query options.
Selection
Queries MAY request a logical result shape:
select:
- title
- file.path
- projection.is_overdue
- name: display_due
expr: 'due == null ? "Unscheduled" : string(due)'
label: Due
A string selects an effective field, file value, or named projection. An object
defines a named CEL result value. Selection expressions run after filtering and
may use the candidate, named projections, and this.
For a string selector, the output name is the effective-field name or the final
member of a file.<name> or projection.<name> selector. An object uses its
required name. Two selections that produce the same output name make the
query invalid; callers use an object selection to assign an unambiguous name.
Computed selection values belong to the query result. Persistence requires an
explicit write operation. Every result includes file.path in its file
object even when file.path is not selected.
Ordering
order_by sorts by one or more effective fields, file values, named
projections, or named selection values.
order_by:
- field: due
direction: asc
- field: projection.urgency
direction: desc
Null values sort last in ascending order and first in descending order.
When all ordering fields compare equal, tools MUST tie-break by ascending
file.path for deterministic results.
Grouping And Summaries
Queries MAY group the complete filtered and ordered result set by one or more values:
group_by:
- field: status
direction: asc
Each distinct ordered tuple of grouping values creates one group. Null and
missing values form the same null group. Group tuple ordering applies the same
direction and null rules as order_by. Results within a group retain query
order.
Queries MAY define custom summary functions and apply built-in or custom functions to values:
summary_functions:
completion_rate:
expr: 'values.size() == 0 ? 0 : values.filter(v, v).size() * 100 / values.size()'
summaries:
- field: estimate
function: sum
name: total_estimate
- field: projection.is_completed
function: completion_rate
name: completion_rate
A custom summary expression receives values, an ordered list containing one
value per matching result in result order. Missing fields contribute null.
When grouping is active, the function is evaluated independently for each
group; otherwise it is evaluated once for the complete match set.
A summary's result key is its explicit name, or its function identifier when
name is omitted. Duplicate result keys make the query invalid.
The portable built-in summary identifiers are count, sum, average,
minimum, maximum, earliest, latest, empty, and filled. count
counts all values. empty and filled count empty and non-empty values using
the CEL missing/null contract. Other built-ins ignore null values and produce
null when no compatible non-null value exists. Incompatible non-null values
produce a summary diagnostic and a null summary result.
Grouping and summaries are calculated before pagination and therefore describe
the complete filtered match set. Group metadata does not replace the flat
results page.
Pagination
limit and offset apply after filtering and sorting.
limit: 0 returns an empty result page. Total count, grouping, and summary
metadata still describe the complete match set.
Body Search
file.body MAY be used in filters even when include_body is false. In that
case the body is available for filtering but not returned in results.
Tools MAY report that body filtering requires a profile or index when they cannot read bodies on demand.
Frontmatter Mode
frontmatter_mode controls which fixed-semantics frontmatter members appear in
each result:
effective(the default) returnseffective_frontmatterpersistedreturnsfrontmatterbothreturns bothfrontmatterandeffective_frontmatter
The mode changes result serialization only. Filtering, projections, selection, ordering, grouping, and summaries continue to use effective values unless an expression explicitly reads the persisted namespace. Unlike a complete record document returned by Core Read or a mutation, a query result is a summary and MAY omit the member excluded by the requested mode.
Result Envelope
Query results MUST use this envelope:
results:
- file:
path: tasks/fix-login.md
effective_frontmatter:
title: Fix login
status: open
values:
title: Fix login
is_overdue: true
meta:
total_count: 1
has_more: false
context:
path: projects/alpha.md
groups:
- values:
status: open
count: 1
summaries:
total_estimate: 30
diagnostics: []
Each result MUST include file.path. meta.total_count is the count before
pagination, and meta.has_more is true when additional matching records remain.
Diagnostics use the canonical diagnostic envelope from the conformance chapter.
Read defaults are included in effective_frontmatter. values contains
requested selection values and is omitted when select is omitted.
meta.context.path identifies the bound invocation context and is omitted when
this is null. meta.groups is present only when grouping or summaries were
requested. Without grouping, summaries appear in one group whose values is an
empty object.
Saved View Records
A saved view is an ordinary Markdown record matched by the view type. The
canonical query object above supplies its execution semantics, and the view
record provides a portable persisted container. The canonical record schema is
schemas/v0.3/view.schema.json; a collection can materialize the corresponding
_types/view.md type file.
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 < 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.
To derive an executable query:
- inherit
query.typesunless the named view suppliestypes - combine
query.whereand named-viewwherewith AND - merge
query.projectionsand named-view projections; the same name may appear in both only when its parsed definition is structurally equal, ignoring mapping key order - inherit
query.contextunless the named view supplies context - copy the named view's selection, ordering, grouping, summaries, pagination, and body options
- bind the invocation context using the rules below
Record-level property metadata and summary functions remain available to every
named view. Resolving an unknown view record or named-view ID produces
view_not_found.
View execution returns the query envelope and adds
meta.view: { path, id }, using the resolved view-record path and named-view
ID.
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. Chapter 15 defines the adapter contract for Obsidian Bases sources.
Optional support
Core Read implementations treat a canonical view file 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.
12. Operations
Operation Model
Write-capable tools operate on Markdown files while preserving the collection contract.
Core operations:
- read
- create
- update
- delete
- rename
- batch
Optional saved-view operations:
- list_views
- execute_view
- read_view_source
- create_view_source
- update_view_source
- delete_view_source
Read
Read returns a record by path, including:
- persisted
frontmatter - derived
effective_frontmatter - body
- file metadata
- matched type names
- diagnostics
Read MUST NOT write defaults or lifecycle values to disk. Every successful read returns the complete record document defined in Chapter 03.
Read accepts optional include_document. When true, the returned record MUST
include the exact document source defined in Chapter 03. Providers that
cannot supply exact source MUST reject the request rather than silently return
a reconstructed serialization.
Create
Create builds a new record.
Pipeline:
- determine target type or types from explicit input or path/match policy
- build draft frontmatter
- freeze pre-lifecycle type membership
- apply lifecycle
on_create - verify type membership did not change as a lifecycle side effect
- validate JSON Schema
- run collection validators
- choose or validate path policy
- write Markdown file
- update derived indexes
- emit watch/runtime events after state is consistent
Static JSON Schema defaults MAY be used by editor and create interfaces. Validation-time mutation occurs when the create operation explicitly copies a default into the draft.
Update
Update modifies an existing record.
Pipeline:
- read existing raw frontmatter
- apply the requested patch
- re-match and freeze types when type-affecting fields or path changed
- apply lifecycle
on_update - verify type membership did not change as a lifecycle side effect
- validate JSON Schema
- run collection validators
- write frontmatter and preserve body
- update derived indexes
- emit watch/runtime events
If a patch sets a field to missing, the key is removed. If a patch sets a field to null, the key is persisted as null unless the operation policy says null means remove.
As an alternative to a frontmatter patch and body replacement, Update accepts
document containing the complete candidate Markdown source. A document
replacement MUST NOT be combined with patch, fields, frontmatter, 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.
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.
Atomic reference updates across all affected files belong to a transaction profile.
Batch
Batch operations group operations for validation and reporting.
Recommended behavior:
- validate every operation before writing unless
allow_partialis true - support dry-run with full diagnostics
- report per-operation result and diagnostics
- stop on first error unless configured otherwise
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. Durable Runtime Companion Profile
Purpose
The durable runtime profile turns passive, portable definitions into recoverable automation. It is deliberately a companion to two simpler specifications:
- the core mdbase contract system defines contract identity, semantic versions, JSON Schemas, digests, record implementations, resolution, and packs;
- the event/action interoperability profile defines CloudEvents, action requests and outcomes, event-source and action-provider declarations, provider selection, and baseline exchange behavior;
- this profile adds durable admission, authorization, execution state, retries, timers, cancellation, and recovery.
The separation is practical. An application can consume portable records or exchange events and actions without implementing a workflow engine.
This chapter defines runtime companion profile 0.2. The profile is versioned
independently from mdbase collection specification 0.3.0.
The Short Version
contract artifact what an event, action, or record means
type implements how a Markdown record stores a record contract
source/provider declares how live code implements an event/action contract
runtime policy whether that exact code may act here
admitted plan exact contract digests and implementations for one run
durable runtime safely executes and recovers that admitted plan
Installing a contract, type, or pack never runs code and grants no authority. Markdown provider-registration records are audit evidence only. A host must verify and admit a live interoperability declaration before code is eligible.
No Second Contract System
Runtime profile 0.2 has no runtime-specific contract record, registry, provider-owned contract definition, event envelope, or contract mode.
- Event and action interfaces are ordinary
mdbase.contractartifacts. - Runtime state is stored in ordinary record types that implement standard record contracts.
- Live event sources and action providers use interoperability declarations.
- Runtime indexes are derived caches of those verified registries.
- Bundled artifacts, installed artifacts, and exported copies have identical identity and conflict semantics.
Private x-* metadata, filenames, and folders have no discovery,
authorization, or activation meaning.
The superseded type: action, type: event, type: provider,
contract_version, schemas.payload, runtime.contract_mode, and
provider-supplied contract arrays are not runtime profile 0.2.
Standard Runtime Pack
The official mdbase.runtime.standard pack is at
`standard-packs/mdbase-runtime/0.2.0`.
It installs atomically and contains:
| Record contract | Canonical record type | Purpose |
|---|---|---|
mdbase.runtime.workflow |
runtime_workflow |
portable workflow definition |
mdbase.runtime.policy |
runtime_policy |
local authority and provider selection |
mdbase.runtime.provider-registration |
runtime_provider_registration |
optional declaration audit snapshot |
mdbase.runtime.capability-grant |
runtime_capability_grant |
optional materialized grant |
mdbase.runtime.run |
runtime_run |
durable run and admitted plan |
mdbase.runtime.action-attempt |
runtime_action_attempt |
invocation intent and outcome evidence |
mdbase.runtime.checkpoint |
runtime_checkpoint |
durable continuation |
mdbase.runtime.timer |
runtime_timer |
generation-safe one-shot timer |
mdbase.runtime.diagnostic |
runtime_diagnostic |
machine-readable runtime issue |
mdbase.runtime.dead-letter |
runtime_dead_letter |
retained unprocessable evidence |
It also contains inspectable event/action artifacts for the four canonical
record-change events (mdbase.record.created, mdbase.record.modified,
mdbase.record.deleted, and mdbase.record.renamed),
mdbase.runtime.timer.fired, and mdbase.runtime.run.cancel. The event data
schemas make a contract definition portable across every source and consuming
application; a source declaration says only who publishes an exact artifact.
These artifacts are a standard library, not privileged built-ins. A host may bundle the pack for offline use without copying it into each collection. Exporting a bundled artifact is an inspection feature; it does not create a new definition or alter precedence.
Runtime Records Are Ordinary Implementations
The canonical workflow type illustrates the model:
kind: mdbase.type
name: runtime_workflow
version: 1
match:
where:
type: runtime_workflow
schema:
dialect: json-schema-2020-12
ref: ../schemas/mdbase.runtime.workflow/1.0.0.schema.json
implements:
- contract: mdbase.runtime.workflow
version: 1.0.0
fields:
type: type
id: id
version: version
name: name
enabled: enabled
triggers: triggers
steps: steps
The core collection engine discovers and validates this exactly like any other
record contract. A durable runtime asks the core engine for the verified
descriptor and projected view. It does not scan for workflows/, special-case
the filename, or reconstruct a separate registry.
Internal database rows and indexes do not need Markdown record contracts. Materialize runtime state when it is intended to be portable, inspectable, exportable, or shared through mdbase. Private storage tables remain implementation details but must preserve the same protocol evidence.
Event Sources And Action Providers
Live implementations use the interoperability profile unchanged.
An event source declaration binds a verified implementation identity to one or more exact event artifacts. An action provider declaration binds handler IDs to exact action artifacts. Declarations include their own canonical digest.
A provider does not return an action or event definition from executable code. Registration supplies the already-resolved artifact to the interoperability bridge, which validates the implementation binding and publishes a declaration.
Several sources may publish one event contract. Several providers may implement one action contract. Events remain multicast. Actions select exactly one provider:
- an explicit provider selector in the workflow wins;
- otherwise a single matching runtime-policy selection may apply;
- otherwise one eligible provider is unambiguous;
- zero providers fails with
no_provider; - multiple providers fail with
ambiguous_provider.
Executable providers are never unioned.
Authorization Is Separate
Conformance answers “can this implementation speak this interface?” Authorization answers “may it act here, for this caller, on this resource?”
A host checks, independently:
- selected and enabled runtime policy;
- caller and executor identity;
- named capabilities;
- collection and record scope;
- action contract and exact provider implementation;
- operational limits;
- required user approval;
- transport and host authority.
Deny takes precedence. The standard pack ships no permissive policy. A contract, type, workflow, provider declaration, or audit record is passive until a host admits it under current authority.
Workflow Requirements
Authors use a contract ID and SemVer requirement:
type: runtime_workflow
id: canvas.zone.set-status
version: 1.0.0
name: Set status from canvas zone
enabled: true
triggers:
- id: drop
event:
id: canvas.drop
version: ^1.0.0
steps:
- id: patch-status
action:
id: mdbase.record.patch
version: ^2.0.0
requires:
capabilities: [mdbase.record.write]
input:
path:
$expr: event.data.file.path
patch:
status:
$expr: event.data.zone.id
The readable workflow does not contain a digest for every normal dependency. An author may add a digest to a requirement when deliberately pinning it.
Workflows normally depend on behavior, not an application name. Add a provider selector only when the workflow intentionally needs a provider-specific implementation:
provider:
application: tasknotes
implementation: tasknotes-v5
Preflight And Admission
Preflight is derived and repeatable. It:
- validates the workflow through
mdbase.runtime.workflow; - checks unique trigger and step IDs and compiles expressions;
- resolves each contract requirement through the core contract registry;
- uses verified interoperability declarations to find implementations;
- applies explicit or policy provider selection;
- evaluates capability and operational policy;
- reports every missing, conflicting, ambiguous, or denied dependency.
Preflight does not make an execution durable.
Admission occurs for one validated event delivery. In one durable transaction the authority:
- journals and deduplicates the CloudEvent;
- identifies the exact event-source declaration;
- evaluates trigger and admission policy;
- resolves every action provider;
- pins the workflow revision;
- pins exact event/action contract ID, version, and digest;
- pins exact source/provider identities and declaration digests;
- records handler IDs, idempotency decisions, and deadlines;
- reserves the run idempotency key and concurrency position;
- writes the run and immutable admitted plan.
Registry changes after this transaction do not alter the plan. Changing an admitted dependency requires explicit migration or re-admission.
Shared Envelopes
Runtime event intake is a structured CloudEvents 1.0 envelope from the interoperability profile. Runtime action dispatch persists and exchanges the standard:
mdbase.action.request;mdbase.action.invocation;mdbase.action.outcome;mdbase.action.cancel.
The runtime does not wrap these in another envelope or rename data to
payload. Runtime-specific run, attempt, cursor, and lease records refer to
the shared envelope evidence.
Runtime Configuration
The core mdbase.yaml schema does not define runtime.contract_mode.
Runtime enablement, storage, worker identity, policy selection, and transport
binding are host configuration. A host may store portable policy records in the
collection, but loading the collection remains safe and does not activate them.
Conformance Boundary
A conformant core engine supports contract artifacts, type implementations, record projection, conflict detection, and packs. It need not execute workflows.
A conformant interoperability implementation supports its claimed event/action roles. It need not persist runs.
A conformant durable runtime supports admission and the execution/recovery rules in this chapter and Chapter 14. It uses core and interoperability evidence rather than publishing a second contract model.
14. Durable Workflow Execution
Purpose
Chapter 13 defines the companion-profile boundary, standard records, policy, and admission. This chapter 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 in Chapter 11. Runtime activation exposes the shared CloudEvent as
event; portable payload values therefore appear below event.data.
- variables are evaluated once in dependency order;
- cycles fail preflight;
- trigger and step conditions must evaluate to boolean true to continue;
- object values are literals unless they have exactly one
$exprkey; - steps execute in document order;
for_eachvisits list items in list order;- evaluated action input is validated by the interoperability bridge before provider invocation;
- action output or declared error data is validated before it becomes durable step state.
Every execution limit is the stricter of the workflow and current policy.
Durable Event Admission
Event delivery may be repeated. The data authority therefore commits the following atomically:
- validated CloudEvent and exact source evidence;
- duplicate/tombstone state;
- monotonically increasing delivery cursor;
- every run admitted from the event;
- run idempotency reservations;
- concurrency queue changes;
- immutable admitted plans.
A crash cannot expose the event without its derived runs or a run without its event. Retrying the same event ID and contract/source evidence returns the original cursor and does not create another logical run. Reuse of an event ID with different canonical content fails closed.
Pruning may remove event bodies after the documented retention period but
keeps sufficient tombstone evidence through the replay horizon. Reading from a
cursor older than retained history reports cursor_expired and the earliest
available cursor; it never silently skips the gap.
Run State Machine
Portable run states and transitions are:
| From | To | Cause |
|---|---|---|
| admitted | queued |
concurrency delays execution |
admitted or queued |
running |
worker obtains current lease |
running |
waiting |
durable checkpoint suspends execution |
waiting |
queued |
checkpoint becomes ready |
running |
succeeded |
all required steps complete |
running |
failed |
deterministic failure or deadline |
queued, running, waiting |
cancelled |
cancellation completes without ambiguous effects |
running |
indeterminate |
non-replayable action outcome is unknown |
succeeded, failed, cancelled, and indeterminate are terminal. A host
rejects every transition from a terminal state.
Leases
A worker claims a run with a bounded lease containing owner, opaque token, and expiry. Every state-changing write compares the current lease token.
- workers renew before expiry;
- an expired lease may be recovered by another worker;
- a stale worker cannot commit a step, checkpoint, or terminal state;
- handler execution is never treated as proof that its outcome was committed.
Lease duration and renewal strategy are host configuration. The observable stale-write rejection is portable.
Action Attempts
Before invoking a provider, the runtime durably writes an action-attempt intent containing the shared request ID, invocation ID, attempt ID, exact contract, provider identity, provider-declaration digest, handler ID, idempotency key, deadline, and input.
It then sends the ordinary interoperability request. The bridge produces the ordinary invocation and outcome evidence. The runtime stores those envelopes without redefining them.
Attempt states are:
admitted → dispatching → succeeded
↘ rejected
↘ failed
↘ cancelled
↘ indeterminate
Only an outcome with matching request, invocation, attempt, contract, and provider evidence completes the attempt.
Idempotency And Crash Recovery
The workflow run has an idempotency key. If the author does not supply one, the host derives:
workflow.id + ":" + event.id + ":" + trigger.id
The key is reserved during admission, before any dispatch.
Action replay follows the action artifact and provider declaration:
- for request-idempotent actions, recovery reuses the same request and invocation identity as required by the binding and obtains the retained outcome or safely retries;
- for non-idempotent actions, a crash after dispatch but before durable outcome
creates
indeterminate; the runtime does not guess or replay; - conflicting reuse of a request or idempotency key fails closed;
- an action marked as requiring idempotency is ineligible when the provider or transport cannot supply it.
Exactly-once external side effects are not promised. The profile provides durable intent, duplicate suppression, explicit replay rules, and honest indeterminate outcomes.
Concurrency
The optional workflow concurrency policy applies to an evaluated group:
| Policy | New run while group is active |
|---|---|
skip |
record the admission decision and do not execute |
queue |
preserve event-cursor order |
replace |
request cancellation, then wait for a terminal state |
allow |
run concurrently |
When no group expression is supplied, the workflow ID is the group. Completed
effects are never rolled back by replace. A replacement waits when the active
action cannot be cancelled safely.
Cancellation And Deadlines
Cancellation is cooperative and uses the interoperability cancellation envelope. The runtime records the request before delivery.
- not-yet-started attempts are not dispatched;
- cooperative providers receive cancellation through the bridge;
- a confirmed cancelled outcome permits
cancelled; - a completed action outcome wins a race with cancellation;
- loss of a non-idempotent outcome becomes
indeterminate; - terminal runs ignore repeated cancellation requests idempotently.
A run deadline prevents all new dispatches after expiry. A deterministic
deadline failure becomes failed; an already-dispatched action whose outcome
cannot be known follows the indeterminate rule.
Checkpoints
A checkpoint stores enough state to resume deterministically:
- run ID and current generation;
- next step/item position;
- immutable variables and completed step outputs;
- pending request/invocation evidence;
- wait condition;
- current workflow/admitted-plan revision;
- lease-safe updated time.
Checkpoint generation only increases. A stale generation or stale lease write is rejected. Completion is recorded before the worker advances.
Timers
A durable timer is generation-safe and one-shot.
- scheduling writes generation
n, fire time, and an event contract requirement plus event data; - rescheduling writes generation
n + 1and makes older generations stale; - a worker atomically claims the current due generation;
- firing publishes
mdbase.runtime.timer.firedor the requested event through an interoperability event source; - the resulting CloudEvent is admitted through the ordinary event journal;
- the timer becomes
firedonly with matching event evidence.
After restart, an overdue current generation fires once under
missed_run_policy: fire_once. Polling or racing workers cannot create two
logical events.
Policy Rechecks
Admission pins compatibility and selection. Authority is checked again immediately before each dispatch because grants, scopes, or approval may have changed.
A new denial prevents dispatch and fails the attempt with
capability_denied. A policy change does not substitute another provider or
contract in an admitted plan. That requires re-admission.
Diagnostics And Dead Letters
Protocol failures use stable codes and durable evidence. Important examples include:
unknown_contract;contract_digest_conflict;event_source_unavailable;no_provider;ambiguous_provider;capability_denied;idempotency_unavailable;invalid_run_transition;stale_lease;cursor_expired;outcome_indeterminate.
Unprocessable event/request/outcome evidence may be retained as
mdbase.runtime.dead-letter. A dead-letter record is passive. Requeue is an
explicit authorized operation that creates new admission evidence.
Minimum Durable Conformance
A runtime claiming profile 0.2 must demonstrate:
- ordinary record-contract validation of runtime records;
- exact contract/source/provider pinning through interoperability declarations;
- authorization separate from conformance;
- atomic event/run admission and duplicate suppression;
- legal run and attempt transitions;
- stale-lease rejection and crash recovery;
- idempotent versus non-idempotent recovery;
- generation-safe timers;
- cursor expiry behavior;
- cancellation and deadline races;
- machine-readable diagnostics.
The reference TypeScript executor demonstrates the boundary and shared exchange model but is intentionally non-durable. Durable conformance belongs to hosts such as the Rust runtime and Connect binding.
15. Migrations And Compatibility
Migration Philosophy
v0.3 uses the source model defined by Chapters 01–14. 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.
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.x generated | v0.3 lifecycle |
|---|---|
now |
on_create.set.field: { now: true } |
now_on_write |
on_update.set.field: { now: true } |
uuid |
{ uuid: true } |
ulid |
{ ulid: true } |
slugify from field |
{ slugify: field } |
Computed Fields
Computed fields migrate to one of:
- query projection suggestion
collection.projectionswhen the target tool supports it- workflow/runtime policy when the computed value should be materialized
- unsupported note when the computation has side effects or depends on non-portable functions
Expressions
Current mdbase expressions migrate to CEL where possible.
Tool-specific expression dialects are adapter concerns. Tools may translate them to CEL for portable storage and translate them back for user interfaces or exports.
Obsidian Bases Views
Obsidian .base files are external saved-view sources. A collection provider
discovers configured sources and exposes them through the saved-view operations
in Chapter 12. 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 glob patterns. 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 Chapter 12. 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.
Runtime Workflows
Current generated-field and tool-conforming behavior that causes mutation should be reviewed as lifecycle or workflow behavior.
Generated IDs and timestamps usually become lifecycle.
Cross-record behavior, agent work, approval flows, external APIs, and scheduled checks become workflows and action/event contracts.
Data Contract Implementations
Portable application interfaces migrate to a local data contract plus a
type-local implements entry.
Example:
implements:
- contract: example.task
version: 1.0.0
fields:
status: status
binding:
completed_values: [done, cancelled]
The migration bundle MUST include the exact mdbase.contract artifact required
by the implementation. Existing convention-based x-* objects do not become
contracts merely because they contain keys named contract or version.
Private annotations with no portable contract meaning remain under a
namespaced x-* section. They SHOULD NOT become JSON Schema custom keywords.
Version Detection
A v0.2.x type file usually has name and fields without kind: mdbase.type.
A v0.3 type file has kind: mdbase.type and schema.
Migration tooling SHOULD refuse ambiguous files unless the user supplies an explicit source version.
Safe Migration Protocol
A conforming migration command has separate analyze and apply phases:
- discover source config, type files, extension metadata, and records
- generate proposed config/types without modifying the collection
- validate generated schemas and type files
- validate all existing records against the proposed target
- emit a human-readable diff and machine-readable report
- require explicit approval unless every target is recognized as generated
- create a backup manifest containing hashes and original paths
- write through temporary files and atomic renames where supported
- re-open and validate the migrated collection
Apply MUST abort before writes when analysis has unsupported features or invalid target records unless the caller explicitly selects a documented partial mode. A failed apply restores files from the backup manifest when restoration is possible and reports any paths requiring manual recovery.
Apply SHOULD durably journal the currently attempted path and completed paths inside the backup before each replacement. Tooling MUST provide a recovery path that can resume or restore an interrupted apply. Restoration MUST validate the backup hashes from the manifest before replacing current files and MUST require explicit approval when invoked as a separate command.
Repeated analysis of unchanged inputs MUST produce the same report. Re-applying an already migrated type MUST fail without writing.
Unknown source metadata is preserved under x-legacy-v0.2 with its original
key paths unless a named migration adapter handles it. YAML comments and style
preservation are best effort. Markdown bodies MUST be preserved byte for byte.
The report includes source and target hashes, generated-file evidence, exact mappings, warnings, unsupported constructs, invalid record paths, proposed file operations, backup location, and post-apply validation status.
16. 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 and data contracts, and validate JSON Schema |
| Collection Semantics | apply defaults, uniqueness, path policy, multi-type composition, and collection diagnostics |
| 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 |
| 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 |
cel |
none |
cel_match |
core_read, cel |
cel_query |
core_read, cel |
links |
collection_semantics, cel |
core_write |
collection_semantics |
lifecycle |
core_write, cel |
event_action_interop/0.1 |
none |
runtime/0.2 |
core_read, event_action_interop/0.1, cel |
watch |
core_read |
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 unsupported_profile, 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, and
data_contract_record_invalid. 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
- load exact-version data contracts and validate type
implementsentries - expose deterministic contract and implementation digests
- project and validate contract views when a record is accessed through an implementation
- 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
Collection Semantics Requirements
Collection Semantics implementations MUST:
- apply
collection.read_defaultsto effective reads - preserve missing, null, raw, and effective distinctions
- validate every
collection.uniquerule in its declared scope - validate portable
collection.pathpolicies for write-capable tools - expose display metadata as advisory values
- compose compatible behavior from multiple matched types
- report
type_conflictfor incompatible matched behavior
CEL Requirements
CEL implementations MUST:
- compile and evaluate portable CEL source used by a claimed feature context
- supply the system bindings defined for that context in Chapter 10
- preserve the mdbase missing, null, raw, and effective-value contract
- provide
now(),today(), andduration()with declared timezone behavior - enforce and report expression, evaluation, and traversal limits
- distinguish compilation diagnostics from evaluation diagnostics
The CEL profile supplies the shared language capability. Each embedding profile defines its context and operational outcome.
CEL Match Requirements
CEL Match implementations MUST:
- compile
match.exprwhile loading its type definition - evaluate it against raw candidate frontmatter and file metadata
- combine it with other members of
matchusing AND - match only a boolean true result
- report per-record evaluation errors and treat that candidate as a non-match
Query Requirements
Query implementations MUST:
- validate portable query objects against the canonical query schema
- resolve and snapshot an optional same-collection invocation context
- expose the complete
thiscontext contract, binding it to null when absent - evaluate named query projections in dependency order before filtering
- reject cyclic projection dependencies and duplicate result names with
invalid_query - evaluate
wherefilters against the effective query context - evaluate requested CEL projections
- support OR-based type filtering
- support deterministic ordering and pagination
- support deterministic grouping and built-in and custom summaries
- return total-count and has-more metadata
- return context, grouping, and summary metadata when requested
- expose raw and effective frontmatter when requested
- report per-record evaluation errors and continue evaluating remaining records
View Record Optional Feature
An implementation advertises view_records through optional_features when it:
- validates view frontmatter against the canonical view schema
- lists view records with stable source and named-view descriptors
- resolves a stable view-record ID or path plus a stable named-view ID
- rejects duplicate named-view IDs with
invalid_view - derives the executable query using the inheritance and merge rules in Chapter 11
- exposes selected result properties in display order with their metadata
- applies
context.this.on_missingand context type constraints before query execution - reports the selected view and resolved context in query result metadata
- applies presentation metadata as advisory input while preserving canonical headless results
- keeps alternate dialect and renderer-specific data under
x-*extensions
A tool MAY advertise supported presentation identifiers separately in
optional_features.
An implementation advertises obsidian_bases_views through
optional_features when it:
- discovers
.basesources selected byx-obsidian.bases.include - assigns deterministic named-view IDs and source revisions
- parses filters and formulas before candidate evaluation
- evaluates the Obsidian Bases expression dialect, including formula dependencies, file and link values, date and duration behavior, methods, and coercions
- combines source and named-view filters with AND
- applies source order, sort, group, limit, and presentation metadata
- exposes the source's ordered properties and display names
- returns the saved-view headless result envelope
- keeps
.basesources authoritative throughout discovery and execution
An implementation advertises writable_view_sources through
optional_features when it:
- marks only writable source formats with
source.writable: true - reads complete source documents with stable opaque revisions
- validates complete candidate documents before create or update
- creates sources without replacing an existing path
- applies
if_revisionto update and delete - writes source replacements atomically
- preserves source-format extension data supplied by the caller
- makes successful mutations visible to subsequent list and execute operations
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.
Links Requirements
Links implementations MUST:
- parse wikilinks, Markdown links, and bare path link values
- resolve collection-relative and file-relative paths safely
- enforce
collection.links.target_typeandvalidate_exists - expose
file.links,file.embeds, andfile.tags - provide the CEL link helpers from Chapter 10
- bound
asFile()traversal
Core Write Requirements
Core Write implementations MUST:
- validate a complete draft before writing
- preserve unrelated Markdown body content where possible
- reject paths that escape the collection root
- enforce
if_revisionand report common concurrency conflicts - return the canonical operation envelope and final record revision
- update derived state before reporting a successful mutation
Lifecycle Requirements
Lifecycle implementations MUST:
- support
on_createandon_update - support
now,today,uuid,ulid,slugify,copy, andliteralvalue providers - 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
Runtime Contracts Requirements
Runtime Contracts implementations MUST:
- load provider, action, event, capability, policy, and workflow contract records
- represent implicit contracts supplied by a runtime
- compose the effective registry deterministically
- validate strict contract shapes and
x-*extension names - validate embedded action and event JSON Schemas
- validate event envelopes, payloads, action inputs, and action outputs
- resolve workflow event, action, capability, and provider references
- reject duplicate trigger and step IDs within a workflow
- resolve provider listings and runtime-policy workflow and capability selectors
- fail preflight for denied required capabilities
- fail preflight with
executor_not_selectedwhen policy selects no executor - include capabilities supplied by providers, actions, and policy in the effective capability set
- materialize implicit contracts when claiming materialization support
This profile covers registry composition, contract validation, authorization preflight, and materialization. Workflow execution has its own profile.
Workflow Requirements
Workflow implementations MUST:
- follow the preflight and event-to-run sequence in Chapter 14
- atomically journal, deduplicate, and admit delivered events
- apply trigger debounce and minimum-interval admission
- evaluate workflow variables and detect dependency cycles
- evaluate trigger, workflow, and step conditions in their defined contexts
- evaluate step inputs and iteration in deterministic order
- validate evaluated inputs before dispatch
- perform dispatch-time capability authorization
- apply execution mode and runtime-policy executor selection
- reserve idempotency keys with the required executor scope
- apply concurrency policy, run limits, and
on_error - pin canonical workflow, registry, action, and policy revisions
- claim work with bounded leases and reject stale lease writes
- persist stable invocation IDs and dispatch intents before provider calls
- recover idempotent attempts and mark ambiguous non-idempotent attempts
indeterminate - record standard run, step, attempt, receipt, and checkpoint results
- validate action outputs and emitted events
- commit action results and emitted-event admissions atomically
- provide generation-safe one-shot timer upsert, cancel, fire, and missed-run behavior when claiming canonical timer support
- report unsupported actions, capabilities, and unsafe execution state through runtime diagnostics
Action handlers are runtime-specific contracts in the effective registry. A
claim lists its tested handler set under optional_features or associated
evidence.
Watch Requirements
A watch notification MUST identify a change kind, unique event ID, observation time, and affected collection-relative path or contract identity. A record notification has this portable shape:
kind: record_modified
id: watch_01J...
observed_at: "2026-07-19T10:30:00Z"
path: tasks/example.md
changed_fields: [status]
frontmatter:
type: task
title: Example
status: done
Record notifications use these change kinds:
record_createdrecord_modifiedrecord_deletedrecord_renamed
Config, type, and runtime-aware implementations also use config_changed,
type_changed, and runtime_registry_changed as applicable. 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, and type 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
An implementation that also claims Runtime Contracts reports effective registry
changes after registry recomposition through runtime_registry_changed.