Specification
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. - Example:
examples/minimal.owlf.json. - Reference implementation:
@owlf/core.
The key words MUST, SHOULD and MAY follow RFC 2119.
Design rules
- 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.
- 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.
- Count what is lost. A field the format cannot carry is counted in
dropped, never silently skipped. - No app is special. Nothing in the format belongs to one app. App-specific meaning goes into scale labels,
valenceordropped.
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 or null |
null when the app has no mood rating (plain journals). |
scales.sleep |
Scale or null |
null when the app has no sleep rating. |
entries |
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 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.
- Package:
@owlf/adapter-<id>. detect(files)returns 0 to 1 and never throws.parse(files)returns a valid document or throwsAdapterErrorwithmessage,whyandfix.moduleslists the kinds of data the adapter reads, in this order. An entry has a module when:mood:moodis set.sleep:sleepis set.emotions:emotionsis not empty.tags:tagsis not empty.people:peopleis not empty.notes:titleornotehas text.prompts:promptsis not empty.photos:media.photosis above 0.
- Conformance: the adapter ships a synthetic fixture and passes
checkAdapterfrom@owlf/core/testkit. The fixture MUST cover every mood position when the app has mood. The fixture MUST cover exactly the declaredmodules: 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.