File formats#

Every document Ohm writes is JSON (or plain text), atomically, in a folder you own.

Scripts folder#

Documents/Ohm/
├─ *.ohm                     scripts
├─ .ohm/
│  ├─ trash/                 deleted files, timestamped
│  ├─ shots/                 screenshots taken by scripts
│  └─ memory.json            script memory
└─ projects/<id>/

Project#

projects/<id>/
├─ ohm.project.json
├─ links.json
├─ docs/  data/  scripts/  files/  annotations/
├─ prompts/<name>.prompt.json          (+ <name>.versions/)
├─ curves/<name>.curve.json
├─ experiments/<name>.experiment.json  (+ <name>.runs/)
├─ graphs/<name>.graph.json
└─ specs/<name>.spec.json

Project ids match ^[a-z0-9][a-z0-9-]{0,63}$; item names match ^[\w .()-]{1,80}$ and may not start with a dot.

ohm.project.json#

{ "name": "Deep research", "kind": "research", "description": "…",
  "question": "…", "createdAt": "2026-09-23T10:00:00.000Z" }

kind is general or research.

[{ "id": "a1b2c3d4", "url": "https://…", "title": "…", "notes": "…",
   "tags": [], "source": "research: sub-question", "addedAt": "…" }]

Links dedupe on the normalised URL.

Items#

`.prompt.json`#

{
  "name": "summarize",
  "status": "active",
  "versions": [{
    "version": 1,
    "hash": "3f2a…",
    "system": "You are…",
    "template": "Summarise {{text}} in {{sentences}} sentences.",
    "variables": [{ "name": "text", "type": "string", "required": true }],
    "model": { "profile": "Claude", "temperature": 0.3 },
    "at": "…"
  }]
}

A version is appended only when the hash of [system, template, variables, model] changes.

`.curve.json`#

{
  "version": 1,
  "name": "temperature",
  "points": [{ "x": 0, "y": 0.1 }, { "x": 0.25, "y": 1 }, { "x": 1, "y": 0.2 }],
  "segments": [{ "interp": "linear" }, { "interp": "cosine" }],
  "output": { "type": "float", "min": 0, "max": 2 },
  "domain": { "label": "step", "min": 0, "max": 100 }
}

segments.length === points.length − 1, always. Segment i spans point i to i+1.

`.experiment.json`#

{
  "name": "summarize-sweep",
  "prompt": { "name": "summarize", "version": 1 },
  "dataset": { "path": "sample.jsonl", "expectedColumn": "expected", "limit": 50 },
  "profiles": ["Claude", "Ollama"],
  "sweep": {
    "mode": "schedule",
    "trials": 4,
    "seed": 42,
    "variables": [
      { "name": "temperature", "type": "float", "source": { "curve": { "curve": "temperature" } } },
      { "name": "style", "type": "string", "source": { "values": ["plain", "bullet points"] } }
    ]
  },
  "evaluators": [{ "type": "contains", "weight": 1, "params": { "ignoreCase": true } }],
  "concurrency": 3
}

temperature, maxTokens and profile are reserved sweep names that set model parameters.

`.runs/.json`#

One file per run holding the whole run: status, totals, and every trial with its summary (passRate, avgScore, avgLatencyMs, p50LatencyMs, p95LatencyMs) and cases. runId is an ISO timestamp with : and . replaced by -, plus four characters of a UUID.

`.graph.json`#

{
  "name": "research",
  "profile": "Deep research",
  "nodes": [{ "id": "n1", "type": "Question", "x": 0, "y": 0, "values": { "fallback": "…" } }],
  "connections": [{ "from": "n1", "output": "text", "to": "n2", "input": "query" }]
}

`.spec.json`#

{
  "version": 1,
  "name": "plan",
  "nodes": [{
    "id": "n1",
    "title": "Goals",
    "kind": "requirement",
    "status": "todo",
    "priority": "high",
    "tags": ["mvp"],
    "body": "markdown",
    "refs": [{ "kind": "doc", "target": "report.md", "blocking": false }],
    "children": []
  }]
}

`.annots.json`#

In annotations/, named after the document's relative path:

{
  "version": 1,
  "file": "report.pdf",
  "annotations": [{
    "id": "amucysjlq5cc",
    "type": "comment",
    "anchor": { "kind": "pdf", "page": 0, "rect": [71.9, 121.3, 183.5, 13.3] },
    "text": "Check this figure",
    "quote": "Select this sentence to annotate it.",
    "color": "#3794ff",
    "status": "open",
    "createdAt": "…"
  }]
}

Anchor kinds: pdf (page + rect), slide (slide + rect), cell (sheet + A1 ref), text (quote + occurrence), image (rect), file. Rectangles are stored unscaled, with a top-left origin and y growing downward. Page and slide indices are 0-based.

Versions#

Saving an item snapshots the previous content:

prompts/summarize.versions/2026-09-23T10-15-30-123Z.json

The newest 50 are kept. A snapshot is written only when the content actually changed, so a new item has none.

Bundle#

{
  "ohmBundle": 1,
  "exportedAt": "…",
  "info": { "name": "…", "kind": "research", … },
  "links": [ … ],
  "items": { "prompt": { "summarize": { … } }, "spec": { "plan": { … } } },
  "files": { "docs/report.md": "…", "annotations/report.pdf.annots.json": "…" },
  "binaries": { "files/report.pdf": "JVBERi0…" }
}

Text under docs/, data/, scripts/ and annotations/; documents base64 under binaries. Importing always creates a new project.

Settings#

<userData>/settings.json:

{
  "activeProfile": "Claude",
  "profiles": [{ "name": "Claude", "provider": "anthropic", "model": "claude-opus-5" }],
  "modelsDir": "…",
  "activeProject": "my-project",
  "graphProfiles": [{ "name": "My palette", "types": ["Text", "Generate"] }]
}

Credentials are never here — they live in <userData>/secrets.json, encrypted by the OS, as profile:<name>:apiKey and hf:token.