Skip to content

Schema Guide

Meridiem apps work best when the source documents stay pleasant to read. A good schema should help your app without turning a markwhen document into a database dump.

Use this guide when deciding what belongs in the header, what belongs on an event, and what should remain plain prose.

The short version

Put data hereWhen it describes
HeaderThe whole document, app settings, defaults, display metadata
Event propertiesOne dated entry, task, post, update, observation, or record
Event descriptionHuman-readable detail about that entry
Markdown documentA long-form page, essay, note, or reference that is not naturally event-shaped
External systemLarge binary data, private secrets, high-volume logs, or data users should not edit directly

If a human opens the document in Meridiem, they should be able to understand what your app is doing.

Namespacing

Put app-specific header data under a namespace. Use a short lowercase key that belongs to your app.

mw
---
mealplanner:
  enabled: true
  defaultMeal: dinner
  shoppingList: true
timezone: America/Los_Angeles
---

Do not put lots of unrelated top-level keys in the header:

mw
---
enabled: true
defaultMeal: dinner
shoppingList: true
---

Namespacing makes it possible for multiple apps to use the same document.

Header fields

Header fields are best for document-level state:

mw
---
publisher:
  site: rob-notes
  title: Rob's notes
  theme: simple
  defaultStatus: draft
timezone: America/Los_Angeles
---

Good header fields:

  • Are stable across many entries.
  • Describe how your app should treat the whole document.
  • Are safe for users to edit by hand.
  • Have clear defaults when missing.

Use header fields for:

  • App opt-in: enabled, view, sync.
  • Display: title, description, avatar, theme.
  • Defaults: defaultStatus, defaultProject, timezone.
  • Integrations: webhookEnabled, externalProjectId.

Event properties

Event properties are best for structured data about one entry:

mw
2026-08-04: Call with Acme
  type: call
  account: acme
  sentiment: positive
  next: send proposal
 
They want a smaller pilot before committing to the annual plan.

Good event properties:

  • Use simple names.
  • Are consistent across entries.
  • Are useful for filtering, sorting, grouping, or rendering.
  • Do not duplicate the entire event description.

Use event properties for:

  • Status: draft, published, open, done.
  • Category: type, project, area, kind.
  • Ownership: owner, assignee, account.
  • App behavior: remind, channel, pinned.
  • Display hints: image, slug, summary.

Event descriptions

Use the description for detail meant to be read by a person:

mw
2026-08-04: Call with Acme
  type: call
  account: acme
 
They asked for a smaller pilot. Follow up with a one-page proposal and pricing for 20 seats.

Descriptions are the right place for context, notes, links, paragraphs, and messy human nuance. If your app turns every sentence into a property, the document gets hard to write.

Markdown or markwhen?

Use markwhen when dates are central:

mw
2026-09-01: Draft published
  status: published
  slug: launch-notes

Use markdown when the document is a page:

md
# Launch notes

This page explains what changed, why it matters, and where to start.

A publishing app might use both:

  • Markwhen for posts, changelog entries, and scheduled drafts.
  • Markdown for about pages, documentation pages, and evergreen essays.

Stable names

Pick property names that you can live with.

Prefer:

mw
2026-10-01: Send renewal note
  status: open
  priority: high
  owner: Maya

Avoid:

mw
2026-10-01: Send renewal note
  Status: Open
  p: 1
  assigned_to_user_visible_display_name: Maya

Rules of thumb:

  • Use lowercase property names.
  • Prefer short words over abbreviations.
  • Use the same value vocabulary everywhere.
  • Avoid renaming fields casually.
  • Treat user-written documents as long-lived data.

Public and private fields

Assume users may read and edit their documents directly. Do not store app secrets in the header or event properties.

Good:

mw
---
publisher:
  site: rob-notes
  sync: true
---

Bad:

mw
---
publisher:
  apiSecret: sec_...
---

If your app needs secrets, store them in your own backend.

Generated fields

Sometimes an app needs IDs or generated metadata. Keep those fields obvious and minimal:

mw
2026-11-12: Contract signed
  account: acme
  appId: deal_123

Generated fields should not dominate the document. If you need a large generated payload, store it elsewhere and keep only a readable reference in markwhen.

Evolving a schema

Schemas change. Plan for it.

Start with versioned header metadata:

mw
---
crm:
  enabled: true
  schemaVersion: 1
---

When your app reads older documents, migrate gently:

ts
type CrmHeader = {
  enabled?: boolean;
  schemaVersion?: number;
  owner?: string;
};

function normalizeCrmHeader(header: { crm?: CrmHeader }) {
  const crm = header.crm || {};
  return {
    enabled: crm.enabled === true,
    schemaVersion: crm.schemaVersion || 1,
    owner: crm.owner || "unassigned",
  };
}
python
def normalize_crm_header(header):
    crm = header.get("crm") or {}
    return {
        "enabled": crm.get("enabled") is True,
        "schemaVersion": crm.get("schemaVersion") or 1,
        "owner": crm.get("owner") or "unassigned",
    }
java
record CrmSettings(boolean enabled, int schemaVersion, String owner) {}

CrmSettings normalizeCrmHeader(Map<String, Object> header) {
    Map<String, Object> crm = (Map<String, Object>) header.getOrDefault(
        "crm",
        Map.of()
    );

    return new CrmSettings(
        Boolean.TRUE.equals(crm.get("enabled")),
        crm.get("schemaVersion") instanceof Number n ? n.intValue() : 1,
        crm.get("owner") instanceof String owner ? owner : "unassigned"
    );
}

When possible, support old field names for a while instead of breaking users immediately.

Reading properties

The entries endpoint returns structured entry rows. Your app can filter by properties on the server:

ts
const entries = await fetch(
  `https://meridiem.markwhen.com/api/v1/docs/${username}/doc/${docName}/entries`,
  {
    method: "POST",
    headers: {
      authorization: `Bearer ${accessToken}`,
      "content-type": "application/json",
    },
    body: JSON.stringify({
      property_filter: {
        status: "published",
      },
    }),
  }
).then((response) => response.json());
js
const entries = await meridiemFetch(
  `/api/v1/docs/${username}/doc/${docName}/entries`,
  {
    method: "POST",
    body: JSON.stringify({
      property_filter: { status: "published" },
    }),
  }
);
python
entries = requests.post(
    f"https://meridiem.markwhen.com/api/v1/docs/{username}/doc/{doc_name}/entries",
    headers={"authorization": f"Bearer {access_token}"},
    json={"property_filter": {"status": "published"}},
    timeout=20,
).json()

Updating header fields

Use the header endpoint when changing document-level app settings:

ts
await fetch(
  `https://meridiem.markwhen.com/api/v1/docs/${username}/doc/${docName}/header`,
  {
    method: "PATCH",
    headers: {
      authorization: `Bearer ${accessToken}`,
      "content-type": "application/json",
    },
    body: JSON.stringify({
      merge: {
        publisher: {
          defaultStatus: "published",
        },
      },
    }),
  }
);
python
requests.patch(
    f"https://meridiem.markwhen.com/api/v1/docs/{username}/doc/{doc_name}/header",
    headers={"authorization": f"Bearer {access_token}"},
    json={
        "merge": {
            "publisher": {
                "defaultStatus": "published",
            }
        }
    },
    timeout=20,
)

Prefer merge for app settings. Use set only when replacing a value is definitely what the user expects.

What not to do

Avoid giant JSON blobs:

mw
2026-12-01: Imported item
  payload: {"very":"large","deeply":{"nested":"object"}}

Avoid unreadable encoded state:

mw
2026-12-01: Imported item
  state: eyJ2ZXJ5IjoibGFyZ2UifQ

Avoid making every user-visible word generated and fragile:

mw
2026-12-01: 
  renderMode: card_v4_final

Better:

mw
2026-12-01: Imported invoice from Acme
  source: acme
  externalId: inv_123
  status: pending

App schema examples

Feed

mw
---
feedapp:
  enabled: true
  title: Workshop notes
  description: Notes from the bench
---
 
2026-03-01: Shipped the prototype
  status: published
  tags: product,prototype

CRM

mw
---
crm:
  account: acme
  owner: Maya
  stage: pilot
---
 
2026-04-12: Follow-up call
  type: call
  sentiment: positive
  next: send proposal

Journal

mw
---
journal:
  enabled: true
  prompts: evening
---
 
2026-05-02 21:10: Evening check-in
  mood: calm
  energy: low

Publishing

mw
---
publisher:
  site: rob-notes
  defaultStatus: draft
---
 
2026-07-01: Building with Meridiem
  status: published
  slug: building-with-meridiem

Agent schema prompt

txt
Design a Meridiem schema for this app.

Return:
- The app namespace to use in the document header.
- Header fields, their types, defaults, and whether users can edit them.
- Event properties, their allowed values, and examples.
- Which data should be human prose in event descriptions.
- Which data should be markdown documents instead of markwhen events.
- What data should not be stored in Meridiem.
- A migration plan with schemaVersion.
- Three example markwhen documents using the schema.

Keep the generated markwhen readable to a person editing it directly.