---
title: Specification
description: The normative text of OWLF 0.1. Generated from spec/SPEC.md.
---

**OWL is an open JSON format for mood and wellbeing data.** One file holds one person's entries from one app: mood, sleep, emotions, tags, people, notes and journal prompts.

- **File**: `*.owlf.json`, UTF-8 JSON.
- **Media type**: `application/vnd.owlf+json`.
- **JSON Schema**: [`schema/0.1.json`](/schema/0.1.json).
- **Example**: [`examples/minimal.owlf.json`](/minimal.owlf.json).
- **Reference implementation**: [`@owlf/core`](https://github.com/mrzmyr/owlf/blob/main/packages/core).

The key words MUST, SHOULD and MAY follow [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119).

## Design rules

1. **Keep source values.** A document stores what the app stored: its own scale, labels and emotion names. Mapping to another app's vocabulary is the consumer's job. So the same file can feed any target, and every mapping can be explained to the person.
2. **Local day first.** An entry belongs to the calendar day the person saw in the app. Producers MUST NOT derive that day from a UTC timestamp.
3. **Count what is lost.** A field the format cannot carry is counted in `dropped`, never silently skipped.
4. **No app is special.** Nothing in the format belongs to one app. App-specific meaning goes into scale labels, `valence` or `dropped`.

## Document

| Field | Type | Rule |
| --- | --- | --- |
| `owlf` | `"0.1"` | MUST. Spec version. |
| `$schema` | string | MAY. Link to the JSON Schema. |
| `source.app` | kebab-case string | MUST. Adapter id: `daylio`, `how-we-feel`. |
| `source.name` | string | MUST. App name as the store lists it. |
| `source.generator` | string | SHOULD. Package and version that wrote the file. |
| `scales.mood` | [Scale](#scale) or `null` | `null` when the app has no mood rating (plain journals). |
| `scales.sleep` | [Scale](#scale) or `null` | `null` when the app has no sleep rating. |
| `entries` | [Entry](#entry)[] | MUST. Source order or by time. |
| `warnings` | string[] | MUST. File-level notes for people, e.g. "2 files ignored". |

Unknown fields are invalid. New fields arrive with a new spec version.

**Fields marked MAY or SHOULD are optional; every other field is required.** Empty values are written out: `null`, `[]`, `{}` or `{ "photos": 0 }`.

## Scale

A scale describes the app's own rating steps, worst to best.

| Field | Type | Rule |
| --- | --- | --- |
| `size` | integer ≥ 1 | Number of positions: 5 for a 1–5 scale. |
| `levels` | Level[] | At least one. |
| `continuous` | `{ min, max, unit? }` | MAY. Only for sliders. Levels are then equal bands of `min..max`. |

**Level**

| Field | Type | Rule |
| --- | --- | --- |
| `id` | string | MUST be unique in its scale. |
| `label` | string | As the app shows it: "rad", "7", "Very good". |
| `position` | integer, `0..size-1` | 0 = worst. Several levels MAY share a position (custom moods in one group). |
| `color` | `#rrggbb` | MAY. The app's color for this level. |
| `valence` | number, `-1..1` | MAY. The adapter's reading of the level's meaning. Set it only when position misleads, e.g. 7 stored values shown as 5 faces. |

**Consumers map by `valence` when present, else by `position / (size - 1)`.** A scale of size 1 maps to the middle.

## Entry

| Field | Type | Rule |
| --- | --- | --- |
| `id` | string | MAY. The app's id, for finding duplicates. |
| `date` | `YYYY-MM-DD` | MUST. Local calendar day. |
| `time` | `HH:mm` or `HH:mm:ss` or `null` | Local wall-clock time. `null` = unknown. |
| `instant` | [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339) date-time with seconds and offset (`2026-03-01T21:40:00+01:00`), or `null` | Only when the file stores an absolute moment. Wins over `date` + `time` for ordering. |
| `zoneless` | boolean | MAY. `instant` comes from a bare epoch; `date` and `time` are a guess in the converting device's zone. Consumers SHOULD re-derive the day in the person's zone. Requires `instant`. |
| `utcDate` | `YYYY-MM-DD` | MAY. The file prints a UTC day next to a local time. Consumers SHOULD re-derive the day in the person's zone. |
| `mood` | ScaleValue or `null` | `level` MUST exist in `scales.mood`. |
| `sleep` | ScaleValue or `null` | `level` MUST exist in `scales.sleep`. |
| `title` | string or `null` | |
| `note` | string or `null` | Free text, line breaks kept. |
| `prompts` | `{ prompt, answer }[]` | Guided questions with answers, source order. |
| `tags` | `{ name, group }[]` | `group` = the app's category, `null` = ungrouped. |
| `emotions` | `{ name }[]` | Names as the app spells them. |
| `people` | string[] | Names as the app spells them. A person on an entry means the person connected with them that day, not that they were together. |
| `media` | `{ photos }` | Counts only. Files are not carried. |
| `dropped` | `{ [field]: count }` | Source fields with no place in this format, with count on this entry. |

**ScaleValue**: `{ level, raw, label? }`. `raw` is the value exactly as in the file. `label` MAY be set, only when the file's label differs from the level's.

## Adapters

An adapter reads one app's export and writes a document. Contract: [`Adapter`](https://github.com/mrzmyr/owlf/blob/main/packages/core/src/adapter.ts).

- **Package**: `@owlf/adapter-<id>`.
- **`detect(files)`** returns 0 to 1 and never throws.
- **`parse(files)`** returns a valid document or throws `AdapterError` with `message`, `why` and `fix`.
- **`modules`** lists the kinds of data the adapter reads, in this order. An entry has a module when:
  - `mood`: `mood` is set.
  - `sleep`: `sleep` is set.
  - `emotions`: `emotions` is not empty.
  - `tags`: `tags` is not empty.
  - `people`: `people` is not empty.
  - `notes`: `title` or `note` has text.
  - `prompts`: `prompts` is not empty.
  - `photos`: `media.photos` is above 0.
- **Conformance**: the adapter ships a synthetic fixture and passes `checkAdapter` from `@owlf/core/testkit`. The fixture MUST cover every mood position when the app has mood. The fixture MUST cover exactly the declared `modules`: each in at least one entry, no other. Real exports never enter the repository.

## Versioning

`owlf` is `MAJOR.MINOR`.

- **Before 1.0**, any version change may break. Consumers MUST reject a version they do not know.
- **From 1.0**, a minor version only adds optional fields. Consumers MUST reject an unknown major version and SHOULD read a higher minor version of a known major, ignoring fields they do not know.
