Developer Guide
Learn how to read, write, validate, and extend .ot files in your own apps and tools.
This guide is for developers integrating OpenTime into their products, plugins, or personal tooling.
What You'll Learn
By the end of this guide, you'll know how to:
- Parse OpenTime
.otfiles (YAML) into in-memory objects - Validate documents using the JSON Schema
- Map OpenTime items to your own data models
- Export your data back into valid OpenTime files
- Handle
x_*extension fields without breaking interoperability
If you haven't already, you may want to skim the OpenTime Specification and JSON Schema pages first.
Quick Start Checklist
To support OpenTime in your app, you'll need:
- A YAML parser for your language
- A JSON Schema validator (optional but recommended)
- Basic models for the core item types you care about
- Logic to import/export between your models and OpenTime items
1. Mental Model: How OpenTime Works
OpenTime is intentionally simple. Think of it like this:
- One file (.ot) = one OpenTime document
- The document has metadata (
opentime_version,default_timezone, etc.) and an array of items - Each item has a
type,id,title, and type-specific fields - Different types represent different concepts:
goal,task,habit,reminder,event,appointment,project - Apps can add their own data using
x_*extension fields (e.g.,x_elysium)
opentime_version: "0.2"
default_timezone: "Asia/Tokyo"
generated_by: "YourApp 1.0"
items:
- type: task
id: task_1
title: "Example task"
status: todo
tags: ["inbox"]The format is YAML for human readability, but the schema is defined in JSON Schema for machine validation.
2. Parsing .ot Files (YAML → Object)
The first step is to parse the YAML into a native object. How you do this depends on your language and environment.
JavaScript / TypeScript
Install a YAML parser, like yaml:
npm install yamlimport fs from "node:fs/promises";
import { parse } from "yaml";
async function loadOpenTime(path: string) {
const text = await fs.readFile(path, "utf8");
const doc = parse(text); // now a JS object
if (typeof doc !== "object" || !doc) {
throw new Error("Invalid OpenTime document root");
}
// Basic shape checks before schema validation
if (!("opentime_version" in doc) || !("items" in doc)) {
throw new Error("Missing opentime_version or items");
}
return doc;
}Swift (using Yams)
This mirrors how Elysium itself parses OpenTime files. Install Yams via Swift Package Manager:
import Foundation
import Yams
struct OpenTimeDocument: Decodable {
let opentime_version: String
let default_timezone: String?
let generated_by: String?
let created_at: String?
let items: [OpenTimeItem]
}
enum OpenTimeItem: Decodable {
case goal(Goal)
case task(Task)
case habit(Habit)
case reminder(Reminder)
case event(Event)
case appointment(Appointment)
case project(Project)
// You'll typically implement init(from:) as a tagged union
}
func loadOpenTime(from url: URL) throws -> OpenTimeDocument {
let yaml = try String(contentsOf: url, encoding: .utf8)
let decoder = YAMLDecoder()
return try decoder.decode(OpenTimeDocument.self, from: yaml)
}In practice, you may mirror the Elysium OpenTimeModels.swift structure so you can keep the spec and your implementation aligned.
3. Validating Against the JSON Schema
Once you have a parsed document, you can optionally run it through a JSON Schema validator. This is especially useful in:
- CLI tools (linting OpenTime files)
- Developer workflows (failing builds on invalid files)
- Server-side import pipelines
The schema for OpenTime v0.2 lives at:
https://elysium.is/opentime/schemas/v0.2/opentime.jsonJS/TS with Ajv
import Ajv from "ajv";
import addFormats from "ajv-formats";
const ajv = new Ajv({ allErrors: true });
addFormats(ajv);
const schemaUrl = "https://elysium.is/opentime/schemas/v0.2/opentime.json";
const schema = await (await fetch(schemaUrl)).json();
const validate = ajv.compile(schema);
const doc = await loadOpenTime("life.ot"); // from previous step
if (!validate(doc)) {
console.error("OpenTime validation errors:", validate.errors);
} else {
console.log("OpenTime document is valid.");
}For local or offline tools, you can vendor the schema into your repo instead of fetching it at runtime.
4. Mapping to Your Own Data Models
OpenTime is a data exchange format, not your internal schema. Typically you'll:
- Parse + validate the document
- Loop over
items - For each item, switch on
typeand map it into your own domain models
Example: Mapping Tasks in JS/TS
type OtItem = {
type: string;
id: string;
title: string;
[key: string]: any;
};
type OtDocument = {
opentime_version: string;
items: OtItem[];
[key: string]: any;
};
type TaskModel = {
id: string;
title: string;
status: "todo" | "in_progress" | "done" | "cancelled";
due?: string;
estimateMinutes?: number;
};
function importTasks(doc: OtDocument): TaskModel[] {
return doc.items
.filter((item) => item.type === "task")
.map((item) => ({
id: item.id,
title: item.title,
status: item.status,
due: item.due,
estimateMinutes: item.estimate_minutes,
}));
}You can repeat this mapping for other item types (event, habit, etc.) depending on what your app supports.
5. Exporting to OpenTime (Your Models → .ot)
To export, you essentially perform the reverse mapping: take your internal models and convert them into OpenTime item objects, then serialize them to YAML.
JS/TS Export Example
import { stringify } from "yaml";
function exportAsOpenTime(tasks: TaskModel[]): string {
const doc = {
opentime_version: "0.2",
default_timezone: "Asia/Tokyo",
generated_by: "MyApp 1.0",
items: tasks.map((t) => ({
type: "task",
id: t.id,
title: t.title,
status: t.status,
due: t.due,
estimate_minutes: t.estimateMinutes,
})),
};
return stringify(doc);
}You can then write the resulting YAML string to disk as something.ot or store it wherever your app needs it.
6. Handling Extensions with x_* Fields
One of the most important parts of OpenTime is extensibility. Apps can store custom fields under namespaced keys like x_elysium without breaking other apps.
- type: event
id: ev_example
title: "Event with extensions"
start: "2025-12-20T10:00:00+09:00"
end: "2025-12-20T11:00:00+09:00"
x_elysium:
focus_mode: "deep"
color: "#3B82F6"
x_other_app:
custom_field: "preserved on round-trip"To preserve round-trip safety, your implementation SHOULD:
- Parse the entire item object, including unknown keys (like
x_*) - Store unknown keys in a generic
extensionsdictionary/map alongside your core fields - When writing back to YAML, re-emit those extension fields exactly as you found them
Swift Example with Extensions
struct BaseItem: Codable {
let type: String
let id: String
let title: String
var extensions: [String: CodableValue] = [:]
private enum CodingKeys: String, CodingKey {
case type, id, title
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
type = try container.decode(String.self, forKey: .type)
id = try container.decode(String.self, forKey: .id)
title = try container.decode(String.self, forKey: .title)
// Capture all other keys (including x_*)
let raw = try decoder.singleValueContainer().decode([String: CodableValue].self)
extensions = raw.filter { key, _ in
key != "type" && key != "id" && key != "title"
}
}
}CodableValue is a common helper type for representing "any JSON-like" value. Your actual implementation may differ, but the key idea is: do not throw away unknown fields.
7. Interoperability & Best Practices
To play nicely with other OpenTime-aware apps, you should:
- Preserve unknown fields and
x_*namespaces on round-trip - Use stable, unique IDs for items (e.g., prefixed by type:
task_,goal_,proj_) - Avoid deleting items from
itemsunless you are certain you own the whole file - Respect
opentime_versionand be prepared for future minor additions to the spec - Consider offering an "Export as OpenTime" option even if your app doesn't use OpenTime internally
8. Where to Go From Here
Specification
Read the full OpenTime specification
Reference
Quick lookup for all fields and values
JSON Schema
Explore the JSON Schema for validation
Examples
Study example .ot files
Our Story
Learn the story behind Elysium and OpenTime
If you build an app, library, or plugin that supports OpenTime, consider reaching out so it can be linked from the ecosystem section in the future.