OpenTime/Developer Guide

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 .ot files (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 yaml
import 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.json

JS/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:

  1. Parse + validate the document
  2. Loop over items
  3. For each item, switch on type and 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 extensions dictionary/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 items unless you are certain you own the whole file
  • Respect opentime_version and 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

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.