# Open Wealth Format (v1)

A plain-file format for one person's financial picture: accounts, income and
expenses, goals, plans, forecasts and wealth history. It is the storage format
of the Wealthos Local Edition, published so that any program, and in
particular any AI agent with file access, can read and write it without
Wealthos.

- **Machine-readable index:** <https://wealthos.cc/open-wealth-format/v1/index.json>
- **JSON Schemas:** <https://wealthos.cc/open-wealth-format/v1/> (one per file
  type, also in [`schema/`](schema/))
- **Reference implementation:** `packages/local-core` (loader, validator,
  writer, MCP server and the `wealthos-core` command-line tool)

## Why it exists

Financial apps keep your data in a database you can't open. An AI agent can
only reach it through whatever API the vendor chose to expose. A folder of
YAML files works with everything: an editor, `git`, a backup tool, a script,
and agents like Claude Code or Codex that already know how to read and edit
files.

The format is deliberately boring. Records are the same snake_case rows the
Wealthos Cloud API uses, so data moves between editions without translation.

## Layout

A vault is a folder. Its name doesn't matter; `wealthos.vault.yaml` makes it a
vault.

| Path | Storage | Contents |
|---|---|---|
| `wealthos.vault.yaml` | YAML | Manifest: `schema_version`, `vault_id`, `user_id`, `email`, `created_at` |
| `settings.yaml` | YAML | `schema_version`, `preferred_currency`, `country` |
| `accounts/<id>.yaml` | one YAML file per record | Accounts and balances |
| `account-categories/<id>.yaml` | " | Custom account categories |
| `goals/<id>.yaml` | " | Savings targets |
| `plans/<id>.yaml` | " | Dated multi-line budgets |
| `plan-templates/<id>.yaml` | " | Reusable plan bodies |
| `metrics/<id>.yaml` | " | User-defined metrics (expression trees) |
| `simulations/<id>.yaml` | " | Saved forecasts and life events |
| `expenses.jsonl` | one JSON object per line | Recurring and one-time expenses |
| `income.jsonl` | " | Recurring and one-time income |
| `snapshots.jsonl` | " | Total wealth over time |
| `assets/images/` | binary | Plan and goal cover images |
| `OVERVIEW.md`, `DEPENDENCIES.md`, `AGENTS.md` | Markdown | **Generated** views. Read-only |
| `.wealthos/` | — | Lock, FX cache, runtime state. Private to the implementation |

Document entities get one file each so they diff and hand-edit well; the
high-volume logs are JSONL so appending is cheap.

## Records

`wealthos-core schema` lists every file type; `wealthos-core schema <name>`
prints its JSON Schema. The schemas are generated from the validators the
reference implementation loads with, and a test keeps the published copies
identical, so they are exact.

An account (`accounts/a0000000-0000-0000-0000-000000000001.yaml`):

```yaml
id: a0000000-0000-0000-0000-000000000001
user_id: 00000000-0000-0000-0000-000000000001
name: Main Checking
type: bank                     # bank | savings | investments | real_estate | a custom category slug
balance: "1234.56"             # money: a decimal STRING
iban: DE89370400440532013000
currency: EUR
institution: Demo Bank
is_connected: false
is_hidden: false
gocardless_id: null
bank_provider: null
bank_provider_id: null
bank_session_id: null
display_order: 0
annual_growth_rate_pct: 1.5    # optional; forecasts compound this account at 1.5%/year
liquidity_tier_override: null  # optional; immediate | high | medium | low, or null to use the category's
created_at: 2026-01-01T00:00:00.000Z
updated_at: 2026-01-01T00:00:00.000Z
last_sync: null
```

An expense (one line of `expenses.jsonl`):

```json
{"id":"e0000000-0000-0000-0000-000000000001","user_id":"00000000-0000-0000-0000-000000000001","name":"Rent","amount":"1200","currency":"EUR","frequency":"monthly","start_year":2026,"start_month":1,"end_year":null,"end_month":null,"category":"housing","notes":null,"created_at":"2026-01-01T00:00:00.000Z","updated_at":"2026-01-01T00:00:00.000Z"}
```

## Rules

### Money

Top-level money fields are **decimal strings** in files and must match
`^-?\d+(\.\d+)?$`. A YAML or JSON number there is rejected: some parsers lose
precision on floats, and refusing numbers forces every writer to produce an
exact value.

| File type | String money fields |
|---|---|
| account | `balance` |
| goal | `target_amount`, `current_progress` |
| simulation | `target_amount`, `initial_amount` |
| expense, income | `amount` |
| snapshot | `total_wealth` |

Amounts nested inside a plan or plan template (`items[].estimated_amount`,
`items[].actual_amount`, `reference_amounts[].amount`,
`starting_balance_manual`, `monthly_contribution`) are plain numbers, as in the
Cloud API. Rates such as `annual_growth_rate_pct` are plain numbers too.

Every amount is in the currency named on its own record (`currency` or
`original_currency`). Totals are converted to `settings.yaml`'s
`preferred_currency` by the reader; the format stores no converted values.

### Identity

- Ids are UUIDs. A document record's file name is `<id>.yaml` and must match
  its `id` field: readers key records by file name.
- `user_id` on every record equals the manifest's `user_id`. A vault has one
  owner.
- Relationships are by id only (`connected_account_id`, `source_plan_id`,
  `starting_balance_account_ids`). Nothing is embedded across files.
- Dates are ISO-8601. Month-precision fields are split into integer
  `*_year` / `*_month` pairs, with months numbered 1–12.
- On a recurring income or expense, `start_year` / `start_month` may both be
  `null`: the flow has no start date and was already running, so it counts
  as active for every past and present month. One-time records always carry
  their month. (Readers from before this was allowed reject such a record
  and report it as a vault health issue rather than failing the file.)

### Compatibility

- **Unknown keys are preserved.** Readers must keep keys they don't
  recognise when they rewrite a record. This is how an agent or another tool
  can attach its own data (`notes`, tags, links) to a record.
- **New fields are optional.** A field added in a later release is optional
  and nullable, and an absent field reads as `null`, so older vaults stay
  valid without a migration.
- `schema_version` in the manifest only changes for a change older readers
  can't handle. This document describes version 1.

### Writing

- Write whole files atomically: write a temporary file in the same folder,
  then rename it over the target. Readers report a malformed file as a health
  issue and skip it; they never repair or overwrite it.
- **One writer at a time.** A process that writes holds
  `.wealthos/lock.json` (`{ pid, ... }`); a lock whose pid is dead is stale
  and may be taken over. A writer that finds a live lock should go through the
  process holding it. The reference implementation does this automatically:
  `wealthos-core call` and `mcp-stdio` route through a running desktop app.
- Plain hand edits are allowed while the app runs: it watches the folder and
  re-validates changed files.
- Deleting a record's file deletes the record.

### Generated files

`OVERVIEW.md` (headline figures and an index), `DEPENDENCIES.md` (which
records refer to which) and `AGENTS.md` (these rules, for agents) are
regenerated after every write. Never treat them as data and never edit them.

## The command-line tool

`wealthos-core` is the reference implementation's CLI. It ships inside the
Wealthos desktop app (`Wealthos.app/Contents/MacOS/wealthos-core`), runs
anywhere with Node 20+ as **`npx wealthos`** (the same CLI, bundled into one
file by `packages/cli`), and runs from source with
`npx tsx packages/local-core/src/cli.ts`.

```sh
wealthos-core tools  ~/Documents/Wealthos --text      # every tool, one line each
wealthos-core call   ~/Documents/Wealthos wealth_get  # JSON on stdout
wealthos-core call   ~/Documents/Wealthos goals_add name="Sabbatical" target_amount=20000 target_year=2028
wealthos-core schema plan                             # JSON Schema for plans/<id>.yaml
wealthos-core doctor ~/Documents/Wealthos             # validate every file, change nothing
```

`tools` and `call` run the same tools as the Wealthos MCP server, with the
same validation, so anything an MCP client can do, a shell script can do. Pass
a server URL instead of a folder to work against Wealthos Cloud with an API
token:

```sh
export WEALTHOS_TOKEN=wos_...   # Settings → Connect to AI tools via MCP
wealthos-core call https://mcp.wealthos.cc wealth_get
```

| Command | Purpose |
|---|---|
| `tools <target> [--text] [--schemas]` | List tools. JSON by default; `--schemas` adds each input schema |
| `call <target> <tool> [--args JSON] [key=value …] [--text]` | Call a tool. `key=value` values are read as JSON when they parse, and dotted keys nest (`date.year=2027`). `--text` prints the human-readable message |
| `schema [name]` | The file-type index, or one JSON Schema |
| `rules [id] [key=value …] [--due]` | The [country rules](https://wealthos.cc/country-rules/v1/index.json) library: list, one rule, or a calculation. No vault needed |
| `init`, `doctor`, `serve`, `mcp-stdio` | Create, validate, serve, or expose a vault over MCP stdio |

Exit codes: `0` success, `1` the tool reported a failure (the JSON result is
still printed), `2` bad usage or an unknown tool.

## Regenerating the schemas

After changing a vault record schema in `packages/local-core/src/schemas/`:

```sh
pnpm --filter @wealthos/local-core format:schema
```

This rewrites `docs/open-wealth-format/schema/` and
`packages/web/public/open-wealth-format/v1/` (served at the `$id` URLs).
`test/format/schema.test.ts` fails until the committed copies match.
