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.
links.json#
[{ "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.