# Authentication Source: https://docs.truemath.ai/api/authentication Authenticate TrueMath API requests with a bearer API key. How to create a key, send it, and rotate or revoke it. The TrueMath API authenticates every request with a bearer API key tied to your account. ## The Authorization header Send your key in the `Authorization` header: ```http theme={null} Authorization: Bearer tm_live_... ``` A key is a single opaque string of the form `tm_live__`. Treat the whole string as a secret credential. ## Creating a key Generate a key in the [app](https://app.truemath.ai), under **••• → API Key**. Creating a key requires the developer or owner role on the account. The full key is shown **once**, at creation. Copy it and store it somewhere secure — it cannot be displayed again. If you lose it, rotate the key to issue a new one. ## Using a key ```bash theme={null} curl https://api.truemath.ai/v1/domains \ -H "Authorization: Bearer tm_live_..." ``` A key is either **active** or **revoked**. Rotating a key issues a new secret and invalidates the previous one; revoking a key disables API access until a new key is created. ## Authentication errors | Status | Code | Meaning | | ------ | ------------------------ | -------------------------------------------------------------- | | `401` | `error_unauthorized` | Missing or malformed `Authorization` header, or an invalid key | | `403` | `error_disabled_api_key` | The key has been revoked or disabled | See [Errors](/api/errors) for the full error reference. # Calculate Source: https://docs.truemath.ai/api/calculate Run a calculation with natural language or structured input via POST /v1/calculate, and read the result, provenance, and scenario it produces. ```http theme={null} POST /v1/calculate ``` Runs a calculation against a published [domain](/concepts/domains). A calculation happens inside a [conversation](/concepts/scenarios): omit `conversation_id` to start a new conversation, or pass one to continue an existing one. ## Request | Field | Type | Required | Description | | ----------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `domain_id` | UUID | Yes | The domain to calculate against. | | `input_format` | string | No | `natural_language`, `structured`, `json`, or `automatic`. Optional — omit it, or pass `null` or `""`, to default to `automatic`. See [Automatic detection](#automatic-detection). | | `prompt` | string | Yes | The input — prose for `natural_language`, `key: value` lines for `structured`, or a JSON string for `json`. See [Input formats](/concepts/input-formats). | | `conversation_id` | UUID | No | Continue an existing conversation. Omit to start a new one. | | `time_zone` | string | No | An IANA zone name, such as `America/New_York`. The zone `today` and `now` are read in, applied once per request. Omitted or unrecognized falls back to your account's zone. See [Time zones](#time-zones). | Send an `Idempotency-Key` header to make a request safe to retry and to poll. Since natural language requests return immediately with 202 Accepted, an Idempotency Key is required to retrieve the final result. See [Limits](/api/limits#idempotency). ### Natural language input ```json theme={null} { "domain_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "input_format": "natural_language", "prompt": "What is the monthly payment on a $400,000 loan at 6% over 30 years?" } ``` ### Structured input Structured input is a set of `key: value` lines — one [variable](/concepts/variables) per line — plus a `calculate:` line naming the variable to solve for. For the full grammar — values, units, tables, scenario selectors, and how omitted lines are inferred — see [Input formats](/concepts/input-formats#structured-text). ```json theme={null} { "domain_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "input_format": "structured", "prompt": "home_price: $500,000\ndown_payment: $100,000\ninterest_rate: 6.5%\nloan_term: 30 yr\ncalculate: monthly_payment" } ``` ### JSON input A JSON request sets `input_format` to `json` and passes a JSON **string** as `prompt`. Like structured text, it involves no language model and runs synchronously — the result returns in the same response. For the full object — `inputs`, `calculate`, `scenario`, and value conventions — see [Input formats](/concepts/input-formats#json). ```json theme={null} { "domain_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "input_format": "json", "prompt": "{\"inputs\":[{\"key\":\"home_price\",\"value\":\"500000 USD\"},{\"key\":\"down_payment\",\"value\":\"100000 USD\"},{\"key\":\"interest_rate\",\"value\":\"6.5%\"},{\"key\":\"loan_term\",\"value\":\"30 yr\"}],\"calculate\":{\"key\":\"monthly_payment\",\"unit\":null},\"scenario\":\"new\"}" } ``` A `prompt` that parses as JSON but does not match the schema returns [`input.invalid_input_schema`](/api/errors#calculation-error-codes); a `prompt` that is not valid JSON returns the transport error [`error_invalid_json`](/api/errors#transport-errors). ### Automatic detection Set `input_format` to `automatic` — or leave it out, or pass `null` or `""` — to let TrueMath determine from the `prompt` whether it is natural language, structured text, or JSON, and process it accordingly. This is the default when the field is absent. ```json theme={null} { "domain_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "prompt": "What is the monthly payment on a $400,000 loan at 6% over 30 years?" } ``` Because the detected format governs processing, the same request behavior applies once it is resolved — a prompt detected as `natural_language` is processed asynchronously, while one detected as `structured` or `json` runs synchronously. Specify `input_format` explicitly when you already know the format and want to skip detection. ## Time zones A stored date or clock time carries **no time zone and never shifts**. A value meaning 2:30 PM on July 21st reads as 2:30 PM on July 21st for every caller, in every country: it is never re-projected into a viewer's zone and shown as 5:30 PM. That is why the UTC pin under [Dates and durations](#dates-and-durations) renders one correctly — there is no zone to convert *from*. A zone is used for one thing, and has no other effect: reading what `today` and `now` mean at the moment a calculation runs and, with them, what year a date written without one falls in. Two things supply that zone: 1. **The request.** Over the API that is the `time_zone` parameter, an IANA name such as `America/New_York`. In the [Playground](/playground/tour) it is the browser's own zone, sent with each calculation — never a timestamp, so a device with a wrong clock cannot move what today is. Either way the zone is applied once per request, so every keyword in one calculation agrees. ```json theme={null} { "domain_id": "…", "time_zone": "America/New_York", "prompt": "…" } ``` 2. **Your account's time zone**, when the request supplies none or names a zone that is not recognized. That covers the API, an integration, and a scheduled job. Every account has a time zone. It is detected from the browser the account was created in and can be changed in account settings; where detection is not possible it is UTC. The fallback is always a real zone rather than an unset one. Send your user's zone whenever a person is waiting on the result. It is the only control you have over what those two words mean, and for an integration the account default may be nowhere near your user. ## Response The response echoes the conversation, message, and request metadata, and reports its outcome with `status`. A `completed` or `error` outcome returns `200 OK`; an asynchronous request that is still running returns `202 Accepted` with `status: "in_progress"`. | Field | Type | Description | | --------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `conversation_id` | UUID | The conversation this calculation belongs to. | | `message_id` | UUID | This request's message. | | `idempotency_key` | string \| null | The key you sent, echoed back. | | `status` | string | `completed`, `in_progress`, or `error`. | | `source` | string | How the request was made — `api` or `chat`. | | `input_format` | string | `natural_language`, `structured`, or `json`. | | `natural_language` | string | The natural-language reading. Present only when `input_format` is `natural_language` and `status` is `completed`; omitted otherwise. | | `parsed_json` | object | The parsed input object — `inputs`, `calculate`, and `scenario` in the same shape as a [JSON request](/concepts/input-formats#json), plus a `version` naming the parse-contract shape it conforms to (currently `2`). Present only when `input_format` is `natural_language` or `json` and `status` is `completed`; omitted otherwise. | | `structured_text` | string \| null | The result as [structured text](/concepts/input-formats#structured-text). Always a key on a completed response: it carries the result for a `structured` request and is `null` for `natural_language` and `json`. `null` until `status` is `completed`. | | `domain` | object \| null | The domain these results belong to — `id`, `title`, `description`. | | `created_at` / `updated_at` | ISO 8601 | When the message was created and last updated. | | `results` | object | The calculation outcome. Present when `status` is `completed`. | | `errors` | array | Calculation errors. Present when `status` is `error`. | | `version` | integer | The shape of this response body, currently `2`. Not the `/v1` API version, and not `parsed_json.version`. See [Response version](/api/overview#response-version). | The echo of your input depends on the format. A `structured` request returns its result as `structured_text`. A `natural_language` request returns the `natural_language` reading plus the parsed input as `parsed_json`. A `json` request returns the parsed input as `parsed_json`. `structured_text` is always a key on a completed response — `null` for `natural_language` and `json` — while `natural_language` and `parsed_json` are present only for the formats noted above and omitted otherwise. ### Completed A completed calculation returns `status: "completed"` and a `results` object: the [action](/concepts/scenarios) taken, the scenario indexes, the activities applied, and every variable with its value, [provenance](/concepts/provenance), and display format. `results.calculated` is the id of the variable that was solved for (`null` when `action` is `fetch`). ```json theme={null} { "conversation_id": "550e8400-e29b-41d4-a716-446655440000", "message_id": "0b7e...e1", "idempotency_key": "a1b2c3", "status": "completed", "source": "api", "input_format": "structured", "structured_text": "loan_amount: 400000 USD\nmonthly_payment: 2398.20 USD", "domain": { "id": "...", "title": "Mortgage", "description": "..." }, "created_at": "2026-06-05T18:30:00Z", "updated_at": "2026-06-05T18:30:00Z", "results": { "action": "new", "scenario": { "original": 0, "current": 1 }, "activities": [ { "id": "...", "title": "Monthly payment", "description": "...", "equation": "...", "state": "active", "variables": [{ "id": "...", "calculable": true }] } ], "variables": [ { "id": "...", "key": "loan_amount", "title": "Loan amount", "description": "...", "source": "user_input", "historical": false, "value": "400000 USD", "display": { "data_type": "number", "kind": "number", "format": "decimals_2" } }, { "id": "...", "key": "monthly_payment", "title": "Monthly payment", "description": "...", "source": "calculated", "historical": false, "value": "2398.20 USD", "display": { "data_type": "number", "kind": "number", "format": "decimals_2" } } ], "calculated": "..." }, "version": 2 } ``` Each variable's `key` is its creator-defined [variable](/concepts/variables) name. `source` is `user_input`, `calculated`, or `default_value`, and `historical` indicates whether the value was carried forward from a prior scenario. `value` is a combined `"value unit"` string at full precision — the unit is part of the value string (see [Units and precision](/concepts/units-and-precision)). For a table, `value` is an array of such strings (nested for a 2-D table); see [Tables](/math/types/tables#as-a-stored-value). The `scenario` object reports the `original` scenario the calculation started from (`0` starts a new scenario) and the `current` scenario the results belong to. `action` is one of `new`, `extend`, `discard`, or `fetch` — see [Scenarios](/concepts/scenarios#what-if-exploration). When `action` is `fetch`, retrieve that scenario with [`GET /v1/conversations/:id/context`](/api/scenario-context). #### Display formats Each variable carries a `display` object. `data_type` names its **shape** — `number`, `table`, `bar_chart`, or `pie_chart` — and the remaining fields depend on it. The shape of the object is not the kind of the value: a date, a percent, and a plain number all arrive as `number`, and `kind` is what tells them apart. Two fields recur across shapes: | Field | Type | Values | Description | | -------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `kind` | string | `number`, `no_separator`, `percent`, `duration`, `date`, `datetime`, `time` | What the value is. Currency is conveyed by the value's unit, not here. The last four are [dates and durations](#dates-and-durations). | | `format` | string | `decimals_0`–`decimals_9`, `decimals_float`, or a [date or duration format token](/authoring/dates-and-durations#choosing-a-format) | How the value is shown, from the vocabulary its `kind` takes: a decimal count for a number, a token such as `date_med` or `duration_hms` for one of the four above. | **`number`** — any single value: a scalar or unit number, a percent, or one of the four [dates and durations](#dates-and-durations). ```json theme={null} { "data_type": "number", "kind": "number", "format": "decimals_2" } ``` **`table`** — a grid with per-column formats and optional row names. ```json theme={null} { "data_type": "table", "start_row_index": 1, "columns": [ { "name": "Payment", "kind": "number", "format": "decimals_2" } ], "rows": { "names": ["Year 1", "Year 2"] } } ``` | Field | Type | Description | | ----------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `start_row_index` | integer | Index of the first row ([tables](/math/types/tables) are 1-indexed). | | `columns[]` | array | Per-column format — each has `name`, `kind`, and `format`, from the same vocabularies as a scalar variable's. A **column** carries the kind for the cells beneath it, so a `date` column formats its cells as dates while the `number` column beside it is unaffected. `format` may be `null`. | | `rows.names` | string\[] | Optional row labels. | **`bar_chart`** — a table presented as a bar [chart](/authoring/charts). ```json theme={null} { "data_type": "bar_chart", "series_by": "columns", "stacked": false, "x_axis": { "title": "Year" }, "labels": { "source": "index", "start_series_index": 1 }, "series": [ { "name": "Payment", "kind": "number", "format": "decimals_2" } ] } ``` | Field | Type | Values | Description | | --------------------------- | ------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `series_by` | string | `columns`, `rows` | What one series is. `columns` plots each column of the table as a series, leaving each row an x-axis category; `rows` plots each row, leaving each column a category. | | `stacked` | boolean | | Whether series are stacked. | | `x_axis.title` | string | | X-axis label. | | `labels.source` | string | `index`, `first_series` | Where bar labels come from. | | `labels.start_series_index` | integer | | The number the labels count up from. With `first_series` it also fills in for a label cell that is empty. | | `series[]` | array | | Per-series format — each has `name`, `kind`, and `format`, from the same vocabularies as a column's. `format` may be `null`. When `labels.source` is `first_series`, `series[0]` describes the **label** series, not a plotted one — see below. | The `series` array above names each series (`name`) and says what its values are and how they are shown (`kind` and `format`). It should match the number of series in the variable's `value` itself. What each of these settings does to the chart is in [Authoring charts](/authoring/charts#bar-charts). `series_by` names what **one series** is. With `columns`, each column of the table is a plotted series and each row is an x-axis category. With `rows`, each row is a series and each column is a category. `columns` is the common case, because that is the shape most tables arrive in: a measure per column, a period per row. When `labels.source` is `index`, all data from the table is plotted and the x-axis labels are derived as integers starting with `start_series_index`. When `labels.source` is `first_series`, the first series in the table is used as the x-axis labels instead and not included in the chart's plotted bars. How that data is formatted is derived from the metadata within `series[0]`. Your first plotted bar is therefore the second series, described by `series[1]`. **`pie_chart`** — a table presented as a pie [chart](/authoring/charts). ```json theme={null} { "data_type": "pie_chart", "kind": "number", "format": "decimals_2", "slices": { "names": ["Principal", "Interest"] } } ``` | Field | Type | Description | | -------------- | --------- | ---------------------- | | `slices.names` | string\[] | Optional slice labels. | #### Dates and durations A `kind` of `duration`, `date`, `datetime`, or `time` marks a value stored as a **count of seconds** carrying the unit `s`. The API renders nothing: the number arrives with its kind and its format token beside it, and you render it yourself. ```json theme={null} { "key": "closing_date", "value": "1787184000 s", "display": { "data_type": "number", "kind": "date", "format": "date_med" } } ``` Rendering one takes three facts, and the second one fails silently. 1. **Seconds, not milliseconds.** `new Date(1784592000)` is 1970-01-21. Multiply by 1000. 2. **Format in UTC.** The number is a naive wall clock — its components *are* the value, and it is not an instant in any particular zone. A zone-aware formatter moves the day. 3. **Branch on `kind`.** It changes the arithmetic, not just the format. A `duration` is a span with no date involved and never becomes a date. A `time` counts from midnight, which is the same number on 1970-01-01, so a UTC-pinned time-only format works. A `date` and a `datetime` count from the epoch. **Skip the UTC pin and the day moves.** In Los Angeles, `toLocaleDateString()` renders `1784592000` as **2026-07-20** for a value that means 2026-07-21 — off by one day, for roughly half the world, and only in some seasons. Nothing reports an error. What each format token renders is listed in [Choosing a format](/authoring/dates-and-durations#choosing-a-format). #### Chart scaling A chart whose cells carry **different units of the same dimension** — `3 ft`, `4 in`, `5 yd` — is drawn to one common unit, so its bars and slices are proportioned honestly; see [Mixed units in a chart](/authoring/charts#mixed-units-in-a-chart). The API renders nothing, so it hands you those converted magnitudes: the variable carries them alongside `value` and `display`, in these fields. They are present only when relevant. `value` is unchanged and is what labels and tooltips are drawn from. | Field | Type | When present | Description | | -------------------- | --------- | --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | `scale` | number\[] | Chart cells share a dimension but span two or more units. | The magnitudes restated in one common unit, in the same shape as `value`. Use for bar heights and slice angles; `null` entries mirror empty cells. | | `scale_unit` | string | With `scale`. | The common unit the `scale` magnitudes are in — use it as the axis unit. | | `scale_incompatible` | boolean | Chart cells span different dimensions. | `true` when the column mixes dimensions — for example length and mass — and has no common axis, so it cannot be charted. | | `scale_types` | string\[] | With `scale_incompatible`. | The conflicting [unit](/concepts/units-and-precision) type names, such as `["Length", "Mass"]`. | A chart variable is in exactly one of three states: 1. **Scaled** — `scale` and `scale_unit` are present. Plot from `scale`, label the axis with `scale_unit`, and show the original `value` cell in tooltips. 2. **Incompatible** — `scale_incompatible` and `scale_types` are present. There is no common axis; render a short message instead of a chart. 3. **Neither** — no scale fields. The column is a single unit or unitless; plot the numeric part of each `value` cell directly, with the first cell's unit as the axis unit. `scale` values are full precision — format them with the variable's `display` decimal settings, the same as `value`. ```json theme={null} { "key": "chart", "value": ["3 ft", "4 in", "5 yd"], "display": { "data_type": "bar_chart" }, "scale": [3, 0.3333333333333333, 15], "scale_unit": "ft" } ``` The bars sit at `3`, `0.333`, and `15` on a `ft` axis, while the second bar's tooltip reads `4 in` — the original authored value, not `0.333 ft`. When the cells span different dimensions, no chart is drawn: ```json theme={null} { "key": "chart", "value": ["3 ft", "4 kg"], "display": { "data_type": "bar_chart" }, "scale_incompatible": true, "scale_types": ["Length", "Mass"] } ``` ### In progress Natural-language requests will be processed asynchronously; structured-text and JSON requests run synchronously and never return `in_progress`. When a natural-language request is still running, the API responds with `202 Accepted`, `status: "in_progress"` and no `results`: ```json theme={null} { "conversation_id": "550e8400-e29b-41d4-a716-446655440000", "message_id": "0b7e...e1", "status": "in_progress", "source": "api", "input_format": "natural_language", "domain": { "id": "...", "title": "Mortgage", "description": "..." }, "created_at": "2026-06-05T18:30:00Z", "updated_at": "2026-06-05T18:30:00Z", "version": 2 } ``` To get the result for natural language requests, re-post the same body with the same `Idempotency-Key` until `status` is `completed` or `error`. Leave a 1–2 second gap between retries. This could take multiple retries but rarely more than 20 total. You can also poll the conversation with [`GET /v1/conversations/:id`](/api/conversations). ### Error A calculation that cannot complete returns `status: "error"` with an `errors` array. Each entry has a `code` and `message`, plus conditionally included `args`, `offset`, and `missing` fields (present only when the error uses them). ```json theme={null} { "conversation_id": "550e8400-e29b-41d4-a716-446655440000", "message_id": "0b7e...e1", "status": "error", "errors": [ { "code": "input.incompatible_units", "message": "The unit '%arg0%' and the unit '%arg1%' are not compatible", "args": ["m", "in²"] } ], "version": 2 } ``` See [Errors](/api/errors#calculation-errors) for what `args`, `offset`, and `missing` contain, and the full list of calculation error codes. ## Transport errors Problems with the request itself — rather than with the calculation — return a `4xx` or `5xx` status and a single `error` object: ```json theme={null} { "error": { "code": "...", "message": "...", "params": null } } ``` | Status | Meaning | | ------ | ------------------------------------------------------------- | | `400` | Invalid JSON or malformed structure. | | `401` | Missing or invalid credentials. | | `403` | API key disabled. | | `409` | Idempotency conflict — same key reused with a different body. | | `422` | Semantically invalid input, such as a missing required field. | | `5xx` | Unexpected server failure. | # Conversations Source: https://docs.truemath.ai/api/conversations List conversations and retrieve a conversation's full message history, including the results and errors of each calculation. A [conversation](/concepts/scenarios) holds the sequence of calculations you run together, across one or more scenarios. These endpoints let you list conversations and read the full history of one. ## List conversations ```http theme={null} GET /v1/conversations ``` Returns conversations most-recently-active first. | Query param | Type | Default | Description | | ---------------- | ------- | ------- | ---------------------------------------------------- | | `limit` | integer | 20 | Page size, 1–100. | | `starting_after` | UUID | — | Cursor — pass the previous response's `next_cursor`. | ```json theme={null} { "conversations": [ { "id": "550e8400-...", "title": "What is the monthly payment?", "notes": null, "domain_id": "7b1e...", "last_activity_at": "2026-06-01T17:04:22Z" } ], "next_cursor": "550e8400-..." } ``` | Field | Type | Description | | ------------------ | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | UUID | The conversation. | | `title` | string \| null | Human-friendly title, auto-set from the conversation's first message and editable in the app. | | `notes` | string \| null | Freeform user notes, set in the app. | | `domain_id` | UUID \| null | The conversation's [domain](/concepts/domains), set from its first message. `null` for older conversations created before this field existed. | | `last_activity_at` | ISO 8601 | When the conversation was last active. | When `next_cursor` is `null`, there are no more pages. See [Limits](/api/limits#pagination). ## Retrieve a conversation ```http theme={null} GET /v1/conversations/:id ``` Returns the conversation's scenario position and its messages in chronological order, oldest first. Each message mirrors the shape returned by [`POST /v1/calculate`](/api/calculate): `completed` messages include `results`, `error` messages include `errors`, and `in_progress` messages omit both. ```json theme={null} { "conversation_id": "550e8400-...", "title": "What is the monthly payment?", "notes": null, "domain": { "id": "7b1e...", "title": "Mortgage", "description": "..." }, "current_scenario_index": 2, "last_scenario_index": 2, "last_activity_at": "2026-06-01T17:06:30Z", "messages": [ { "message_id": "0b7e...e1", "idempotency_key": "a1b2c3", "status": "completed", "source": "api", "input_format": "natural_language", "domain": { "id": "...", "title": "Mortgage", "description": "..." }, "natural_language": "What is the monthly payment on a $400,000 loan at 6% over 30 years?", "parsed_json": { "version": 2, "inputs": [ { "key": "loan_amount", "value": "400000 USD" }, { "key": "interest_rate", "value": "6%" }, { "key": "loan_term", "value": "30 yr" } ], "calculate": { "key": "monthly_payment", "unit": null }, "scenario": "new" }, "structured_text": null, "created_at": "2026-06-01T17:04:20Z", "updated_at": "2026-06-01T17:04:22Z", "results": { "...": "see POST /v1/calculate" }, "version": 2 }, { "message_id": "1c8f...a2", "idempotency_key": null, "status": "error", "source": "api", "input_format": "structured", "domain": { "id": "...", "title": "Mortgage", "description": "..." }, "structured_text": null, "created_at": "2026-06-01T17:05:01Z", "updated_at": "2026-06-01T17:05:01Z", "errors": [ { "...": "see Errors" } ], "version": 2 }, { "message_id": "2d9a...b3", "idempotency_key": "d4e5f6", "status": "in_progress", "source": "api", "input_format": "natural_language", "domain": { "id": "...", "title": "Mortgage", "description": "..." }, "structured_text": null, "created_at": "2026-06-01T17:06:30Z", "updated_at": "2026-06-01T17:06:30Z", "version": 2 } ] } ``` | Field | Type | Description | | ------------------------ | --------------- | ---------------------------------------------------------------------------------------------------------- | | `conversation_id` | UUID | The conversation. | | `title` | string \| null | Human-friendly title, auto-set from the conversation's first message and editable in the app. | | `notes` | string \| null | Freeform user notes, set in the app. | | `domain` | object \| null | The conversation's [domain](/concepts/domains) — `id`, `title`, `description`. Set from the first message. | | `current_scenario_index` | integer \| null | The [scenario](/concepts/scenarios) currently in focus, or `null` if none. | | `last_scenario_index` | integer \| null | The most recently created scenario, or `null` if none. | | `last_activity_at` | ISO 8601 | When the conversation was last active. | | `messages` | array | The messages, oldest first. | Each message carries the same fields as a [`POST /v1/calculate`](/api/calculate) response, except that `conversation_id` is lifted to the top level: | Field | Type | Description | | --------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `message_id` | UUID | This message. | | `idempotency_key` | string \| null | The key sent with the request, echoed back. | | `status` | string | `completed`, `error`, or `in_progress`. | | `source` | string | `api` or `chat`. | | `input_format` | string | `natural_language`, `structured`, or `json`. | | `domain` | object \| null | The domain — `id`, `title`, `description`. | | `natural_language` | string \| null | The natural-language reading. Present only when `input_format` is `natural_language` and `status` is `completed`; omitted otherwise. | | `parsed_json` | object | The parsed input object — `inputs`, `calculate`, and `scenario` in the same shape as a [JSON request](/concepts/input-formats#json), plus a `version` naming the parse-contract shape it conforms to (currently `2`). Present only when `input_format` is `natural_language` or `json` and `status` is `completed`; omitted otherwise. | | `structured_text` | string \| null | The result as [structured text](/concepts/input-formats#structured-text). Carries the result for a `structured` request and is `null` for `natural_language` and `json`; `null` until `status` is `completed`. | | `created_at` / `updated_at` | ISO 8601 | When the message was created and last updated. | | `results` | object | Present when `status` is `completed`. See [Calculate](/api/calculate#completed). | | `errors` | array | Present when `status` is `error`. See [Errors](/api/errors#calculation-errors). | | `version` | integer | The shape of this response body, currently `2`. Not the `/v1` API version, and not `parsed_json.version`. See [Response version](/api/overview#response-version). | A request for a conversation that does not exist returns `404`; see [Errors](/api/errors#transport-errors) for transport-level failures. `title` and `notes` are read-only over the API. They are set in the TrueMath app — `title` is auto-generated from the first message and editable there, and `notes` is entered by the user. There is no API endpoint to edit them. To read the resolved state of a scenario — its domains, activities, and variables — rather than the message history, use [Scenario context](/api/scenario-context). # Domains Source: https://docs.truemath.ai/api/domains List the published domains available to your account, and read a domain's variables and activities. These endpoints let you discover the [domains](/concepts/domains) available to your account and inspect their [variables](/concepts/variables) and [activities](/concepts/activities). Domains, activities, and variables are addressed by `id`. Only [published](/concepts/versioning) domains are returned. Each response carries a top-level `version` reporting the shape of its body, currently `2`. It is not the `/v1` API version — see [Response version](/api/overview#response-version). ## List domains ```http theme={null} GET /v1/domains ``` ```json theme={null} { "domains": [ { "id": "6ba7b810-...", "title": "Mortgage", "description": "..." } ], "version": 2 } ``` ## List a domain's variables ```http theme={null} GET /v1/domains/:id/variables ``` Returns the domain and its variables, each with its default value and the two fields needed to render it. Useful for building [structured text](/concepts/input-formats#structured-text). ```json theme={null} { "domain": { "id": "6ba7b810-...", "title": "Mortgage", "description": "..." }, "variables": [ { "id": "...", "key": "home_price", "title": "Home price", "description": "...", "default_value": "0 USD", "kind": "number", "format": "decimals_2" } ], "version": 2 } ``` Each variable carries exactly these fields: `id`, `key`, `title`, `description`, `default_value`, `kind`, and `format`. | Field | Type | Description | | --------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `default_value` | string \| array | The value applied when none is provided in a calculation, written as a combined `"value unit"` string (see [Provenance](/concepts/provenance)), or an empty string when the variable has no default. A [table](/authoring/tables) variable's default is an array of combined cell strings. | | `kind` | string | `number`, `percent`, `no_separator`, `duration`, `date`, `datetime`, `time`, `table`, `bar_chart`, or `pie_chart`. | | `format` | string \| null | How the value is shown, from the vocabulary its `kind` takes. `null` for `table`, `bar_chart`, and `pie_chart`, whose formats are set per column or per series — read those from [`/activities`](#list-a-domains-activities). | ### Rendering a default value `default_value` is not self-describing, so pair it with `kind` and `format`: * A `percent` stores a decimal — `0.06` renders as `6%`. * A `duration`, `date`, `datetime`, or `time` stores a **count of seconds**. Render it with a UTC-pinned formatter; the notes under [Dates and durations](/api/calculate#dates-and-durations) apply to a default verbatim. * A default of one of those four kinds may instead be the literal word `today` or `now`, which stands for the moment the calculation runs rather than the moment the domain was authored. See [`today` and `now` as defaults](/authoring/dates-and-durations#today-and-now-as-defaults). ## List a domain's activities ```http theme={null} GET /v1/domains/:id/activities ``` Returns a domain's **definition**: its [activities](/concepts/activities) — each with its equation, state, and the [variables](/concepts/variables) it references — and its variables with their stored display configuration. This describes how the domain is *configured*, in contrast to [`POST /v1/calculate`](/api/calculate), which returns *computed values* with a render-ready `display` for each. The two carry similar display information in different shapes — see [Display configuration vs. display](#display-configuration-vs-display). ```json theme={null} { "domain": { "id": "6ba7b810-...", "title": "Mortgage", "description": "...", "has_draft": false, "has_published": true }, "activities": [ { "id": "...", "title": "Monthly payment", "description": "...", "equation": "...", "state": "active", "variable_refs": [ { "variable_id": "...", "calculable": true } ] } ], "variables": [ { "id": "...", "key": "interest_rate", "title": "Interest rate", "description": "...", "default_value": "0", "kind": "percent", "display_metadata": { "number": { "format": "decimals_2" } } } ], "version": 2 } ``` **Domain** — the published domain, with flags for whether a draft and a published version exist: | Field | Type | Description | | ----------------------- | ------- | ------------------------------------ | | `id` | UUID | The domain. | | `title` / `description` | string | Human- and agent-readable labels. | | `has_draft` | boolean | Whether an unpublished draft exists. | | `has_published` | boolean | Whether a published version exists. | **Activities** — one entry per [activity](/concepts/activities): | Field | Type | Description | | ----------------------- | ------ | --------------------------------------------------------------------------------------------- | | `id` | UUID | The activity. | | `title` / `description` | string | Labels. | | `equation` | string | The activity's equation. | | `state` | string | `active` or `disabled`. Deleted activities are not returned. | | `variable_refs[]` | array | The variables the activity references — each a `variable_id` with whether it is `calculable`. | **Variables** — one entry per [variable](/concepts/variables), in definition form: | Field | Type | Description | | ----------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | UUID | The variable. | | `key` | string | The author-chosen name used in [equations](/authoring/writing-equations) and in [input](/concepts/input-formats#structured-text) — `home_price`, `loan_term`. | | `title` / `description` | string | Labels. | | `default_value` | string \| array | The value applied when none is provided, as a combined `"value unit"` string, or an empty string when the variable has no default. A [table](/authoring/tables) variable's default is an array of combined cell strings. | | `kind` | string | What the value is: `number`, `percent`, `no_separator`, `duration`, `date`, `datetime`, `time`, `table`, `bar_chart`, or `pie_chart`. The four time kinds are described in [Authoring dates and durations](/authoring/dates-and-durations). | | `display_metadata` | object | The variable's display configuration (see below). | #### display\_metadata `display_metadata` holds the variable's display configuration under a single key naming its structural type — `number` (for `number`, `percent`, and `no_separator`), `table`, `bar_chart`, or `pie_chart`: ```json theme={null} "display_metadata": { "number": { "format": "decimals_2" } } ``` The inner object carries the same fields as the matching [`display` object](/api/calculate#display-formats) in a calculation result — `format` for a scalar; `start_row_index`, `columns`, and `rows` for a table; series or slice configuration for a chart. Unlike a calculation result's `display`, the definition is not normalized: optional fields may be absent rather than filled with defaults. The `number` key holds the configuration for every scalar type, the four time kinds included, so a `date` variable's [format token](/authoring/dates-and-durations#choosing-a-format) is at `display_metadata.number.format`: ```json theme={null} "display_metadata": { "number": { "format": "date_med" } } ``` #### Display configuration vs. display The same variable's formatting appears in two shapes, and `kind` does not cover the same ground in each: | | `/domains/:id/activities` (definition) | `/calculate` (result) | | ------------ | -------------------------------------------------------------------------------- | ------------------------------------------------------------- | | Structure | nested under the type key: `display_metadata..{...}` | flat: `display.{...}` | | Type field | `kind` covers the whole variable, `table`, `bar_chart`, and `pie_chart` included | split into `data_type` (structural) and `kind` (scalar only) | | Completeness | as stored — optional fields may be absent | normalized — defaults filled, empty collections shown as `{}` | That split is the gotcha: in a definition `kind` can be `table`, but in a result `kind` is only ever one of the scalar kinds and the structural shape is in `data_type`. # Errors Source: https://docs.truemath.ai/api/errors How the TrueMath API reports transport-level errors via HTTP status codes, and calculation errors within a successful response. TrueMath distinguishes two kinds of failure: **transport errors** (the request itself was malformed or unauthorized) and **calculation errors** (the request was valid but the calculation could not complete). ## Transport errors Transport errors use a standard HTTP status code and a JSON body of the form: ```json theme={null} { "error": { "code": "error_unauthorized", "message": "..." } } ``` | Status | Code | Meaning | | ------ | ---------------------------- | --------------------------------------------------------------------------------------------- | | `400` | `error_invalid_json` | The request body is not valid JSON. | | `401` | `error_unauthorized` | Missing, malformed, or invalid API key. | | `403` | `error_disabled_api_key` | The API key has been revoked or disabled. | | `409` | `error_idempotency_conflict` | An `Idempotency-Key` was reused with a different body. See [Limits](/api/limits#idempotency). | | `422` | `error_invalid_param` | A parameter is missing or out of range. | | `5xx` | `error_server_internal` | An unexpected server error. | ## Calculation errors A calculation that cannot complete returns an HTTP `200` with `status: "error"` in the body and an `errors` array — one entry per problem. Each entry has: * **`code`** — a stable, machine-readable identifier for the failure. Branch on this, not on `message`. The full set is listed [below](#calculation-error-codes). * **`message`** — an English description **template** containing numbered `%argN%` placeholders. It is *not* pre-filled; substitute the `args` before displaying it (see [Building the message](#building-the-message)). * **`args`** — a positional array of string values for the `%argN%` placeholders in `message` (`%arg0%` → `args[0]`, `%arg1%` → `args[1]`, and so on). Read them directly when you need the raw values, such as the unit names. See [Building the message](#building-the-message). * **`offset`** — present only on `input.syntax_error`: an integer, the zero-based character offset of the error within the [equation](/authoring/writing-equations). It is a caret position *between* characters — `0` is before the first character, `1` is after the first, `2` after the second, and so on. * **`missing`** — when the calculation is blocked on inputs, the variables you could supply to continue, grouped as alternatives. See [Missing inputs](#missing-inputs). `args`, `offset`, and `missing` are each **conditionally included** — present only when the error uses them, and omitted (never `null`) otherwise. A consumer must not assume the keys exist. An incompatible-units error, for example, carries the offending units in `args`, to substitute into the `message` template: ```json theme={null} { "status": "error", "errors": [ { "code": "input.incompatible_units", "message": "The unit '%arg0%' and the unit '%arg1%' are not compatible", "args": ["m", "in²"] } ], "version": 2 } ``` #### Building the message `message` is a template, not a finished string. Each placeholder is a numbered token `%argN%` (zero-indexed), filled from `args` **by index** — `%arg0%` is replaced with `args[0]`, `%arg1%` with `args[1]`, and so on (the reference client matches `/%arg(\d+)%/g`): ```yaml theme={null} template: "The unit '%arg0%' and the unit '%arg1%' are not compatible" args: ["m", "in²"] result: "The unit 'm' and the unit 'in²' are not compatible" ``` Substitution rules: * If `args` is absent or empty, render `message` as-is — it has no placeholders to fill. * Replace each `%argN%` with `args[N]`. The values are already strings in the response. * Match by index, not order of appearance: `%arg1%` always means `args[1]`, even if it appears before `%arg0%` in the text. * If a placeholder's index has no matching value (`N` is at or beyond the length of `args`), leave the literal `%argN%` token in place — do not blank it. If you only need the offending values — to highlight a field, say — read them straight from `args` and ignore `message`. The same substitution applies to every error, engine- or request-originated. #### Missing inputs When an error is blocked on inputs, `missing` lists the variables you could supply to continue. It is an **array of option groups** — each inner array is one set of variables that must be supplied **together** to unblock the calculation. More than one group means there are **alternative** ways to proceed. Read `missing` as: provide every variable in any one group, then retry. Each entry is a fully formed [variable](/concepts/variables) object — `id`, `key`, `title`, `description`, and a `display` render snapshot (the same `display` shape as a [calculation result](/api/calculate#display-formats)) — but with **no `value`**, since these are candidates you can supply, not resolved values. ```json theme={null} { "status": "error", "errors": [ { "code": "calculation.unable_to_resolve_target", "message": "Not able to resolve this combination of inputs and requested calculation", "missing": [ [ { "id": "...", "key": "down_payment", "title": "Down payment", "description": "...", "display": { "data_type": "number", "kind": "number", "format": "decimals_2" } } ] ] } ], "version": 2 } ``` The example has one group with one variable, but a group may list **several** variables that must be provided together, and there may be several groups. For instance, one group might list both `down_payment` and `loan_term` while another lists only `loan_amount` — satisfying *either* group (all of its variables) lets the calculation continue. ### Calculation error codes Codes are namespaced by category — `input.*` for a malformed or under-specified request, `calculation.*` for a valid request the system cannot solve, and `math.*` for an undefined numeric result. Branch on the exact `code`. **Input errors** — the request is malformed or under-specified: | Code | Meaning | | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `input.unknown_domain` | The requested domain does not exist. | | `input.unpublished_domain` | The domain has not been published; publish it before use. | | `input.unknown_scenario` | The calculation references a scenario that does not exist. | | `input.missing_calculate_target` | No calculation target was named — the request did not specify a variable to solve for. | | `input.unknown_calculate_target` | A target was named, but it is not a variable in this domain. | | `input.invalid_prompt` | The input could not be parsed. | | `input.invalid_format` | A value provided for a key has an invalid format. | | `input.invalid_input_schema` | A `json` request body parsed as JSON but does not match the [input schema](/concepts/input-formats#json). A `prompt` that cannot be parsed as JSON at all returns the transport error [`error_invalid_json`](#transport-errors) instead. | | `input.invalid_variable_name` | A variable name is empty or uses illegal characters — names must start with a letter and contain only letters, numbers, and underscores. | | `input.invalid_argument` | A function was called with the wrong number of arguments, or with an argument that is not valid for it. | | `input.invalid_dimensions` | A table input is structurally invalid — rows have differing column counts, or a table is nested inside another table. | | `input.invalid_unit` | A unit in the input is unknown or invalid. | | `input.incompatible_units` | An equation or definition combines units that are not dimensionally compatible — for example, adding meters to seconds. | | `input.incompatible_type` | A value's type is not valid where it was used — an operation between types that do not support it, such as adding, dividing, comparing, or negating them, or an argument passed to a function that does not accept that type. | | `input.out_of_range` | A value you supplied falls outside the limits accepted for it — a table index beyond the bounds of the table, or a negative period count where only non-negative values are accepted. Distinct from `math.out_of_range`, which reports a computed result. | | `input.missing_value` | A required variable referenced by the calculation has no value supplied. | | `input.syntax_error` | An equation or variable definition could not be compiled — it contains a syntax error. Carries an `offset` to the location. | | `input.timeout_error` | Natural-language processing took too long; try again. | | `input.llm_no_parsed_data` | No recognized inputs or a calculation target were found for the selected domain. The unified "nothing recognized" signal across all input formats. | | `input.unknown_llm_error` | An unexpected error occurred while translating natural language. | `input.missing_calculate_target` and `input.unknown_calculate_target` are distinct codes: the first means no target was named in the request; the second means the named target is not a variable in this domain. Three codes cover the ways an argument can be wrong: the wrong **number** of arguments, or an argument value the function does not accept, is `input.invalid_argument`; an argument of the wrong **type** is `input.incompatible_type`; an argument outside the **limits** for that value is `input.out_of_range`. A type mismatch never returns `input.invalid_argument` — code that branches on it for type errors should handle `input.incompatible_type` as well. **Calculation errors** — the inputs are valid but the system cannot solve for the target: | Code | Meaning | | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `calculation.circular_reference` | A variable depends on itself, directly or through a cycle of other variables. | | `calculation.conflicting_results` | The requested target resolved to more than one value from the supplied inputs — an inconsistent, over-determined system. | | `calculation.undefined_variable` | An equation references a variable name that is not defined anywhere in the domain. | | `calculation.unable_to_resolve_target` | The supplied inputs and requested target are not solvable — not enough is known to reach the target. Often accompanied by a `missing` list of inputs that would unblock it. | | `calculation.unknown_error` | An unexpected error during execution. | **Math errors** — a numeric result is undefined or unrepresentable: | Code | Meaning | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `math.divide_by_zero` | A division by zero occurred during evaluation. | | `math.positive_infinity` | Evaluation produced positive infinity. | | `math.negative_infinity` | Evaluation produced negative infinity. | | `math.not_a_number` | Evaluation produced an undefined numeric result (NaN) — for example, `0/0`. | | `math.out_of_range` | Evaluation produced a value too large to represent — a magnitude beyond ±1e308. A value *you supplied* that is outside its limits is `input.out_of_range` instead. | # Limits Source: https://docs.truemath.ai/api/limits Pagination and request idempotency for the TrueMath REST API. ## Pagination List endpoints use cursor-based pagination. Request a page size with `limit` (1–100, default 20), and pass the `next_cursor` from the previous response as `starting_after` to fetch the next page: ```bash theme={null} curl "https://api.truemath.ai/v1/conversations?limit=50" \ -H "Authorization: Bearer tm_live_..." # then, using next_cursor from the response: curl "https://api.truemath.ai/v1/conversations?limit=50&starting_after=550e8400-..." \ -H "Authorization: Bearer tm_live_..." ``` When `next_cursor` is `null`, you have reached the last page. See [Conversations](/api/conversations). ## Idempotency Send an `Idempotency-Key` header on [`POST /v1/calculate`](/api/calculate) to make the request safe to retry. The key is an opaque string you choose (a UUID is a good choice). * **Same key, same body** — returns the original result rather than running the calculation again. This is also how you poll a natural-language request that is still `in_progress`: retry with the same key until it completes. * **Same key, different body** — rejected with `409 error_idempotency_conflict`. ```bash theme={null} curl https://api.truemath.ai/v1/calculate \ -H "Authorization: Bearer tm_live_..." \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 9f1c8e2a-..." \ -d '{ "domain_id": "...", "input_format": "structured", "prompt": "..." }' ``` # API overview Source: https://docs.truemath.ai/api/overview Base URL, versioning, authentication, and conventions for the TrueMath REST API. The TrueMath API lets you run calculations and read domains over HTTP from your own application or agent. ## Base URL ```http theme={null} https://api.truemath.ai ``` All endpoints are versioned under `/v1`. Requests and responses are JSON; send `Content-Type: application/json` on requests with a body. ## Authentication Every request is authenticated with a bearer API key: ```http theme={null} Authorization: Bearer tm_live_... ``` See [Authentication](/api/authentication) for how to obtain and use a key. ## Endpoints | Method | Path | Purpose | | ------ | -------------------------------------------- | ------------------------------------------------------- | | `POST` | `/v1/calculate` | [Run a calculation](/api/calculate) | | `GET` | `/v1/conversations` | [List conversations](/api/conversations) | | `GET` | `/v1/conversations/:id` | [Retrieve a conversation's history](/api/conversations) | | `GET` | `/v1/conversations/:id/context` | [Retrieve all scenarios](/api/scenario-context) | | `GET` | `/v1/conversations/:id/context/:scenario_id` | [Retrieve one scenario](/api/scenario-context) | | `GET` | `/v1/domains` | [List published domains](/api/domains) | | `GET` | `/v1/domains/:id/variables` | [List a domain's variables](/api/domains) | | `GET` | `/v1/domains/:id/activities` | [List a domain's activities](/api/domains) | ## Conventions * **Identifiers** — domains, conversations, and messages are identified by UUIDs. * **Keys** — a [variable](/concepts/variables) is referenced by its short key, which the API returns on each variable, and a UUID. Domains, activities, conversations, and messages are referenced by UUID. UUIDs are static; keys can change. * **Values carry units** — a value is returned as a string with its unit included, for example `"375000 USD"` or `"30 yr"`. See [Units and precision](/concepts/units-and-precision). * **Provenance** — each returned variable reports a `source` and whether it was carried forward. See [Provenance](/concepts/provenance). * **Timestamps** — ISO 8601. * **Published only** — the API returns and calculates against [published](/concepts/versioning) domains. * **Response version** — a `version` field reports the shape of the response body. See [Response version](#response-version). ## Response version A `version` field reports the **shape of the response body** — which keys are present and what they are named. It is at the top level of a [calculation](/api/calculate) result and of each [domain](/api/domains) read response, and on each message in a [conversation's history](/api/conversations). ```json theme={null} { "domains": [ { "id": "6ba7b810-...", "title": "Mortgage", "description": "..." } ], "version": 2 } ``` It is not the version of the API: the path stays `/v1` whatever this number is. `version` is `2` today. A different number means the body's shape is not the one your code was written against, so read it and treat an unfamiliar value as a signal to check your parsing. A calculation result carries a second `version` unrelated to this one. `parsed_json.version` reports the shape of the parsed input inside `parsed_json`, not of the response body around it. It is also `2` today, and the two are versioned separately. See [Calculate](/api/calculate#response). ## Errors Transport-level problems use standard HTTP status codes with a JSON error body; calculation problems are returned in a successful response with a status of `error`. See [Errors](/api/errors). # Scenarios Source: https://docs.truemath.ai/api/scenario-context Retrieve the resolved domains, activities, and variables for every scenario in a conversation, or for a single scenario. These endpoints return the resolved state of a conversation's [scenarios](/concepts/scenarios) — the domains, activities, and variables in effect — rather than the message-by-message history returned by [`GET /v1/conversations/:id`](/api/conversations). ## All scenarios ```http theme={null} GET /v1/conversations/:id/context ``` Returns every scenario in the conversation, each with its domains, activities, and variables. ```json theme={null} { "conversation_id": "550e8400-...", "scenarios": [ { "id": 1, "domains": [{ ... }], "activities": [{ ... }], "variables": [ { "id": "...", "key": "monthly_payment", "title": "Monthly payment", "source": "calculated", "historical": false, "value": "2398.20 USD", "display": { ... } } ] } ] } ``` The `domains`, `activities` and `variables` objects use the same shape as the `results` object in [`POST /v1/calculate`](/api/calculate). A conversation with no completed calculations returns an empty `scenarios` array. ## A single scenario ```http theme={null} GET /v1/conversations/:id/context/:scenario_id ``` Returns one scenario by its index. ```json theme={null} { "conversation_id": "550e8400-...", "scenario": { "id": 1, "domains": [ "..." ], "activities": [ "..." ], "variables": [ "..." ] } } ``` # Authoring charts Source: https://docs.truemath.ai/authoring/charts Display a table-valued variable as a bar or pie chart, and configure its series, labels, and slices. A chart is a **presentation of a [table](/authoring/tables)**, not a value of its own. You display a table variable as a chart by giving it the `bar_chart` or `pie_chart` kind (see [Defining variables](/authoring/defining-variables)). The variable's value stays a table — the chart only changes how that table is shown. ## Bar charts A `bar_chart` plots a table's values as bars. Each **series** is one column or one row of the table, drawn as a set of bars, and the table's shape is what decides how many series there are. You configure: * **Series orientation** — whether one series is a **column** or a **row** of the table. See [Series orientation](#series-orientation). * **Stacking** — whether the series are drawn side by side or stacked into a single bar per group. * **Axis title** — the label shown under the x-axis. * **Labels** — whether the x-axis labels are a running number or the table's first series. See [X-axis labels](#x-axis-labels). * **Series format** — for each series, a name, a kind, and a format. The name labels that series in the legend; the kind and format apply to its values, the same scalar formatting a [column](/authoring/tables#configuring-columns) uses. ### Series orientation The orientation setting says what **one series** is. Under column series, each column of the table is plotted as a series and each **row** is an x-axis category. Under row series, each row is plotted and each **column** is a category. Column series are the common case, because that is how most tables are shaped. An [amortization schedule](/math/functions/finance#amortization-table-amortization), or a [`columns`](/math/functions/tables#column-column-columns) slice of one, has a period per row and a measure per column, so a schedule sliced to a payment column and a balance column, plotted by column, is two series with one x-axis category per period. Choose row series only for a table laid out the other way: one row per measure, one column per period. ### X-axis labels The x-axis labels come from a running number or from the table's first series. When the labels are a running number, they are integers counting up from a starting number you set, and every series in the table is plotted. When the labels come from the first series, you build the table with a series of label values in front — a column of dates, or of period names — and the chart draws the axis from that series instead of plotting it. The labels take their kind and format from it, so a `date` series gives an axis of dates. Where a label value is empty, the axis falls back to a positional name for that category, `Row 3` under column series or `Column 3` under row series, numbered from the same starting number. ## Pie charts A `pie_chart` plots one series as slices of a whole. A pie plots a **single row**. Give it a flat list of values, or a table of one row. Where the table has more than one row, only the first is plotted. You configure: * **Slice names** — a label per slice, paired with the values in order. Where a name is missing or empty, that slice reads `Slice 3`. * **Kind and format** — the scalar kind and format applied to slice values. Unlike a bar chart, which formats each series separately, a pie takes one setting for every slice. A pie chart is suited to parts of a single total — principal versus interest, a budget split across categories — where a bar chart is suited to comparing values across a series. ## Charting dates and durations A chart series and a pie slice take the same kinds a [column](/authoring/tables#configuring-columns) does, the four time kinds included, and the axis labels, tooltips, and legend format through whichever token you give them. A pie of durations — time spent per activity, as proportions of a whole — is the everyday case. Plotting instants works too, though a bar's height is then measured from 1970, which rarely makes a readable chart. Dates are usually more useful as the axis labels than as the bars. ## Mixed units in a chart When the column behind a chart holds values in **different units of the same dimension** — `3 ft`, `4 in`, and `5 yd` in one column, say — the chart draws every bar or slice to a single common unit, so their sizes are proportioned honestly rather than taken from the raw numbers. Each label and tooltip still shows the value's original unit, so the `4 in` bar reads `4 in` even though it is drawn on a `ft` axis. The common unit is the first cell's unit, and a pie chart's percentages are taken from those common-unit magnitudes. This needs no authoring action — a mixed-unit column charts correctly on its own. To pin the axis to a particular unit instead of the first cell's, [cast](/math/units#casting-to-a-specific-unit) the column to that unit in the chart variable's equation; casting converts the data, so the labels then show the unit you chose. If a column instead mixes **different dimensions** — a length and a mass, say — there is no common axis to draw against, so the chart shows a short message naming the conflicting unit types rather than a misleading chart. Keep a chart's column to a single dimension. ## The chart is a view of the table Because a chart is display configuration over a table, the data behind it is authored, supplied, and returned as a table in every case: you build it from a [table variable](/authoring/tables), users provide its values through [structured-text input](/concepts/input-formats#tables), and a calculation returns it as a table in the [result](/api/calculate). Choosing a chart over a grid is purely a presentation decision — the math is unchanged. # Creating a domain Source: https://docs.truemath.ai/authoring/creating-a-domain Create a new calculation domain — set its title and description, build it up from activities and variables, and import, export, copy, or archive it. A [domain](/concepts/domains) is the self-contained model you author and target when you run a calculation. Creating one gives you a **draft** — a working copy you build up and later publish. Calculations only ever run against the published version, so a new domain is yours to shape until you decide it is ready. See [Versioning](/concepts/versioning). ## What you set when you create a domain * **Title and description** — human- and agent-readable labels. The description is more than display text: clear, precise descriptions improve natural-language and agent results. Write it precisely, the way you would write the one line a search engine will index. * **Identity** — TrueMath assigns a stable unique identifier (UUID) (the `domain_id` you pass to the [API](/api/calculate)) and a stable **key**. You do not choose these; they identify the domain for as long as it exists. The UUID is how you reference the domain through the API; the key is how TrueMath recognizes the same domain across accounts when you [copy](#copy-to-another-account) it. The key is not part of a domain's exported definition. See [Managing a domain](#managing-a-domain) and [Domains](/concepts/domains#identity-and-versions). ## How a domain takes shape A domain is a set of [activities](/concepts/activities) and the [variables](/concepts/variables) they relate. You build it from the activities outward: 1. **Write activities.** Each activity is an equation that relates a set of variables. See [Writing activities](/authoring/writing-activities). 2. **Write the equations.** The expression syntax — literals, operators, functions, and units. A variable first comes into being when an equation references it. See [Writing equations](/authoring/writing-equations). 3. **Define the variables.** Configure each variable's display, default, and unit. See [Defining variables](/authoring/defining-variables). 4. **Add tables and charts** where a variable holds more than one value or is presented visually. See [Tables](/authoring/tables) and [Charts](/authoring/charts). 5. **Test and publish.** Verify the domain calculates as expected, then publish the draft. See [Testing and publishing](/authoring/testing-and-publishing). ## Where a domain starts * **From scratch** — an empty draft you build up from your own activities and variables. * **From a library domain** — start from a pre-built domain and adapt it to your conventions and business logic. See [Customizing a library domain](/authoring/customizing-a-library-domain) and the [Domain library](/domain-library/overview). * **From a JSON definition** — import an existing [domain definition](/authoring/domain-definition). See [Managing a domain](#managing-a-domain). ## Managing a domain Beyond editing its activities and variables, a domain can be exported, imported, copied, and archived. A domain's exported definition carries its activities and variables, but not its identity — neither the UUID nor the [key](#what-you-set-when-you-create-a-domain). Identity matters when you **copy** a domain between accounts: the copy shares the source's key (under its own UUID in the destination), which is how TrueMath recognizes the two as the same domain and updates the copy in place rather than duplicating it. ### Export and import JSON A domain's full definition can be **exported as JSON** — the complete set of activities and variables, ready to copy. See [The domain definition](/authoring/domain-definition) for the structure of that JSON. **Importing** accepts a pasted JSON definition and shows a comparison of the changes against the domain you are importing into before you accept them, so you can review exactly what an import will add, remove, or alter rather than applying it blind. An import overrides the domain you selected: the definition carries no key, so it is your selection — not anything in the JSON — that determines which domain is replaced. ### Copy to another account If you belong to more than one account, you can **copy a domain to a different account**. This is how a domain authored in one account is brought into another. The option is available to the domain's owner. The copy shares the source domain's key but takes its own UUID in the destination account, so copying the same domain again later updates that copy rather than creating a second one — the key is what TrueMath matches on to recognize the two as the same domain. ### Archiving **Archiving** retires a domain and hides it from use, the final stage of the domain [lifecycle](/concepts/versioning). An archived domain is no longer returned by the [API](/api/domains) or available in the [Playground](/playground/tour). There is currently no interface for reviewing or recovering archived domains. Archive a domain only when you are sure you no longer need it. A draft is not usable for calculations until it is published. Only published domains are returned by the [API](/api/domains) and available in the [Playground](/playground/tour). See [Testing and publishing](/authoring/testing-and-publishing). # Customizing a library domain Source: https://docs.truemath.ai/authoring/customizing-a-library-domain Adapt a pre-built domain to your own conventions, regulatory requirements, or business logic. # Authoring dates and durations Source: https://docs.truemath.ai/authoring/dates-and-durations Give a variable a duration, date, datetime, or time-of-day kind, choose the format it renders in, and set a default written the way people write dates. TrueMath has four kinds for dates and durations: `duration`, `date`, `datetime`, and `time`. Setting one on a [variable](/concepts/variables) declares what its number counts and how it is shown. The value itself stays an ordinary number. Each of these is a count of **seconds** carrying the unit `s`, in the same `"value unit"` form every other quantity uses (see [Units and precision](/concepts/units-and-precision)). What separates a date from a duration is the kind, not the number. This page covers authoring: the four kinds, their formats, and their defaults. For the arithmetic these values take part in, see [Dates](/math/types/dates) — and read [`mo` and `yr` shift by an average](/math/types/dates#mo-and-yr-shift-by-an-average-not-a-calendar-step) before writing an equation that shifts a date by a term in months. For the spellings a value can be written in, see [Dates, times, and durations](/concepts/input-formats#dates-times-and-durations) under Input formats. ## The four kinds | Kind | What it is | The number counts | Example value | Renders as | | ---------- | ---------------------- | ----------------------------------- | ---------------- | ------------------ | | `duration` | A span of time | Seconds elapsed | `"5405 s"` | 1:30:05 | | `date` | A calendar day | Seconds since `1970-01-01 00:00:00` | `"1784592000 s"` | 7/21/2026 | | `datetime` | A day and a clock time | Seconds since `1970-01-01 00:00:00` | `"1784644205 s"` | 7/21/2026, 2:30 PM | | `time` | A time of day | Seconds since midnight | `"52205 s"` | 2:30 PM | A `time` counts from midnight rather than from the epoch, which is the same number a `datetime` would hold on 1970-01-01. In the Builder these appear in the **Kind** list beside `number`, `percent`, and `no_separator`, each labeled by an example: Duration (1:30:00), Date (7/21/2026), Date & Time (7/21/2026, 2:30 PM), Time of Day (2:30 PM). Everything else about the variable is unchanged. It takes a key, a title, a description, and a default like any other, and the [guidance for defining any variable](/authoring/defining-variables) applies unaltered. The one difference is that there is no unit to choose: these values are always in seconds, so the unit field is not offered. ## Choosing a format Every variable has a **format**, drawn from the vocabulary its kind takes. On an ordinary number that is a decimal count, `decimals_2`. On one of these four it is a **format token** matching the kind: a `date` takes a `date_*` token, and pairing it with `decimals_2` is rejected when you save. ### Duration formats Rendering `"5405 s"`: | Token | Renders | Reads as | | --------------- | --------- | ------------------------------------ | | `duration_hms` | 1:30:05 | 1 hr 30 min 5 sec | | `duration_hm` | 1:30 | 1 hr 30 min | | `duration_ms` | 90:05 | 90 min 5 sec | | `duration_ms_3` | 90:05.000 | 90 min 5 sec to three decimal places | **The leading group is unbounded.** `duration_ms` renders `"5405 s"` as `90:05` rather than `1:30:05` — ninety minutes, not one hour and thirty. A format with no hours group does not drop the hours or roll them over; it counts them as further minutes. So a span of any size renders in whichever format you pick, and nothing is lost off the large end. What can be lost is precision at the small end — see [Two formats do not round-trip](#two-formats-do-not-round-trip). Rounding is applied to the **total** before it is split into groups, so 59.6 seconds renders as `1:00` and never as `0:60`. ### Date formats Rendering `"1784592000 s"`: | Token | Renders | | --------------- | ---------------------- | | `date_short` | 7/21/2026 | | `date_med` | Jul 21, 2026 | | `date_long` | July 21, 2026 | | `date_full` | Tuesday, July 21, 2026 | | `date_my_short` | 7/2026 | | `date_my_med` | Jul 2026 | | `date_my_long` | July 2026 | | `date_iso` | 2026-07-21 | ### Date and time formats Rendering `"1784644205 s"`: | Token | Renders | | ---------------- | ------------------------ | | `datetime_short` | 7/21/2026, 2:30 PM | | `datetime_med` | Jul 21, 2026, 2:30 PM | | `datetime_long` | July 21, 2026 at 2:30 PM | | `datetime_iso` | 2026-07-21 14:30:05 | ### Time-of-day formats Rendering `"52205 s"`, seconds past midnight: | Token | Renders | | ------------ | ---------- | | `time_short` | 2:30 PM | | `time_med` | 2:30:05 PM | ### US formatting throughout TrueMath formats dates and times US-style today: numeric dates month-first, English month and weekday names, and a 12-hour clock with AM/PM. That applies on input and on display alike, for every reader, so `10/5/2026` is October 5th everywhere. The two `_iso` tokens are the exception. `date_iso` renders `2026-07-21` and `datetime_iso` renders `2026-07-21 14:30:05`, which read the same way for everyone. Reach for one where the value is going somewhere ambiguity would cost you. ## Setting a default A [default](/authoring/defining-variables#default-value-and-unit) on one of these variables is written the way you would write the value by hand. There is no unit to pick and no seconds to work out. | The variable is | You write | Stored as | | --------------- | ------------------- | ---------------- | | `date` | `7/21/2026` | `"1784592000 s"` | | `datetime` | `7/21/2026 2:30 PM` | `"1784644200 s"` | | `time` | `2:30 PM` | `"52200 s"` | | `duration` | `22:47` | `"1367 s"` | | `duration` | `90 min` | `"90 min"` | Every spelling a user can type is a spelling you can author a default in. The accepted spellings are listed under [Input formats](/concepts/input-formats#dates-times-and-durations); the formats a stored value is *shown* in are [above](#choosing-a-format). A colon value on a `duration` is read through the variable's format token, so pick the format before you type the default: `22:47` is 1367 seconds under `duration_hms`, `duration_ms`, or `duration_ms_3`, and 82020 seconds under `duration_hm`. See [One genuine ambiguity: the colon](/concepts/input-formats#one-genuine-ambiguity-the-colon). Two kinds of default are stored as you wrote them rather than converted to seconds: * **A value that already names a time unit** — `90 min`, `1 hr 30 min` — stays as written and is converted when the calculation runs. * **A keyword** — `today` and `now` are stored as the word. See [`today` and `now` as defaults](#today-and-now-as-defaults). A bare number is a count of seconds already, and is stored with the unit: a `duration` default of `90` becomes `"90 s"`. ### What the Builder shows back Once saved, a default is shown back through the variable's own kind and format token, **not** as the text you typed. Enter `Jul 21, 2026` on a variable formatted `date_short` and it reads back as `7/21/2026`. The two cases above are the exceptions: neither a keyword nor a value carrying its own unit is a count of seconds, so both are shown exactly as written. **A default that cannot be read is refused when you save**, by name, with a message listing the spellings that do fit. `2026-02-29` is not a date — 2026 is not a leap year — so it fails at the save rather than becoming something surprising later. ### `today` and `now` as defaults A default of `today` or `now` is stored as **the word**, not as the date you authored it on. The value used is the current date at the moment the calculation runs, in the caller's own time zone (see [Time zones](/api/calculate#time-zones)). That is how a domain anchors on the current date: a quote valid from today, a closing date thirty days out. Which keyword fits depends on the kind, and what survives depends on it too: | The variable is | `today` | `now` | | --------------- | -------------- | ------------------------------------------------------ | | `date` | midnight today | midnight today — a date has no clock to carry the rest | | `datetime` | midnight today | the current date and time | | `time` | refused | the current time of day | | `duration` | refused | refused | The two refusals are refusals rather than readings. Midnight is not what anyone means by "today" on a clock, and neither word names a span. Both are caught when you save, with the same message a misspelled date gets. A `date` variable keeps only the day from `now` because a time it cannot show would make two identical-looking dates compare unequal. ### Two formats do not round-trip Both are safe, and both are worth knowing: * `date_my_short`, `date_my_med`, and `date_my_long` omit the day, so `Jul 2026` read back is the **1st** of July. * `duration_hm` rounds to the whole minute, so seconds entered are not shown. Neither loses the stored value. Changing how a default is *displayed* never rewrites it; only editing the field does. ## Dates and durations in tables and charts A [table](/authoring/tables) variable's own kind is always `table`, so a **column** carries the kind and the format token for the cells beneath it. A payment schedule can put a date column beside a currency column, and each cell is read, stored, and rendered against its own column. A [chart](/authoring/charts) series and a pie slice take the same four kinds a column does. See [Columns of dates and durations](/authoring/tables#columns-of-dates-and-durations) and [Charting dates and durations](/authoring/charts#charting-dates-and-durations). ## Formatting is not validation A `date` variable renders **whatever count of seconds it holds** as a date. Nothing checks that the number means a date, because at that point there is nothing left to check: a date is a count of seconds, and so is every number arithmetic produces from one. TrueMath computes with these as ordinary numbers and blocks exactly one operation, a date plus a bare number (see [A bare number is not a duration](/math/types/dates#a-bare-number-is-not-a-duration)). Everything else goes through. A `closing` of 7/21/2026 is `"1784592000 s"`, so `closing / 2` is half as many seconds — still seconds — and the variable shows it as **April 11, 1998**. A real-looking date, from an equation that means nothing, with no error anywhere. **A wrong date looks exactly like a right one.** An equation that produces a nonsense count of seconds produces a nonsense date, and the format token renders it as confidently as any other. This is the same trap as [`mo` and `yr` shifting a date by an average](/math/types/dates#mo-and-yr-shift-by-an-average-not-a-calendar-step), and it is why an unexpected date in a result is worth tracing back to its equation rather than to its kind. What a `date` variable does **not** show as a date is a value carrying a different unit. A variable that ends up holding `15 ft` is shown as `15 ft`, since feet are not seconds and there is no date to render. A stray unit where a date belongs is a visible signal; a wrong number is not. ## A time of day is not bounded to one day A `time` variable holds seconds since midnight, but nothing confines it to a single day, and a calculation routinely produces a value outside one. A shift starting at 1:40 PM and running 845 minutes ends at 3:45 AM the next morning, and that is the correct answer. A time-of-day format shows a clock and nothing else, so 3:45 AM tomorrow and 3:45 AM today render identically. Where the day matters to the reader as well as to the arithmetic, model the value as a `datetime` and let it carry the date. # Defining variables Source: https://docs.truemath.ai/authoring/defining-variables Configure a variable's key, labels, kind, format, and default value with its unit. A [variable](/concepts/variables) comes into being when an [equation](/authoring/writing-equations) first references it by name — see [Writing activities](/authoring/writing-activities). Defining a variable is configuring everything else about it: the labels it reads under, how its value is displayed, and the default it takes when none is provided. ## What you configure * **Key** — the identifier used in equations and in input. * **Title and description** — human- and agent-readable labels. * **LLM prompt** — optional, authoring-only instructions for interpreting natural-language input onto this variable, kept separate from the description. * **Kind and format** — what the value is, and how it is shown. * **Default value** — an optional value, with its unit, used when none is provided. ## Key and labels A variable's **key** is its identifier within the domain. It is the name you write in [equations](/authoring/writing-equations) and the name you reference when you set or solve for the variable in [structured-text input](/concepts/input-formats#structured-text). A key starts with a letter and contains letters, digits, and underscores, and is unique within the domain: `home_price`, `interest_rate`, `loan_term`. It's always lowercase. To **rename** a variable, edit its key here, in the variable's details. The change propagates to every [equation](/authoring/writing-equations) that references it, so the variable stays a single thing under its new name. Renaming inside an equation does not work this way: because a variable comes into being the moment an equation references a key, typing a name there that no longer matches an existing key creates a *new* variable instead of renaming the old one. Rename from the variable's details, not by editing equation text. The **title** and **description** are labels. They do not affect calculation, but clear, specific titles and descriptions improve natural-language and agent results, so keep them clear and specific. **Describe variables in the words users say.** Clear, inclusive titles and descriptions improve natural-language and agent results, so phrase them the way a user would — and include the variations they might use. A home-price variable's description might mention "home price, property value, purchase price." When natural-language requests keep landing on the wrong variable or none at all, the fix is almost always a clearer or more inclusive description, not a change to the equation. And when two variables could be described the same way, note in each how it differs from the other — "the rate after points, not the note rate" — so results land on the one you mean. **State the unit and form a value should take.** Where the form of a value isn't obvious, say so in the description — a term as a length of time such as `30 yr` or `360 mo`. This keeps input both correct and *consistent*. Watch for a variable whose value arrives in a different form from one run to the next — the same term coming back as a bare `360` on one request and `30 yr` on another — which usually traces to a loose description. The fix is to tighten the description so it names the unit the value should carry, not to change the equation. ## LLM prompt A variable can carry an optional **prompt** — guidance for interpreting natural-language input onto this variable. The prompt is distinct from the description, and the two serve different purposes: * The **description** is the customer-facing label. It is what the [API](/api/domains), conversation sharing, and the MCP tools return for the variable. * The **prompt** is authoring-only. It is never returned by the API, a calculation result, conversation sharing, or the MCP tools. Use it for interpretation guidance you want to apply to natural-language input but do not want to surface to callers. The prompt is optional. Left empty, the description alone guides interpretation. When you set it, it supplements the description rather than replacing it, so keep the description clear regardless of what the prompt says. Set the prompt in the Builder, in the **LLM Prompt** field below Description. It is included with the domain through [export, import, and copy](/authoring/creating-a-domain#managing-a-domain), so it round-trips for authors, while staying absent from every public response. ## Kind and format A variable's **kind** says what its value is. It is one of: * **`number`** — a plain number, shown with thousands separators and the unit its value carries, if any. A monetary amount is a `number` whose unit is a currency. * **`percent`** — a rate shown as a percentage. A unit on the value is still shown — `0.20 ft` displays as `20% ft` — though pairing a unit with a percentage is unusual. * **`no_separator`** — a number shown without thousands separators, such as a year, with the unit its value carries, if any. * **`duration`** — a span of time, such as a shift length or a processing time. * **`date`** — a calendar day, such as a closing date. * **`datetime`** — a calendar day together with a clock time. * **`time`** — a time of day, such as a shift start. * **`table`** — a collection of values in named rows and columns. See [Tables](/authoring/tables). * **`bar_chart`** / **`pie_chart`** — a table presented as a chart. See [Charts](/authoring/charts). The **format** says how the value is shown, and each kind has its own vocabulary of formats. For `number`, `percent`, and `no_separator` the format is a precision: Decimals 0 through 9 (`decimals_0` through `decimals_9`) for a fixed number of decimal places, or float (`decimals_float`) for the value's natural precision, up to a maximum of 10 decimal places. The format affects display only — the stored value keeps [full precision](/concepts/units-and-precision) and is what later steps compute with. The four date and duration kinds take a **format token** instead — `date_med`, `duration_hms` — and take their default the way you would write the value by hand, `7/21/2026` rather than a count of seconds. Everything specific to them is on one page: [Authoring dates and durations](/authoring/dates-and-durations). ## Default value and unit A variable can carry a **default value** — the value used when a calculation provides none. A value supplied from a default is recorded with default [provenance](/concepts/provenance). A default is written as a value together with its unit, the way you would write it by hand — `0 USD`, `30 yr`, `0.065`. A variable of one of the four date and duration kinds is the one exception to the unit half: its value is always in seconds, so there is no unit to pick and you write the date or the span itself — `7/21/2026`, `22:47`. See [Setting a default](/authoring/dates-and-durations#setting-a-default). The unit is part of the value, not part of the kind, and it stays attached to the value through every calculation (see [Units and precision](/concepts/units-and-precision)). This is why a currency amount is simply a `number` whose value carries a currency unit — there is no separate "money" kind. A variable with no default has none applied; if a calculation needs its value and none is provided, the value is left unset rather than assumed. **Default to what a typical user would assume** Set a default to the value most callers would expect for the domain: a US mortgage might default its term to `30 yr`, its down payment to `0.20`, and a balloon payment to `0`. The right default is the conventional answer, and it differs from one variable to the next. Leave a variable *undefaulted* when there is no safe assumption and the user should state it — a loan amount or a home price. A wrong default there is worse than none, since a missing value is left unset rather than invented. ## Let variables carry their units A dimensional quantity — a term, a distance, a price, a weight — should be a variable that carries its unit, not a bare number that assumes one. Model a loan term as a value entered in a unit of time (`30 yr` or `360 mo`, either works), not as a `loan_term_months` whose name and description ask every caller to supply a unitless count of months. Carrying the unit is what makes the value safe: TrueMath validates it against the dimension it expects, converts it without loss wherever it is used, and keeps the whole calculation in units (see [Units and precision](/concepts/units-and-precision#model-quantities-as-unit-carrying-values)). The [description guidance above](#key-and-labels) — state the form a value should take — still applies, but a carried unit *enforces* that form where a description only suggests it. Often you need no conversion at all: many functions accept unit-carrying values directly. Passing `loan_term` straight to a [finance function](/math/functions/finance) works — the function reads the time value and converts it as needed — so there is no reason to hand it a bare month count. When a calculation *does* genuinely need a bare number — a count to drive a loop, say — strip the unit with a single explicit conversion within that one equation, not as a separate unitless variable carried through the domain: a loop bound can be written `unitvalue(loan_term; "mo")` at the point of use, while `loan_term` stays a time value everywhere else. Stripping a unit is strategic and local; a value passed between activities should keep its unit. Reaching for [`unitvalue`](/math/functions/units#unit-value-unitvalue) and [`unit`](/math/functions/units) throughout a domain is usually a sign a value was declared without the unit it should carry. **Don't declare a unitless variable for a dimensional quantity.** A bare `360` and a `30 yr` describe the same term, but a variable typed to hold the bare count assumes its unit by convention — and that convention is invisible to the engine and to whoever supplies the value. Nothing catches a `30` that meant 30 *years* arriving where the domain expected 30 *months*, and the inconsistency stays silent until a downstream calculation produces a wrong result. Prefer a unit-carrying input and convert once where a bare number is actually required. The display configuration you set here is what `GET /v1/domains/:id/variables` and `GET /v1/domains/:id/activities` return for each variable, and the same information a calculation result is rendered from. See [Domains](/api/domains). # The domain definition Source: https://docs.truemath.ai/authoring/domain-definition The JSON shape of a domain — its domain object, variables, and activities — as exported, imported, and authored. The complete structure of a TrueMath domain. A [domain](/concepts/domains) has a single, portable representation: a JSON **definition** holding the domain's labels, its [variables](/concepts/variables), and its [activities](/concepts/activities). This is the form a domain takes when you [export, import, or copy](/authoring/creating-a-domain#managing-a-domain) it, and it is the complete shape of everything a domain is made of. This page is the reference for that structure — every field, its type, and its allowed values. If you are authoring by hand you rarely write this JSON directly; you build the domain up from [activities](/authoring/writing-activities), [equations](/authoring/writing-equations), and [variables](/authoring/defining-variables), and TrueMath holds the definition for you. The structure here is what those pieces add up to, and what you produce when you generate a domain programmatically. This is the **definition** shape — what a domain *is*. It is related to, but not the same as, the read shapes the API returns from [`GET /v1/domains/:id/activities`](/api/domains): the definition relates activities to variables by **key**, while the read endpoints relate them by `variable_id` and carry server-side fields such as identity and version. Use this page for the portable definition; use [Domains (API)](/api/domains) for what a calculation or read endpoint returns. ## Top-level structure A definition is an object with three members: ```json theme={null} { "domain": { "title": "...", "description": "..." }, "variables": [ /* one entry per variable */ ], "activities": [ /* one entry per activity */ ] } ``` | Member | Type | Description | | ------------ | ------ | ------------------------------------------------------------------------------------- | | `domain` | object | The domain's details — see [The domain object](#the-domain-object). | | `variables` | array | The domain's [variables](/concepts/variables) — see [Variables](#variables). | | `activities` | array | Every [activity](/concepts/activities) in the domain — see [Activities](#activities). | Variables and activities relate to one another **by key**, not by position or by id — see [How variables and activities relate](#how-variables-and-activities-relate). **What an author assigns, and what TrueMath assigns.** A variable's `key` is the only identifier you choose — the human-readable name you write in [equations](/authoring/writing-equations) and input (`loan_amount`, `interest_rate`). Everything else is assigned by TrueMath, not author-chosen: every `id` (a UUID), each activity `key` (`act_` followed by hex), and the domain's key (`dm_` followed by hex). All are stable for the life of the domain. **Generating a new domain.** Omit every system-assigned identifier: leave out each `id` and each activity `key`, and give the `domain` object only a `title` and `description`. Supply just the author content — variable `key`s, labels, `default_value`s, and display configuration, plus each activity's `equation` and `calculable` list. TrueMath assigns the identifiers on import. The `id` and activity `key` fields shown throughout this page appear in a definition *exported* from an existing domain, where TrueMath has already assigned them. ## The domain object ```json theme={null} "domain": { "title": "Mortgage", "description": "Mortgage and home-loan calculations: home price, down payment, loan amount, interest rate, monthly payment, amortization, escrow, and related figures." } ``` | Field | Type | Description | | ------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `title` | string | The domain's human- and agent-readable name. | | `description` | string | What the domain is for. Clear, precise descriptions improve natural-language and agent results, so write it precisely — see [Creating a domain](/authoring/creating-a-domain). | The definition carries no identifier in the `domain` object — only its title and description. A domain's identity is assigned and managed by TrueMath and is not part of the definition body. Importing a definition applies it to the domain you import it into, overriding that domain's current contents; see [Managing a domain](/authoring/creating-a-domain#managing-a-domain). ## Variables Each entry in `variables` describes one [variable](/concepts/variables): its key, its labels, its default, and how it is displayed. ```json theme={null} { "id": "53c87c9b-6dfb-4869-9eea-4f71560f282d", "key": "apr", "title": "Annual Percentage Rate (APR)", "description": "Annual percentage rate displayed as a percentage...", "prompt": "", "default_value": "", "kind": "percent", "display_metadata": { "number": { "format": "decimals_3" } } } ``` | Field | Type | Description | | ------------------ | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | UUID | System-assigned identifier for the variable. | | `key` | string | The variable's [key](/authoring/defining-variables#key-and-labels) — the **author-chosen** name used in equations and input. Lowercase, starts with a letter, contains letters, digits, and underscores, unique within the domain. | | `title` | string | Human- and agent-readable label. | | `description` | string | What the variable means. Clear, specific descriptions improve natural-language and agent results — see [Defining variables](/authoring/defining-variables#key-and-labels). | | `prompt` | string | Optional, authoring-only [interpretation guidance](/authoring/defining-variables#llm-prompt) for natural-language input onto this variable. An empty string `""` when unset. It round-trips through export, import, and copy, but is never present in a calculation result or any [public API](/api/domains), sharing, or MCP response. | | `default_value` | string | The [default](/authoring/defining-variables#default-value-and-unit) applied when a calculation provides none, written as a combined `"value unit"` string (`"30 yr"`, `"0 USD"`, `"0.20"`), or an empty string `""` when the variable has no default. | | `kind` | string | What the value is — one of the [kinds](#kinds) below. | | `display_metadata` | object | The display configuration, keyed by the variable's structural type — see [display\_metadata](#display-metadata). | ### Kinds A variable's `kind` says what its value is. How the value is read on input, how it is shown, and which vocabulary its `format` is drawn from all follow from it. It is one of: | Value | Meaning | | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `number` | A plain number, shown with thousands separators and the [unit](/concepts/units-and-precision) its value carries, if any. A monetary amount is a `number` whose unit is a currency. | | `percent` | A rate shown as a percentage. A unit on the value is still shown — `0.20 ft` displays as `20% ft` — though pairing a unit with a percentage is unusual. | | `no_separator` | A number shown without thousands separators, such as a year, with the unit its value carries, if any. | | `duration` | A span of time, held as a count of seconds — see [Authoring dates and durations](/authoring/dates-and-durations). | | `date` | A calendar day, held as a count of seconds from the Unix epoch. | | `datetime` | A calendar day and a clock time, held the same way. | | `time` | A time of day, held as a count of seconds from midnight. | | `table` | A collection of values in rows and columns — see [Tables](/authoring/tables). | | `bar_chart` | A table presented as a bar chart — see [Charts](/authoring/charts). | | `pie_chart` | A table presented as a pie chart — see [Charts](/authoring/charts). | ### display\_metadata `display_metadata` holds the variable's display configuration under a **single key naming its structural type**. Every scalar kind — `number`, `percent`, `no_separator`, and the four time kinds — nests under `number`; the others nest under `table`, `bar_chart`, or `pie_chart`. #### Scalar variables — `number` For `number`, `percent`, `no_separator`, `duration`, `date`, `datetime`, and `time`: ```json theme={null} "display_metadata": { "number": { "format": "decimals_2" } } ``` | Field | Type | Description | | -------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `format` | string | How the value is rendered, from the vocabulary its `kind` takes. Display only — the stored value keeps [full precision](/concepts/units-and-precision). | For `number`, `percent`, and `no_separator` that vocabulary is a decimal count: `decimals_0` through `decimals_9` for a fixed number of decimal places, or `decimals_float` for the value's natural precision (at most 10 decimal places). For the four date and time kinds the same slot holds a **format token** matching the kind — `date_med` for a `date`, `duration_hms` for a `duration`. A token from the wrong vocabulary is rejected on save. See [Choosing a format](/authoring/dates-and-durations#choosing-a-format) for the full list. ```json theme={null} "display_metadata": { "number": { "format": "date_med" } } ``` For those four kinds a `default_value` is stored as a combined `"value unit"` string in seconds — `"1784592000 s"` — or as the literal word `today` or `now`. You author it in the form you would write by hand; see [Setting a default](/authoring/dates-and-durations#setting-a-default). #### Table variables — `table` ```json theme={null} "display_metadata": { "table": { "start_row_index": 1, "columns": [ { "name": "Payment", "kind": "number", "format": "decimals_2" }, { "name": "Principal Paid", "kind": "number", "format": "decimals_2" }, { "name": "Interest Paid", "kind": "number", "format": "decimals_2" }, { "name": "Ending Balance", "kind": "number", "format": "decimals_2" } ], "rows": { "names": [] } } } ``` | Field | Type | Description | | ----------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `start_row_index` | integer | The number shown as the label of the first row — a display indicator like a spreadsheet's row numbers, not the table's intrinsic indexing (which is always 1-based in equations). Usually `1`; sometimes `0`, e.g. a cash-flow table whose initial flow is period 0. See [Tables](/authoring/tables#configuring-columns). | | `columns` | array | The table's columns. **Columns are defined; rows are data** — see [Tables](/authoring/tables#rows-versus-columns). Each column is an object with `name` (string), `kind` (any scalar kind: `number`, `percent`, `no_separator`, `duration`, `date`, `datetime`, or `time`), and `format` from that kind's vocabulary. | | `rows` | object | `rows.names` is an array of [row labels](/authoring/tables#row-labels), or an empty array `[]` when the table has no row labels. | Columns carry no unit field: each cell carries its own unit with its value — see [Tables as a stored value](/math/types/tables#as-a-stored-value). #### Bar chart variables — `bar_chart` A chart is a [presentation of a table](/authoring/charts); the variable's value stays a table and `display_metadata.bar_chart` configures how it is drawn. ```json theme={null} "display_metadata": { "bar_chart": { "series_by": "columns", "stacked": false, "x_axis": { "title": "Periods" }, "labels": { "source": "index", "start_series_index": 1 }, "series": [ { "name": "Ending Balance", "kind": "number", "format": "decimals_2" } ] } } ``` | Field | Type | Description | | ----------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `series_by` | string | What one series is. `"columns"` plots each column of the underlying table as a series, leaving each row an x-axis category; `"rows"` plots each row, leaving each column a category. Required. See [Series orientation](/authoring/charts#series-orientation). | | `stacked` | boolean | Whether series are stacked into one bar per group (`true`) or drawn side by side (`false`). | | `x_axis` | object | `x_axis.title` is the axis label. | | `labels` | object | Where bar labels come from. `source` is `"index"` (a running number) or `"first_series"` (the table's first series, which then supplies the labels instead of a plotted bar). `start_series_index` is the number those labels count up from. | | `series` | array | One entry per plotted series, each with `name`, `kind`, and `format` — the same scalar formatting a [column](#table-variables-table) uses, the four time kinds included. When `labels.source` is `"first_series"`, the first entry describes the label series rather than a plotted one. | #### Pie chart variables — `pie_chart` ```json theme={null} "display_metadata": { "pie_chart": { "kind": "number", "format": "decimals_2", "slices": { "names": ["Payment", "Private Mortgage Insurance", "Property Tax", "Insurance"] } } } ``` | Field | Type | Description | | -------- | ------ | ------------------------------------------------------------------------------------------------------------------ | | `kind` | string | The scalar kind applied to slice values, the four time kinds included. | | `format` | string | How slice values are rendered, from that kind's vocabulary. | | `slices` | object | `slices.names` is an array of optional [slice labels](/authoring/charts#pie-charts), or an empty array when unset. | ## Activities Each entry in `activities` describes one [activity](/concepts/activities): its equation and which of the variables it references it may solve for. ```json theme={null} { "id": "215122f5-b4b1-4512-8286-0b01682dac7d", "key": "act_e986dd2e74a415149c58", "title": "Loan – Interest Rate – Term – Payment", "description": "Calculates a mortgage's loan amount, interest rate, periods, or monthly payment.", "equation": "payment = -pmt(loan_amount; 0; interest_rate; periods; 12; 12; 0)", "calculable": ["loan_amount", "interest_rate", "periods", "payment"] } ``` | Field | Type | Description | | ------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `id` | UUID | System-assigned identifier for the activity. | | `key` | string | The activity's identifier — system-assigned, in the form `act_` followed by hex. It is not author-chosen (unlike a variable key) and is not referenced in equations. On re-import into the same domain it acts as an idempotency key: activities are matched by key to decide which to update and which to create. | | `title` | string | Human-readable label stating what the activity computes. | | `description` | string | What the activity computes — documentation for the people reading the domain. | | `equation` | string | The activity's [equation](/authoring/writing-equations) in TrueMath's expression syntax. It references variables by key; a variable comes into being the moment an equation references its key. | | `state` | string | `active` or `disabled` — see [Disabling an activity](/authoring/writing-activities). Absent means `active`, so the field appears only on a disabled activity. Deleted activities are not part of a definition. | | `calculable` | array | The variable **keys** this activity is allowed to [solve for](/authoring/writing-activities#calculable-variables). An activity may solve for any one variable in this list depending on which others are known; a variable the equation references but that is *not* listed here is only ever read as an input. | ## How variables and activities relate Within a definition, activities and variables are connected by **key**, with no separate wiring: * An activity's `equation` references variables by their `key`. Reusing the same key across activities refers to the same variable — this is how activities compose into a domain. See [How activities compose](/authoring/writing-activities#how-activities-compose). * An activity's `calculable` array lists variable keys — among the variables its `equation` references, the ones the activity may solve for, rather than only read as input. Because the relationship is carried by key, the keys in `equation` and `calculable` must match the `key` of an entry in `variables`. The `id` fields identify existing elements; they are not how activities point at variables. The read endpoint [`GET /v1/domains/:id/activities`](/api/domains) returns the same configuration in a different shape: it relates each activity to its variables through a `variable_refs` array of `variable_id`s, and addresses both by `id`. Use the key-based shape on this page when generating or reading a portable **definition**. ## Complete example A self-contained excerpt of a mortgage domain — scalar variables, a table, a bar chart, and a pie chart, with the activities that drive them. Every key referenced in an `equation` or `calculable` list is defined below as a variable: ```json theme={null} { "domain": { "title": "Mortgage", "description": "Mortgage and home-loan calculations: home price, down payment, loan amount, interest rate, monthly payment, amortization, escrow, and related figures." }, "variables": [ { "id": "9e747a43-bee9-4943-9674-125f46349355", "key": "loan_amount", "title": "Loan Amount", "description": "The mortgage amount or amount of the loan for the property.", "default_value": "", "kind": "number", "display_metadata": { "number": { "format": "decimals_2" } } }, { "id": "484c52a2-f711-4301-b784-ac13a9e4a420", "key": "interest_rate", "title": "Interest Rate", "description": "Annual interest rate expressed as a percentage.", "default_value": "", "kind": "percent", "display_metadata": { "number": { "format": "decimals_3" } } }, { "id": "067f0036-52d5-47b7-8dc7-c5aa0529304a", "key": "periods", "title": "Term", "description": "Number of loan months or years.", "default_value": "30 yr", "kind": "number", "display_metadata": { "number": { "format": "decimals_float" } } }, { "id": "0fd3b8f8-a61e-447d-84fd-d3a35d63f145", "key": "payment", "title": "Payment", "description": "Mortgage or loan payment amount per month.", "default_value": "", "kind": "number", "display_metadata": { "number": { "format": "decimals_2" } } }, { "id": "8eb7362c-6839-4e9f-9629-483239b84129", "key": "property_taxes", "title": "Property Taxes", "description": "Annual property tax amount for the escrow payment.", "default_value": "", "kind": "number", "display_metadata": { "number": { "format": "decimals_2" } } }, { "id": "d270dd0d-7e58-43dd-9624-ab60a525e630", "key": "insurance", "title": "Insurance", "description": "Annual home owner's insurance amount for the escrow payment.", "default_value": "", "kind": "number", "display_metadata": { "number": { "format": "decimals_2" } } }, { "id": "7b544646-83e8-4651-8f76-4d7ca2bdf984", "key": "amortization", "title": "Amortization", "description": "Present an amortization table during the life of the loan.", "default_value": "", "kind": "table", "display_metadata": { "table": { "start_row_index": 1, "columns": [ { "name": "Payment", "kind": "number", "format": "decimals_2" }, { "name": "Principal Paid", "kind": "number", "format": "decimals_2" }, { "name": "Interest Paid", "kind": "number", "format": "decimals_2" }, { "name": "Ending Balance", "kind": "number", "format": "decimals_2" } ], "rows": { "names": [] } } } }, { "id": "9bfbc482-784e-42a6-8528-209ffcda08ed", "key": "ending_balance_chart", "title": "Ending Balance Pay Down Chart", "description": "Chart showing the ending balance at each period throughout the life of the loan.", "default_value": "", "kind": "bar_chart", "display_metadata": { "bar_chart": { "series_by": "columns", "stacked": false, "x_axis": { "title": "Periods" }, "labels": { "source": "index", "start_series_index": 1 }, "series": [ { "name": "Ending Balance", "kind": "number", "format": "decimals_2" } ] } } }, { "id": "72407c73-8bfc-487c-8a69-0e066520da1a", "key": "escrow_payment_chart", "title": "Escrow Payment Chart", "description": "Chart showing percentages of an escrow payment by principal plus interest, property taxes, and insurance.", "default_value": "", "kind": "pie_chart", "display_metadata": { "pie_chart": { "kind": "number", "format": "decimals_2", "slices": { "names": ["Payment", "Property Tax", "Insurance"] } } } } ], "activities": [ { "id": "215122f5-b4b1-4512-8286-0b01682dac7d", "key": "act_e986dd2e74a415149c58", "title": "Loan – Interest Rate – Term – Payment", "description": "Calculates a mortgage's loan amount, interest rate, periods, or monthly payment.", "equation": "payment = -pmt(loan_amount; 0; interest_rate; periods; 12; 12; 0)", "calculable": ["loan_amount", "interest_rate", "periods", "payment"] }, { "id": "a029cb79-3adc-4864-80d3-0128f126c529", "key": "act_3b1f9c2a7d4e0a6f8b5c", "title": "Amortization Table", "description": "Equation to present an amortization table.", "equation": "amortization = amortization(1; periods; loan_amount; 0; -payment; interest_rate; periods; 12; 12; 0; 1)", "calculable": ["amortization"] }, { "id": "85b8b91d-15af-4255-a53d-a9b82466619d", "key": "act_7c0d4a9e2f1b6038d5ae", "title": "Ending Balance Pay Down Chart", "description": "Extracts the ending-balance column from the amortization table to drive the pay-down chart.", "equation": "ending_balance_chart = column(amortization; 4)", "calculable": ["ending_balance_chart"] }, { "id": "7d558a96-1345-4b40-b13b-f8ee22d6e30c", "key": "act_5d2e8f1a0c7b3946e1f2", "title": "Escrow Payment Chart", "description": "Displays a pie chart showing escrow payments for the first period.", "equation": "escrow_payment_chart = [payment; property_taxes / 12; insurance / 12]", "calculable": ["escrow_payment_chart"] } ] } ``` # Natural language domain creation Source: https://docs.truemath.ai/authoring/natural-language-domain-creation Describe a calculation workflow in plain language and generate a structured domain definition. # Authoring overview Source: https://docs.truemath.ai/authoring/overview How to create, version, publish, and archive calculation domains in TrueMath. Coming soon. # Authoring tables Source: https://docs.truemath.ai/authoring/tables Define a table-valued variable — its columns, per-column format, row labels, starting row index, and an optional default table. A **table** variable holds a collection of values in rows and columns rather than a single number — an amortization schedule, a series of cash flows, a set of line items, a lookup grid. Whether a variable holds a table is determined by its value — produced by a calculation or supplied as input — not by its kind. The `table` kind (see [Defining variables](/authoring/defining-variables)) is how you get the table-specific display options: named columns, per-column formats, and row labels. A table value given a scalar kind such as `number` still renders as a table, but every cell takes that kind's formatting — thousands separators, the configured decimal places — and the columns fall back to generic headers. This page covers the authoring side; for the underlying value model — how tables are written, stored, and combined — see [Tables](/math/types/tables). ## When to use a table Use a table variable when a quantity is naturally many values rather than one: a payment broken out month by month, a list of material costs, a grid of rate-by-term lookups. A variable that holds a single value should stay a scalar `number`, `percent`, or `no_separator`. ## Rows versus columns The authoring distinction that matters: **columns are defined, rows are data.** You name and format the columns when you author the variable; the rows are produced by a calculation or supplied as input, and there can be any number of them. Tables are **1-indexed** — the first row and column are index `1`. So you configure the shape across, not down. ## Configuring columns Each column has: * **A name** — the column's label. * **A kind** — any of the scalar kinds a plain variable uses: `number`, `percent`, `no_separator`, `duration`, `date`, `datetime`, or `time`. * **A format** — how the column's values are shown, from the vocabulary that kind takes: a decimal count such as `decimals_2`, or a [date or duration format token](/authoring/dates-and-durations#choosing-a-format) such as `date_short`. You also set the **starting row index** — the number shown as the label of the first row, like the row numbers down the side of a spreadsheet. It is a display indicator only and does not change how the table is indexed in equations, which is always 1-based. The starting row index is usually `1`; however, setting it to `0` is handy, for example, when displaying a cash-flow table whose initial flow is period 0 (`CF[0]`) — see [reading the cash flow table](/math/functions/finance#reading-the-cash-flow-table). Columns have no unit field: each cell carries its own unit, supplied with the value, so the variable's own unit is empty (see [Tables as a stored value](/math/types/tables#as-a-stored-value)). ## Columns of dates and durations A table variable's own kind is always `table`, so a **column** carries the kind and the format for the cells beneath it. That is what lets a payment schedule put a date column beside a currency column: each cell is read, stored, and rendered against its own column, and the column beside it is unaffected. ```yaml theme={null} schedule: [2/1/26; 1000][3/1/26; 500] ``` Everything on [Authoring dates and durations](/authoring/dates-and-durations) applies one level down. A cell takes any spelling a scalar takes, `today` and `now` included, read through its column's kind. `today` names a day and `now` names a day and a time, and the column decides how much of that survives: a `datetime` column keeps both from `now`, a `date` column keeps only the day from either, a `time` column takes `now` and refuses `today`, and a `duration` column refuses both. Two duration columns with different format tokens read the same `22:47` differently, because the token is a property of the column. **A cell that cannot be read is named by its column** — `'Date' cell '2/45/26' is not a valid date` — because there is otherwise no way to identify which of forty values is wrong. A cell written as a date in a column that is not one of those kinds is named the same way, rather than reported as an arithmetic error. ## Row labels A table can optionally carry **row labels**, so its rows read by name rather than only by index — for example, labeling an amortization table's rows by month, or a cost breakdown's rows by line item. Row labels are a presentation aid; they name the rows without changing the values in them. Row labels are an independent option: a table can have row labels with or without a [default table](#default-table), or none at all, but accessed and set in the default table view. ## Default table A table variable can carry a **default table** — a complete table value used when a calculation provides none, the multi-value counterpart to a scalar [default value](/authoring/defining-variables#default-value-and-unit). A value supplied from a default is recorded with default [provenance](/concepts/provenance). A default table is also independent of row labels: you can set a default table on its own, pair it with row labels, or use neither. A table variable with no default has none applied. ## Presenting a table as a chart The same table variable can be displayed as a bar or pie chart instead of a grid — the chart is a presentation choice over the underlying table. See [Charts](/authoring/charts). # Testing and publishing Source: https://docs.truemath.ai/authoring/testing-and-publishing Test a domain draft by running calculations against it, then publish it so calculations can use it — or revert the draft. Everything you author starts as a **draft**. Calculations through the [API](/api/calculate) and the [Playground](/playground/tour) run only against the **published** version. Testing a draft and then publishing it is how the work you do in the builder becomes live. See [Versioning](/concepts/versioning). ## Testing a draft You test a draft in the builder's **Test view**, before publishing it. The Test view provides a form for entering values for the domain's [variables](/concepts/variables), runs the calculation against the draft, and shows the result. Scalar and unit values are shown with the display you configured for each variable. Tables and charts are output as TrueMath-formatted tables. Testing against the draft is the only way to run a domain before it is published — the public API never calculates against a draft. Use it to confirm that: * **Activities solve in the directions you intend** — each question you expect the domain to answer resolves to a value, which depends on having marked the right variables [calculable](/authoring/writing-activities#calculable-variables). * **Defaults apply as expected** — variables fall back to their [default values](/authoring/defining-variables#default-value-and-unit) when you leave them blank. * **Units resolve** — values combine as intended, and incompatible combinations are rejected rather than coerced. See [Combining types](/authoring/writing-equations#combining-types). * **Results display correctly** — numbers, percentages, tables, and charts read the way you configured them. **Round-trip each variable to catch drift.** When an [activity](/concepts/activities) can solve for more than one of its variables, take a scenario you trust and re-derive each variable from the others. In the Test view, every [calculable](/authoring/writing-activities#calculable-variables) variable has a **?** button that recalculates it in place — tap it on each one and watch the value. Because activities are reversible, it should not change. If a value shifts when you solve for it a different way, the domain has **drift**. Track it down and fix it before publishing. Not every shift is drift. Two functions are legitimately not one-to-one, and a value re-derived through either can come back different: * [`adjdate`](/math/functions/dates#month-ends) adjusted by months or years — its month-end rules land several starting dates on the same result, so a month forward and a month back need not return the original day. * [`ddays`](/math/functions/dates#day-counts-ddays) with basis `1` — several dates share the same 30/360 count, so a date re-derived from one may be a different date that satisfies it. A shift in an activity that uses one of those two may be correct. Anywhere else, treat it as drift. ## Common validation errors When a test calculation fails, the Test view shows the error rather than a result. Most failures fall into a handful of categories, each with a clear fix in the draft. The same conditions surface over the API as machine-readable codes — see [Errors](/api/errors#calculation-error-codes) for the full catalog and the codes to branch on. **An equation won't compile.** A [variable definition](/authoring/defining-variables) or [activity](/authoring/writing-activities) equation contains a syntax error — an unbalanced parenthesis, a stray operator, a malformed function call. Fix the [equation](/authoring/writing-equations) text. Over the API this is `input.syntax_error`, which carries an `offset` to the character position of the fault. **Units don't combine.** The equation mixes [units](/concepts/units-and-precision) that are not dimensionally compatible — adding meters to seconds, or comparing a length to an area. Either correct the units on the variables involved, or convert explicitly within the equation. A unit you typed may also be unrecognized. See [Combining types](/authoring/writing-equations#combining-types). Over the API: `input.incompatible_units`, `input.invalid_unit`, and `input.incompatible_type`. **A name or argument is invalid.** A [variable](/concepts/variables) name is empty or uses illegal characters — names must start with a letter and contain only letters, numbers, and underscores — or a function was called with the wrong number of arguments, an argument of the wrong type, or an argument whose value is outside the limits for that value, or a [table](/authoring/tables) is malformed (ragged rows, or a table nested in a table). Correct the name, the call, or the table shape. Over the API: `input.invalid_variable_name`, `input.invalid_argument`, `input.incompatible_type`, `input.out_of_range`, `input.invalid_dimensions`. **A variable can't be reached.** A test references a variable that no equation defines, or an equation depends on itself through a cycle. Define the missing variable, or break the dependency loop. Over the API: `calculation.undefined_variable` and `calculation.circular_reference`. **The target can't be solved, or solves to more than one value.** The inputs you supplied are not enough to reach the variable you asked for, or they over-determine it — the same target resolves to conflicting values. For an unsolvable target, supply more inputs or mark the variables you expect to solve for as [calculable](/authoring/writing-activities#calculable-variables); the error lists which inputs would unblock it. For a conflict, this usually signals **drift** — re-derive the value the way the Tip above describes and reconcile the activities that disagree. Over the API: `calculation.unable_to_resolve_target` and `calculation.conflicting_results`. **A numeric result is undefined.** Evaluation produced a divide-by-zero, an infinity, a NaN, or a value too large to represent. Guard the equation against the offending input. Over the API: the `math.*` codes. ## Publishing Publishing promotes the draft to the published version. From that point, the [API](/api/domains) and [Playground](/playground/tour) calculate against it. A domain can have both a published version and a newer draft at once — the API reports this with `has_published` and `has_draft` (see [Domains](/api/domains)) — and the published version keeps serving until you publish again. Republishing after edits is a deliberate step: calculation logic never changes silently underneath the callers using a domain. When business logic changes, you edit the draft, test it, and publish a new version on purpose. Combined with [provenance](/concepts/provenance), this is what lets a result be tied to the exact logic that produced it. You publish and revert from the same **Publish view**. Reverting a draft discards its unpublished changes and returns the draft to the published version — useful when you want to abandon in-progress edits rather than promote them. ## Before you publish A quick checklist: * **Descriptions are legible** — the domain and its variables have descriptions written in the language users use, since clear descriptions improve natural-language and agent results; activities have clear titles and descriptions for the people reading the domain. * **Calculable coverage** — the variables you expect to solve for are marked [calculable](/authoring/writing-activities#calculable-variables) on the activities that produce them. * **Defaults are set** where a variable should have a fallback value. * **Representative scenarios pass** in the Test view, including the what-if directions you expect callers to use. ## After publishing Editing a published domain starts a new draft; the published version continues to serve calculations until you publish the draft. To retire a domain entirely, archive it — see [Archiving](/authoring/creating-a-domain#archiving) and [Versioning](/concepts/versioning). # A worked domain: contractor estimating Source: https://docs.truemath.ai/authoring/worked-example-estimating Build a small contractor-estimating domain end to end — loaded labor rates, markup versus margin, a bid price that solves in several directions, and a cash-flow chart. This page builds one small [domain](/concepts/domains) from start to finish, so you can see how the parts covered elsewhere in this section fit together. The subject is a general contractor pricing a job: a handful of [activities](/concepts/activities) connected only by the [variables](/concepts/variables) they share, answering the question every estimator starts from — *what should I bid?* It is a focused starting point, not a full job-costing system. By the end it prices a job and charts the cash flow of the work; change orders, retainage, and progress billing are left as natural extensions. That is the point of a [library domain](/domain-library/overview): cover one workflow well and leave room to grow. Throughout, one scenario keeps the numbers concrete: a worker at **$35 per hour**, **40 hours** of labor, **$8,000** of materials, a **\$32,000** subcontractor, **12%** overhead, and a target **20% margin**. ## From a wage to a loaded rate The first [activity](/authoring/writing-activities) turns a base wage into a fully burdened rate: ``` loaded_labor_rate = base_wage * (1 + labor_burden_rate) ``` Writing this equation brings three variables into being. You then [configure each one](/authoring/defining-variables): `base_wage` and `loaded_labor_rate` are money — a `number` whose value carries a currency [unit](/math/units), so `35` is `35 USD` — and `labor_burden_rate` is a `percent`, entered as a decimal (`0.30` for 30%). At a $35 wage and 30% burden, the loaded rate is **$45.50\*\*. The same equation answers more than one question. Mark all three variables [calculable](/authoring/writing-activities#calculable-variables) and the activity solves in whichever direction the inputs allow: * *"What's my loaded rate at a \$35 wage and 30% burden?"* → solves `loaded_labor_rate`. * *"What burden rate gives me a $52 loaded rate on a $40 wage?"* → solves `labor_burden_rate`. You did not write a second equation for the reverse question. One relationship, marked calculable, covers both. ## Costing the work A task's labor cost is the loaded rate times the hours: ``` labor_cost = loaded_labor_rate * labor_hours ``` `loaded_labor_rate` is the same variable the previous activity produces — naming it here is the whole of the wiring. That shared name is how the two activities connect, and why consistent keys matter. At $45.50 for 40 hours, labor is **$1,820\*\*. For a crew, the natural input is a list of workers rather than a single rate. A [table](/authoring/tables) variable holds it — one row per worker, a loaded-rate column and an hours column — and one function totals it: ``` crew_cost = sumofproducts(crew_cost_table) ``` [`sumofproducts`](/math/functions/statistics#sum-of-products-sumofproducts) multiplies each row's two cells and sums the products: it is the right tool for any weighted sum. (Reaching for `sum` here would total only the first column — the rates — and ignore the hours.) Materials and subcontractors follow the same markup shape: ``` material_sell_price = material_cost * (1 + material_markup_rate) sub_sell_price = sub_cost * (1 + sub_markup_rate) ``` At $8,000 of materials marked up 15% and a $32,000 subcontractor marked up 10%, those are **$9,200** and **$35,200**. ## Rolling up to a bid The costed pieces sum into a direct cost, overhead is allocated on top, and the two give the job's total cost: ``` total_direct_cost = labor_cost + material_sell_price + sub_sell_price overhead_amount = total_direct_cost * overhead_rate total_job_cost = total_direct_cost + overhead_amount ``` For the scenario: direct cost **$46,220**, overhead at 12% **$5,546.40**, total job cost **\$51,766.40**. None of these activities needed explicit wiring — each names variables the earlier activities produce, and the domain composes into a single solvable chain. See [how activities compose](/authoring/writing-activities#how-activities-compose). A contractor states the markup two ways, so there are two routes to the bid: ``` bid_price = total_job_cost / (1 - margin_pct) # from a target margin bid_price = total_job_cost * (1 + markup_pct) # from a markup on cost ``` These are two activities that produce the **same** variable from different inputs. Supply a margin and the first resolves; supply a markup and the second does. They are complementary pathways, not duplicates — the domain answers the question whichever way the estimator happens to phrase it. A 20% margin gives a bid of **\$64,708**. ## Margin is not markup `margin_pct` and `markup_pct` are the kind of pair that quietly breaks a domain if you let them blur together. Margin is a percentage of the **sell price**; markup is a percentage added to the **cost**. A 20% margin is a 25% markup on the same job — the bid is identical, the number is not. Two small activities convert between them: ``` markup_pct = margin_pct / (1 - margin_pct) margin_pct = markup_pct / (1 + markup_pct) ``` The real safeguard is in the [descriptions](/authoring/defining-variables). Because a contractor might say "twenty percent" for either, each variable's description claims its own phrasing and names the other to rule it out — *margin is a percentage of the bid price, distinct from markup, which is a percentage added to cost.* That mutual disambiguation is how you keep results landing on the variable you mean. When two variables compete for the same words, resolve it in the descriptions, not the equations — and make the distinction point both ways, so each names the other. A pair separated in only one description still collides from the other side. ## Seeing the result Pricing the job is one workflow; tracking its cash is the next, and it shows how a domain turns a series of values into a picture. Say the work bills and pays out over several months. Two [table](/authoring/tables) inputs hold the draws and the costs, one row per period, and a subtraction gives the net per period: ``` net_cash_per_period = draw_schedule - cost_schedule ``` Two tables of the same shape subtract element-wise, so this is a one-row-per-period table of net cash — positive in months you collect more than you spend, negative when you spend ahead of billing. Give that variable the `bar_chart` [kind](/authoring/charts) and it reads as bars above and below zero; the chart is a presentation of the table, not a separate value. The running total — *how far underwater is the job at its worst?* — is a [loop](/math/functions/conditionals-and-logic#building-a-series) that carries a cumulative sum: ``` cumulative_cash_position = loop(1; length(net_cash_per_period)) + (last + item(net_cash_per_period; index)) ``` Each row adds the period's net cash to the running total in `last`, so the table climbs and dips with the project and its final row is the job's net cash position. The functions here — [`loop`](/math/functions/conditionals-and-logic#loop-loop), [`length`](/math/functions/tables#length-length), [`item`](/math/functions/tables#item-item) — are covered in the function library. ## Where it could go next The domain prices a job and shows its cash flow — a real tool an estimator would use, and an obvious invitation to extend: * *"Add a change order at a price and roll it into the contract value."* * *"Withhold retainage from each draw and track the balance."* * *"Track cost-to-date against the budget and project the cost at completion."* Each is a few more activities over the variables already here. Before publishing any of it, round-trip the calculable variables in the Test view to catch [drift](/authoring/testing-and-publishing#testing-a-draft): solve `bid_price` from a margin, then recover that margin from the bid, and confirm the value holds. Because the activities are reversible, it should. # Writing activities Source: https://docs.truemath.ai/authoring/writing-activities Define an activity — its equation, the variables it relates, and which of those variables it is allowed to solve for. An [activity](/concepts/activities) is the equation unit of a [domain](/concepts/domains). Writing an activity means three things: giving it a title and description, writing its equation, and marking which of the variables it touches it is allowed to solve for. This page covers the activity as a unit; for the expression syntax itself — literals, operators, functions, units — see [Writing equations](/authoring/writing-equations). ## What an activity has * **A title and description** — human-readable labels that state what the activity computes. * **An equation** — the mathematical relationship among its variables, written in TrueMath's [expression syntax](/authoring/writing-equations). * **Calculable variables** — for each variable the equation references, whether this activity may solve for it (see [below](#calculable-variables)). * **A state** — active, disabled, or deleted (see [Activity state](#activity-state)). An activity's title and description document what the activity computes, for the people reading a domain. ## Variables come from the equation You do not create variables separately and then wire them into an activity. A [variable](/concepts/variables) first comes into being when you reference it by name in an equation. Writing ``` payment = -pmt(loan_amount; 0; rate; term) ``` introduces `payment`, `loan_amount`, `rate`, and `term` into the domain. Once a variable exists, you configure its display, default, and unit on the [Defining variables](/authoring/defining-variables). Reusing the same name in another activity refers to the same variable — this is how activities connect. ## Calculable variables An activity is not locked to a single input-output direction. The same equation that solves for a monthly payment from a loan amount, rate, and term can, given a target payment, solve for the loan amount instead. You control this by marking which of an activity's variables are **calculable** — the ones it is allowed to solve for. Within that set, which variable an activity actually computes on a given run depends on the values provided and the variable asked for. Marking more variables calculable lets a domain answer more questions from the same activity; marking a variable as not calculable means the activity will only ever read it as an input, never produce it. Mark a variable calculable when it is meaningful to solve for it from the others. In a payment relationship, payment, loan amount, and term are all reasonable to solve for; a variable that should always be supplied — never derived — by this equation is left non-calculable. **A test for what to mark calculable.** Go variable by variable and ask: *if I knew all the others, would a user naturally want to solve for this one?* If yes, mark it calculable. If the math allows it but no one would ask it that way, leave it non-calculable and give that variable its own activity. For an equation relating payment, loan amount, rate, and term, the answer is yes for all four — each is a question a user might bring. But you would not back-solve Private Mortgage Insurance (PMI) from an escrow payment: PMI is naturally derived from loan amount and home price, so it belongs in its own activity rather than marked calculable on the escrow equation — even though the algebra would permit it. ## How activities compose A domain is many activities connected through the variables they share. An activity that produces `loan_amount` and another that consumes `loan_amount` are linked by that shared variable, without any explicit wiring beyond the common name. Because of this, consistent variable naming is what holds a domain together — reuse the exact key across every activity that refers to the same quantity. ``` loan_amount = home_price - down_payment payment = -pmt(loan_amount; 0; rate; term) total_paid = payment * term * 12 ``` These three activities form a small mortgage domain: `loan_amount` flows from the first into the second, and `payment` from the second into the third. Depending on what you supply and ask for, and depending on which variables in which activities are marked calculable, the domain can solve forward to a payment or backward to an affordable home price. **Break a problem into several small activities rather than one large equation.** Model each real-world relationship as its own activity and let them connect through shared variables. More focused activities give a domain more ways to reach an answer — it can solve from whatever combination of values a caller happens to have — where one fused equation locks you into fewer directions. Smaller activities are also easier to reason about and to reverse. A useful signal for where to split: nested parentheses in an equation usually mark a seam — if the inner result has its own meaning, something a caller might ask for directly or another activity might consume, give it its own activity. Let TrueMath do the work. ## Activity state An activity is in one of three states: * **Active** — part of the domain and used when running calculations. * **Disabled** — kept in the domain definition but not used in calculations. Use it to set an activity aside without losing it, or to stage one that is not ready. * **Deleted** — removed from the domain. Disabling is reversible and non-destructive; deleting removes the activity upon publishing the domain. `GET /v1/domains/:id/activities` reports the state of a live activity as `active` or `disabled`; deleted activities are not returned. See [Domains](/api/domains). Equations are validated as part of authoring, and incompatible operations — combining values whose dimensions do not match, for example — are rejected rather than silently coerced. See [Combining types](/authoring/writing-equations#combining-types). # Writing equations Source: https://docs.truemath.ai/authoring/writing-equations Write TrueMath activity equations — literals, variables, operators, functions, units in backticks, and how scalar, unit, and table values combine. An [activity](/concepts/activities) equation is written in TrueMath's expression syntax. This page covers the building blocks and how value types combine when an equation runs. For the full operator list and ordering, see [Operators and precedence](/math/functions/operators-and-precedence); for the built-in functions, see the [Function library](/math/functions/index). ## Literals * **Numbers** — `42`, `3.14`, `-7.2`. Scientific notation uses `e`: `1.5e-3` is `0.0015`. * **Numbers with units** — a unit literal wrapped in backticks (see [Units in equations](#units-in-equations)). * **Tables** — bracketed lists: `[1; 2; 3]`. See [Tables](/math/types/tables). ### Units in equations A unit used as a literal must be wrapped in **backticks**, so it reads as a unit and not a variable name. Both simple and compound units work: ``` 5`m` 30`yr` 400000`USD` 12`m/s` ``` The backticks are required: without them, `m`, `yr`, or `USD` is parsed as a variable name. See [Unit numbers](/math/types/unit-numbers). This applies only inside equations. In [structured-text input](/concepts/input-formats#values) a value is not an expression, so units are written plainly — `30 yr`, no backticks. ## Variables A variable name starts with a letter and contains letters, digits, and underscores: `rate`, `home_price`, `term`. Variable names are case insensitive and will be lowercased automatically. In an equation, a variable refers to one of the [domain's variables](/concepts/variables). ``` payment = -pmt(loan_amount; 0; rate; term) ``` Changing a name here does not rename a variable — a name that doesn't match an existing one creates a *new* variable. To rename, edit the key in the variable's details. See [Key and labels](/authoring/defining-variables#key-and-labels). ## Constants | Constant | Value | | -------- | ------------ | | `pi` | π (3.14159…) | | `true` | 1 | | `false` | 0 | ## Operators Arithmetic `+ - * / ^`, comparison `== != < > <= >=`, and logical `&& || !`. Group with parentheses to control evaluation order: ``` (principal + interest) / periods ``` See [Operators and precedence](/math/functions/operators-and-precedence) for the full list and ordering. ## Functions Call a function by name with arguments separated by **semicolons**: ``` root(volume; 3) pmt(loan_amount; 0; rate; term) ``` Function names are case-insensitive (`sqrt`, `SQRT`, and `Sqrt` are equivalent). See the [Function library](/math/functions/index). ## Assignment `=` binds the result of an expression to a variable: ``` loan_amount = home_price - down_payment ``` Do not confuse `=` with `==`: `=` assigns a value to a variable, while `==` tests whether two values are equal inside a [condition](/math/functions/conditionals-and-logic). They are not interchangeable. See [Operators and precedence](/math/functions/operators-and-precedence). The argument separator and the table/row separator are both the semicolon (`;`). Commas are not used in equations. ## Combining types Equations operate on three value types: [scalar numbers](/math/types/scalar-numbers), [unit numbers](/math/types/unit-numbers), and [tables](/math/types/tables). When an operation combines values, TrueMath applies consistent rules — and rejects combinations that are not meaningful. ### Numbers * **Scalar with scalar** → scalar. * **Compatible units** → converted to a common unit and combined. * **Incompatible units** → rejected, because the dimensions differ. * **Multiplication and division** combine dimensions — a length multiplied by a length is an area; a length divided by a time yields a velocity; a currency divided by an volume yields a cost-per-volume. See [Units](/math/units). ``` 2 + 3 # 5 1`m` + 10`cm` # a length 5`m` + 3`kg` # error: length and mass are different dimensions ``` ### Tables * **Table with scalar** → the scalar is applied to every cell. `[1; 2; 3] * 2` is `[2; 4; 6]`. * **Table with table** → element-wise, and the tables must have the same shape. `[1; 2] + [3; 4]` is `[4; 6]`. * **Mismatched shapes** → rejected. Empty cells in a table are treated as having no value and are skipped by aggregating [functions](/math/functions/statistics). ### What is not coerced TrueMath does not silently coerce across incompatible dimensions or mismatched table shapes. These produce an error rather than a wrong answer — keeping with the principle that "close" is not acceptable. See [Guarantees](/introduction/guarantees). # Activities Source: https://docs.truemath.ai/concepts/activities Activities are the equation units of a domain. Each activity relates a set of variables and can solve for different variables depending on which values are known. An **activity** is an equation unit within a [domain](/concepts/domains). It defines a relationship among [variables](/concepts/variables) — for example, the relationship between a loan amount, an interest rate, a term, and a monthly payment. Activities are the building blocks of a domain. A domain is typically made up of many activities, connected through the variables they share. ## What an activity defines * **An equation** — the mathematical relationship, written in TrueMath's [writing equations](/authoring/writing-equations). * **The variables it relates** — and, for each, whether it is **calculable** by this activity (that is, whether the activity can solve for it). * **A title and description** — so the activity's purpose is clear to the people reading the domain. ## Solving in more than one direction An activity is not locked to a single input-output direction. Given a payment, a rate, and a term, an activity can solve for the loan amount; given a target payment, it can solve for the affordable purchase price. The author defines which of an activity's variables are **calculable** — the ones it may solve for; within that set, which variable is actually computed depends on the values you provide and what you ask to solve for. This is what lets a domain answer what-if questions naturally rather than only running a fixed calculation. ## Activity state An activity can be **active** or **disabled**. Disabled activities remain part of the domain definition but are not used when running calculations. You define activities and their equations when [authoring a domain](/authoring/writing-activities). To see the activities in a published domain, use [`GET /v1/domains/:id/activities`](/api/domains). # Domains Source: https://docs.truemath.ai/concepts/domains A domain is a collection of variables and activities that models a specific area of calculation, such as a mortgage or a construction estimate. A **domain** is a self-contained model of a calculation area. It bundles the [variables](/concepts/variables) you calculate with and the [activities](/concepts/activities) — the equations — that relate them. A domain is the unit you target when you run a calculation. A mortgage domain, for example, brings together variables like home price, down payment, interest rate, term, and monthly payment, along with the activities that connect them. A construction domain brings together material quantities, unit conversions, and cost projections. The domain changes from one area to the next; the way you work with it does not. ## What a domain contains * **Variables** — the named values in the domain, each with a type, an optional default, and units. See [Variables](/concepts/variables). * **Activities** — the equations that define how variables relate. Depending on which values are known, an activity can solve for different variables. See [Activities](/concepts/activities). ## Identity and versions A domain is identified by a stable **UUID** — the `domain_id` you pass to the [API](/api/calculate) — and carries a title and description. The description is written for both people and machines; clear, precise descriptions improve natural-language and agent results. The UUID is what you use to reference a domain. A domain also carries a stable **key** that TrueMath assigns when the domain is created. Where the UUID identifies a domain within the account it belongs to, the key is how TrueMath recognizes the *same* domain across accounts: when you [copy a domain to another account](/authoring/creating-a-domain#copy-to-another-account), the copy shares the source's key but takes its own UUID, so copying it again later updates that copy in place rather than creating a duplicate. The key is assigned, not chosen, and it does *not* appear in a domain's exported definition — an [import](/authoring/creating-a-domain#export-and-import-json) overrides the domain you import it into, which you select; it is not matched by a key in the JSON. See [Creating a domain](/authoring/creating-a-domain#managing-a-domain). A domain moves through a [version](/concepts/versioning) lifecycle: you edit a draft, then publish it. Calculations always run against the published version, and only published domains are available through the [API](/api/domains) and the [Playground](/playground/tour). ## Where domains come from * **The domain library** — TrueMath ships pre-built, role-based domains across several professional verticals. See the [Domain library](/domain-library/overview). * **Your own domains** — author a domain from scratch, or customize a library domain to match your conventions and business logic. See [Authoring overview](/authoring/overview). To list the published domains available to your account, use [`GET /v1/domains`](/api/domains). See what's available out of the box. # Input formats Source: https://docs.truemath.ai/concepts/input-formats How calculation inputs are expressed — natural language prose, structured text key/value lines, or a JSON object. Natural language and structured text work in the Playground and over the API; JSON is API-only. Every calculation is submitted as a **prompt** in one **input format**, chosen per request. TrueMath accepts three input formats: * **Natural language** — prose describing what you want. * **Structured text** — explicit `key: value` lines. * **JSON** — a structured object carrying the same values as structured text. Available over the [API](/api/calculate) only. Natural language and structured text apply the same way in the [Playground](/playground/tour) and over the [API](/api/calculate) — only the surface differs. JSON is accepted over the API only. For how natural language fits into TrueMath's use of language models, see [How TrueMath fits with LLMs](/introduction/how-it-fits-with-llms). A completed calculation returns its full result, and a structured-text request also gets that result serialized back as structured text, so it round-trips: capture a result, change a value, and resubmit it. See [Round-trip](#round-trip). ## Natural language Natural language is free-form prose. TrueMath uses a language model to parse your intent into the [variables](/concepts/variables) to set and the variable to solve for, then executes the calculation deterministically. Use it for exploration, or when you do not know a domain's exact keys. ```nlp theme={null} What is the monthly payment on a $400,000 loan at 6% over 30 years? ``` Because natural language must be interpreted, a request may be processed asynchronously — see [In progress](/api/calculate#in-progress). ## Structured text Structured text is one instruction per line, in `key: value` form. It is precise and fast: there is no language to interpret, so the same input always produces the same request. Use it when you know the [variables](/concepts/variables) you want to set and the target you want to solve for. ```yaml theme={null} # A mortgage payment scenario: new home_price: $500,000 down_payment: $100,000 interest_rate: 6.5% loan_term: 30 yr calculate: monthly_payment ``` ### Lines Each line is one instruction: * **`: `** — set a variable. `` is the variable's [key](/concepts/variables). * **`calculate: []`** — name the variable to solve for, optionally with the unit you want the result in (`calculate: monthly_payment USD`). * **`scenario: `** — choose the [scenario](/concepts/scenarios) to calculate against (see [Scenarios](#scenarios)). * **`# ...`** — a comment; the whole line is ignored. `calculate:` and `scenario:` are optional. When omitted, they are inferred from the prior request in the conversation. ### Values Write a value the way you would by hand; the unit is part of the value (see [Units and precision](/concepts/units-and-precision)). * **Numbers** — `3000.54` or `3,000.54`; thousands separators are optional. * **Units inline** — `30 yr`, `5.2 m`, `1500 USD`; abbreviations or full names both work (`yr` or `years`, `m` or `meters`). See [Units](/math/units). * **Currency** — a leading `$` is shorthand for USD: `$1,000.00` is the same as `1000.00 USD`. * **Percent** — a trailing `%` is converted to a rate: `6.5%` becomes `0.065`. * **Dates, times, and durations** — `7/21/2026`, `2:30 PM`, `90 min`, `today`. See [Dates, times, and durations](#dates-times-and-durations). Units are written plainly here — no backticks. Backticks are only needed when a unit appears in an [activity equation](/authoring/writing-equations#units-in-equations), where it could otherwise be read as a variable name. ### Dates, times, and durations A variable typed as a `duration`, `date`, `datetime`, or `time` takes its value the way people write one, and TrueMath converts it to the count of seconds the variable stores. You never type seconds yourself. See [Authoring dates and durations](/authoring/dates-and-durations) for what each kind holds. | The variable is | You can write | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `date` | `7/21/2026` · `07/21/2026` · `7-21-2026` · `2026-07-21` · `2026/07/21` · `7/21/26`
`Jul 21, 2026` · `July 21st, 2026` · `21 July 2026` · `Tuesday, July 21, 2026`
`Oct 5` · `October 5th` · `5 Oct` · `10/5` (the current year)
`Jul 2026` · `July 2026` · `7/2026` (the 1st) · `today` | | `datetime` | `2026-07-21T14:30:05` · `2026-07-21 14:30` · `7/21/2026 2:30 PM` · `Jul 21, 2026 2:30 PM`
`today` · `now` | | `time` | `14:30` · `2:30 PM` · `2:30:05 PM` · `9pm` · `12 AM` · `now` | | `duration` | `22:47` · `1:05:30` · `-1:30` · `22:47.5`
`90 min` · `1.5 hr` · `1 hr 30 min` | Month and weekday names are English. A named weekday is checked against the date rather than taken on trust, so `Monday, July 21, 2026` is an error: that day is a Tuesday. **Numeric dates are read month-first**, matching how they render: `10/5/2026` is October 5th and `5/10/2026` is May 10th. A two-digit year reads `00`–`49` as this century and `50`–`99` as the last, so `1/1/49` is 2049 and `1/1/50` is 1950. Write the year in full for a value near that boundary — a birth date especially. **A date with the year left out takes the current year**, read in the same time zone as `today` (see [Time zones](/api/calculate#time-zones)). `Feb 29` is a date in 2028 and an error in 2026. **A value that names its own time unit is read as written.** `90 min`, `1.5 hr`, and the compound `1 hr 30 min` are quantities of time already, and are taken as they stand. These spellings work in [natural language](#natural-language) too. A date written the way you would say it — *"what is the payoff if I close on July 21st?"* — is read the same way as one typed into a field. #### `today` and `now` `today` and `now` name a moment rather than describing one. Either can be given as a value, and the moment used is the one the calculation runs at, in the caller's own time zone: ```yaml theme={null} start_date: today calculate: closing_date ``` Both are accepted wherever the variable can hold one: a `date` or `datetime` takes either, a `time` takes `now`, and a `duration` takes neither. For what each kind keeps, see [`today` and `now` as defaults](/authoring/dates-and-durations#today-and-now-as-defaults) — the same rules apply to a value you supply and to a default an author sets. Nothing else is read this way. `tomorrow` and `next Friday` are not keywords. #### One genuine ambiguity: the colon `22:47` is either 22 minutes 47 seconds or 22 hours 47 minutes, and the text alone does not distinguish them. The variable's kind and format are what separate the two readings, so the same string lands on a different number: ```text theme={null} 22:47 on a duration formatted duration_ms → 1367 s (22 min 47 sec) 22:47 on a duration formatted duration_hm → 82020 s (22 hr 47 min) 22:47 on a time of day → 82020 s (22:47 on the clock) ``` A clock always fills hours-first, so `9:59` on a `time` or `datetime` is 9:59 AM whatever its format. For a duration, a third group states the reading outright — `0:22:47` is 22 minutes 47 seconds under any format — and a meridiem removes the ambiguity entirely. Because only the variable separates the two readings, a colon value given to an ordinary `number` variable is an error rather than a guess. #### A value written as a date is read as one or refused A value *written* as a date — three numeric groups on one separator, or a month name — is either read as a date or returned as [`input.invalid_prompt`](/api/errors#calculation-error-codes). This applies on any variable, not only one typed as a date or a duration, so a domain that predates those kinds and holds its dates as plain numbers is covered too. | Refused | Why | | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | | `21/07/2026` | numeric dates are month-first, and there is no month 21 | | `7.21.2026` | dot separators collide with decimal separators | | `26-07-21` · `7/21/6` | only a *trailing* two-digit year is read; which end carries the year is the only thing separating one order from the other | | `7-2026` · `2026-07` | a month and year is read with a slash — `7/2026` | | `2026-07-21T14:30:00Z` · `2026-07-21T14:30:00-07:00` | a stored date carries no time zone, and accepting one would look as though it had been applied | | `2026-02-29` · `2026-13-01` · `2026-04-31` | not real dates — 2026 is not a leap year, there is no thirteenth month, April has 30 days | | `1899-12-31` · `2201-01-01` | outside the [supported range](/math/types/dates#supported-range-and-calendar-rules) | | `24:00` · `14:60` · `13:00 PM` | a clock time is range-checked, not rolled over | | `9` on a `time` variable | a bare hour is a time only when a meridiem says so, as in `9pm` | #### A table cell follows its column Every rule above applies one level down inside a [table](#tables). A table variable's own kind is always `table`, so the kind and the format come from the cell's **column**: ```yaml theme={null} schedule: [2/1/26; 1000][3/1/26; 500] ``` Two consequences are worth knowing: * **A refused cell is named by its column** — `'Date' cell '2/45/26' is not a valid date` — because there is otherwise no way to identify which of forty values is wrong. * **A flat table is one column of many rows**, so every cell in it is read against the first column. Rows need their own brackets. ### Tables A [table](/math/types/tables) value uses **semicolons** to separate cells — never commas, which collide with decimal and thousands separators. A one-dimensional table is a list on a single line; brackets are optional: ```yaml theme={null} costs: 1500; 1600; 1700 costs: [1500; 1600; 1700] ``` Each cell may carry its own unit; an outer unit after the table fills only the cells that lack one — a cell's own unit always wins: ```yaml theme={null} lengths: 3 in; 4 ft; 5 m # each cell carries its own unit lengths: [3; 4; 5] ft # ft fills every cell lengths: [3; 4 in; 5] ft # ft fills cells 1 and 3; 4 in keeps its own ``` Cells follow the same value conventions as scalars — `[$3; $4]` is two currency amounts, and `[5%; 10%]` becomes `[0.05; 0.1]`. A two-dimensional table is written as bracketed rows. Put the rows on one line, or one row per line: ```yaml theme={null} schedule: [1500; 12][1600; 12][1700; 12] schedule: [1500; 12] [1600; 12] [1700; 12] ``` See [Tables](/math/types/tables) for the underlying model and how tables are returned. ### Scenarios `scenario:` selects which [scenario](/concepts/scenarios) the calculation runs against: * **`scenario: new`** — start a new scenario. * **`scenario: last`** — the most recent scenario. * **`scenario: first`** — the first scenario in the conversation. * **`scenario: `** — a specific scenario by its index. When `scenario:` is omitted, the calculation continues the conversation's current scenario. ### Persistence Values persist within a [conversation](/concepts/scenarios). A variable you set but do not use in the current calculation is retained and stays available until a later calculation needs it. Likewise, when you omit `calculate:`, the prior target is retained. You can build up inputs across several requests and solve for different targets without restating values. ## JSON JSON expresses the same request as structured text, but as a structured object rather than `key: value` lines. It is available over the [API](/api/calculate) only — set `input_format` to `json` and pass the object as a JSON string in `prompt`. Like structured text, it is precise and deterministic: there is no language to interpret, so the same input always produces the same request, and the calculation runs synchronously. The object has three fields: ```json theme={null} { "inputs": [ { "key": "home_price", "value": "500000 USD" }, { "key": "down_payment", "value": "100000 USD" }, { "key": "interest_rate", "value": "6.5%" }, { "key": "loan_term", "value": "30 yr" } ], "calculate": { "key": "monthly_payment", "unit": null }, "scenario": "new" } ``` * **`inputs`** — an array of `{ "key", "value" }` objects, one per [variable](/concepts/variables) to set. `key` is the variable's [key](/concepts/variables). * **`calculate`** — `{ "key", "unit" }` naming the variable to solve for, with the optional [unit](/math/units) you want the result in (`null` for the variable's own unit). Set `calculate` to `null` to infer the target from the prior request in the conversation. * **`scenario`** — the [scenario](/concepts/scenarios) selector as a string: `"new"`, `"first"`, `"last"`, or an index such as `"2"`. Set it to `null` to continue the conversation's current scenario. ### Values in JSON Each `value` is a string, following the same conventions as a structured-text [value](#values) — the unit is included in the string: * **Scalars and units** — `"3000.54"`, `"30 yr"`, `"1500 USD"`. A leading `$` is shorthand for USD (`"$1,000.00"`), and a trailing `%` becomes a rate (`"6.5%"` → `0.065`). * **A 1-D [table](/math/types/tables)** — an array of cell strings: `["1500 USD", "1600 USD", "1700 USD"]`. * **A 2-D table** — an array of row arrays: `[["1500", "12"], ["1600", "12"]]`. Table cells are separate array elements, so JSON uses no semicolons or brackets within a value. Each cell carries its own unit; there is no outer unit that fills cells lacking one. ### Nothing recognized A request that resolves to no inputs and no calculation target — `{ "inputs": [], "calculate": null, "scenario": null }` — is treated as "nothing recognized" and returns the [`input.llm_no_parsed_data`](/api/errors#calculation-error-codes) error. This is the same signal used across every input format. ## Round-trip A completed calculation always returns its full result in the `results` object. A structured-text request additionally gets that result serialized back as a `structured_text` string, so you can capture it, change a value, and resubmit. See [Calculate](/api/calculate#completed) for the response fields each input format returns. # Provenance Source: https://docs.truemath.ai/concepts/provenance Every variable value records where it came from — user-stated, calculated, carried forward, or default. Provenance is what makes a result accountable. **Provenance** is the record of where a [variable's](/concepts/variables) value came from. Every value TrueMath works with carries this metadata, so a result is not just a number — it is a number you can trace back to its origin. ## Kinds of provenance A value's origin is one of: * **User-stated** — provided directly as an input. * **Calculated** — computed by an [activity](/concepts/activities) from other values. * **Carried forward** — brought forward from an earlier point in the same [conversation](/concepts/scenarios). * **Default** — supplied from the variable's default value. In API results, each variable reports a `source` of `user_input`, `calculated`, or `default_value`, and a `historical` flag indicating whether the value was carried forward from a prior [scenario](/concepts/scenarios). ## Resolution When more than one value could apply to a variable, TrueMath resolves it deterministically — the same inputs always produce the same result — and every resolved value keeps the provenance record described above, so you can always see which value was used and where it came from. A **default is a fallback**: it applies only when no other value has been provided or calculated for that variable. ## Why it matters Because every value's origin is tracked through each step, a calculation can be explained as a sequence of documented inputs and results rather than presented as an unaccountable answer. Provenance is the foundation of that explainability — and a core part of what makes TrueMath an execution layer you can trust. See [Guarantees](/introduction/guarantees). # Scenarios Source: https://docs.truemath.ai/concepts/scenarios A scenario is a distinct calculation state within a conversation. Scenarios let you explore what-if outcomes without losing prior work. Calculations in TrueMath happen inside a **conversation** — an ongoing session that holds one or more **scenarios**. A scenario is a distinct calculation state: a set of [variable](/concepts/variables) values and the results derived from them. This mirrors how professionals actually work — iteratively, exploring alternatives, building toward a decision — rather than running isolated one-off calculations. ## Conversations and scenarios A conversation begins with a first calculation, which establishes the first scenario. As you continue, each request either refines the current scenario or branches into a new one. The conversation tracks a **current scenario** and the **most recent scenario** created, so you always know which state you are looking at. ## What-if exploration When you change an input to explore an alternative — "what if the down payment were 15% instead of 20%" — TrueMath creates a **new scenario**. The original is preserved, so you can compare outcomes rather than overwrite them. Each calculation reports the action it took: * **`new`** — a new scenario was created. Returned when you start a new conversation, explicitly ask for a new scenario, or change an input you already provided to explore an alternative — the original scenario is preserved so you can compare. Converting a value to a different unit does *not* create a new scenario; it changes what is shown, not the underlying calculation. * **`extend`** — the current scenario was extended in place: additional values were supplied without changing any it already had. Converting a known value in the scenario to a different unit also returns `extend`. * **`discard`** — the requested calculation exactly matches one already in the scenario. Nothing was modified. * **`fetch`** — an existing scenario was retrieved. A `fetch` does *not* return the scenario itself — read it with [`GET /v1/conversations/:id/context/:scenario_id`](/api/scenario-context). ## Working with scenarios * In the Playground, follow-up questions and what-if changes create and move between scenarios for you. See [What-if scenarios](/playground/what-if-scenarios). * Over the API, you can retrieve the resolved state of every scenario in a conversation with [`GET /v1/conversations/:id/context`](/api/scenario-context), or a single scenario by its index with [`GET /v1/conversations/:id/context/:scenario_id`](/api/scenario-context). # Units and precision Source: https://docs.truemath.ai/concepts/units-and-precision TrueMath carries units with values and converts compatible units automatically, and it maintains full precision across every step of a calculation. Two properties of TrueMath's number handling matter across every calculation: values carry their **units**, and they are kept at **full precision** from input to output. ## Values carry their units A value is not just a number — it is a number with a unit. A home price is `375000 USD`; a term is `30 yr`; a distance is `5.2 m`. Because the unit is part of the value, TrueMath can: * **Convert compatible units automatically.** Adding `1 m` and `10 cm` yields a length; you do not convert by hand. * **Reject incompatible combinations.** Adding a length to a mass is an error rather than a silently wrong number. TrueMath supports units across many dimensions — length, area, volume, mass, time, angle, information, and currency — as well as compound units such as velocity and cost-per-unit. For the full catalog and conversion behavior, see [Units](/math/units). ## Model quantities as unit-carrying values Units are a correctness layer, not decoration — the same role types play in a programming language. They are most valuable when you model a domain so that every dimensional quantity carries its unit, rather than reducing it to a bare number that *assumes* one. A term is `30 yr`, not a `30` that the rest of the domain has agreed to read as years. Carrying the unit is what lets TrueMath validate the value — a term supplied in inches is rejected, not silently used — and convert it without loss wherever it is needed. A bare number assumes its unit by convention, and that convention is invisible to both the engine and whoever supplies the value, so nothing catches a value entered in the wrong unit. The result is a confident, silently wrong answer of exactly the kind units exist to prevent. The guidance follows: keep values in their units through the calculation, and reduce one to a bare number only at the point where a scalar number is genuinely required, with a single explicit conversion. Stripping a unit reflexively throws away the safety the engine was carrying for you — see [When to strip a unit](/math/functions/units#when-to-strip-a-unit). ## Full precision, no intermediate rounding TrueMath carries values at full internal precision through every step of a calculation — roughly 15–16 significant digits — and that full-precision result is what every step computes with. An intermediate result is never rounded before the next step uses it; rounding is applied only when a value is displayed, according to the [variable's](/concepts/variables) format. This matters in multi-step work. When a rounded value becomes the input to the next equation, small approximations compound into a meaningfully wrong answer. By keeping every intermediate value exact, TrueMath eliminates that drift — the difference is invisible in a single calculation and decisive in a long one. Display formatting changes how a value is *shown*, not how it is *computed*. The stored value remains at full precision and is what subsequent steps use. Even a format set to show a value's natural precision displays at most 10 decimal places — the full-precision value behind it is unchanged. # Variables Source: https://docs.truemath.ai/concepts/variables Variables are the named values a domain calculates with. Each is defined by a key, kind, format, and optional default, and carries a value with its unit and provenance when a calculation runs. A **variable** is a named value within a [domain](/concepts/domains) — a home price, an interest rate, a monthly payment. Variables are what [activities](/concepts/activities) relate, and what you provide and receive when you run a calculation. ## What a variable has A variable is **defined** in a domain and takes on a **value** when a calculation runs. As defined, a variable has: * **A key** — an identifier, unique within the domain. You reference a variable by its key when you provide or solve for it (see [Input formats](/concepts/input-formats)), and the API returns it on each variable. * **A title and description** — human- and agent-readable labels. * **A kind and a format** — what the value is, and how it is shown (see below). * **A default value** — an optional value and unit used when none is provided. When a calculation runs, each variable also has: * **A value** — the number or table it currently holds, with its **unit** where applicable; the unit is part of the value (see [Units and precision](/concepts/units-and-precision)). * **Provenance** — where that value came from: user-stated, calculated, carried forward, or a default. See [Provenance](/concepts/provenance). ## Kinds and formats A variable holds one of TrueMath's value types — a [scalar number](/math/types/scalar-numbers), a [unit number](/math/types/unit-numbers), or a [table](/math/types/tables) — and its **kind** says what that value is: * **`number`** — a plain number, shown with thousands separators. A monetary amount is a number whose unit is a currency (for example `USD`). * **`percent`** — a rate shown as a percentage. * **`no_separator`** — a number shown without thousands separators (for example, a year). * **`duration`, `date`, `datetime`, `time`** — a span of time, a calendar day, a day with a clock time, or a time of day. Each is an ordinary number counting seconds. See [Authoring dates and durations](/authoring/dates-and-durations). * **`table`** — a collection of values with named rows and columns. * **`bar_chart` / `pie_chart`** — a table presented as a chart. The variable's **format** says how that value is shown, from the vocabulary its kind takes: a number of decimal places for a `number`, `percent`, or `no_separator`, and a [date or duration format token](/authoring/dates-and-durations#choosing-a-format) for the four time kinds. A format applies to display only. The stored value keeps [full precision](/concepts/units-and-precision). For how values combine across types, see [Combining types](/authoring/writing-equations#combining-types). For authoring variables, see [Defining variables](/authoring/defining-variables). To list the variables in a published domain, use [`GET /v1/domains/:id/variables`](/api/domains). # Versioning Source: https://docs.truemath.ai/concepts/versioning Domains move through a draft-to-published lifecycle so calculation logic is explicit and controlled. Calculations run against the published version. Every [domain](/concepts/domains) is versioned. Calculation logic does not change silently underneath you: a domain moves through an explicit lifecycle, and calculations run against a published version. ## The lifecycle * **Draft** — your working copy of the domain. Editing a domain's [variables](/concepts/variables) or [activities](/concepts/activities) updates its draft. Drafts are not used for calculations through the API. * **Published** — the live version. Publishing promotes the draft so it becomes the version calculations run against. * **Archived** — a domain that has been retired and hidden from use. Only published domains are returned by the [API](/api/domains) and available for calculation. ## Why it matters Versioning makes the rules behind a result explicit. When business logic changes — a revised methodology, a new product definition — you update the draft and publish a new version deliberately, rather than altering live behavior in place. Combined with [provenance](/concepts/provenance), this is what lets a result be tied to the specific logic that produced it. You publish a domain from the builder after testing its activities. See [Testing and publishing](/authoring/testing-and-publishing). # Domain library Source: https://docs.truemath.ai/domain-library/overview Browse TrueMath's library of pre-built, role-based calculation domains across professional verticals. # Guarantees Source: https://docs.truemath.ai/introduction/guarantees What deterministic execution gives you: reproducible results, full precision across every step, provenance for every value, and versioned business logic. TrueMath exists to make math execution trustworthy. Five guarantees define what that means in practice. ## Determinism and reproducibility The same inputs produce the same outputs, every time. Results do not vary across runs or drift as models update. A calculation is a function of its inputs and the [version](/concepts/versioning) of the logic in effect — nothing else — so a result can be reproduced rather than re-estimated. One kind of input names a moment rather than a value. A value or default written as `today` or `now` becomes the date at the time you supply it, and a value already in a [scenario](/concepts/scenarios) does not change on its own afterwards. Supply the word again — in a new calculation, or a new scenario — and you get the date as of then, so a saved request carrying `today` does not describe the same question next week. Where a result has to be reproducible exactly, supply the date itself rather than the word. See [Dates](/math/types/dates). ## Full precision TrueMath carries values at full internal precision through every step of a calculation. Intermediate results are not rounded before being passed to the next step; rounding is applied only when a value is displayed. This eliminates the drift that accumulates when rounded values feed subsequent calculations — a failure mode that is invisible in a single step and compounding across many. See [Units and precision](/concepts/units-and-precision). ## Dimensional safety TrueMath treats units like a strongly-typed language, not cosmetic labels. Every value carries its unit, and an operation that is not dimensionally meaningful is rejected rather than silently computed. Operations that *are* meaningful produce the correct resulting unit: a length times a length is an area; a distance divided by a time is a velocity. This keeps a calculation from returning a confident but dimensionally nonsensical result. See [Units and precision](/concepts/units-and-precision). ## Provenance Every variable carries metadata about where its value came from — whether it was stated by the user, calculated, carried forward from a prior step, or supplied as a default. Because each value's origin is tracked, a result is not just a number; it is a number you can account for. See [Provenance](/concepts/provenance). ## Versioned business logic [Domains](/concepts/domains), and the [activities](/concepts/activities) within them, move through an explicit draft-to-published lifecycle. Calculations run against a published version of the logic, so the rules that produced a result are explicit and controlled rather than implicit and shifting. See [Versioning](/concepts/versioning). These guarantees hold regardless of which language model interprets a request. The model can change; the execution layer's behavior does not. # How TrueMath fits with LLMs Source: https://docs.truemath.ai/introduction/how-it-fits-with-llms LLMs reason and parse intent; TrueMath executes the math deterministically. How the two layers work together today, and why the boundary between them is permanent. Large language models and deterministic math engines are complementary layers, not competitors. LLMs are extraordinarily good at understanding intent: they parse natural language, handle ambiguity, and translate a request into structured values. They are the reasoning layer. TrueMath is the execution layer — it takes those values and computes the result exactly, the same way every time. ## Division of responsibility | The LLM handles | TrueMath handles | | ------------------------------------------- | --------------------------------------- | | Understanding a request in natural language | Executing the calculation | | Turning it into structured values | Carrying values at full precision | | Explaining a result in prose | Recording the provenance of every value | | Drafting domains for review | Enforcing versioned business logic | The model decides *what* to compute. TrueMath determines *the answer* — and can account for how it was reached. ## How it works today You work with TrueMath through the [Playground](/playground/tour) or the [API](/api/overview), and you choose a [domain](/concepts/domains) to calculate against. The LLM boundary shows up at one point: **input**. * **Natural language** — you describe the request in prose. TrueMath uses a language model to parse it into structured values and a calculation target, then executes deterministically. The model interprets; it never computes. * **Structured text** — you provide the values directly. No language model is involved; TrueMath executes them as given. * **JSON** — you provide the values as a structured object over the API. Like structured text, no language model is involved; TrueMath executes them as given. See [Input formats](/concepts/input-formats) for all three. Either way, TrueMath returns the result with every variable's value, unit, and [provenance](/concepts/provenance) — as raw JSON over the API, or presented visually in the Playground. ## Where this is heading The same division extends outward as new ways to connect arrive. Through an MCP interface, a CLI, and client libraries, an external agent or application will be able to drive TrueMath directly — reasoning about a request, calling TrueMath to execute it, and narrating the result — without changing what TrueMath itself does. These interfaces are not available yet; today the connection points are the Playground and the API. ## Why the boundary is permanent A language model is probabilistic by design: it predicts likely outputs, which is exactly what makes it powerful at language and reasoning. Mathematics is deterministic — there is one right answer. A more capable model reasons better; it does not become a deterministic execution engine. The two are architecturally different systems, and the right approach is to connect them rather than collapse them. This mirrors every trust-critical layer that came before: payment systems execute transactions exactly rather than approximating them, and database engines return precise results rather than estimates. TrueMath occupies the same position for computation in AI workflows. See [Guarantees](/introduction/guarantees) for what the execution layer gives you. # What is TrueMath Source: https://docs.truemath.ai/introduction/what-is-truemath TrueMath is a deterministic math execution engine for AI-native workflows. LLMs reason and parse intent; TrueMath executes the math — at full precision, with provenance, against versioned business logic. TrueMath is a deterministic mathematical execution engine. It is built to run in partnership with large language models in the AI stack: the model reasons about a request and parses intent, and TrueMath performs the actual calculation — deterministically, at full precision, with provenance for every value, against versioned business logic. When someone asks "what would my monthly payment be if I put 15% down instead of 20%," an LLM understands the question and identifies the values involved. TrueMath computes the answer and records how it got there. **LLMs reason. TrueMath executes.** This is a division of responsibility, not a competition. See [How TrueMath fits with LLMs](/introduction/how-it-fits-with-llms). ## What TrueMath is not TrueMath is not a better calculator, and it is not a smarter language model. It is execution infrastructure — the layer where correctness, reproducibility, and auditability are enforced. A language model predicts likely text; TrueMath computes guaranteed results. The two solve different problems, and TrueMath is built for the one where "close" is not acceptable. ## How you work with it You organize calculations into [domains](/concepts/domains) — collections of [variables](/concepts/variables) (the values you calculate with) and [activities](/concepts/activities) (the equations that relate them). TrueMath ships a [library of pre-built domains](/domain-library/overview), and you can author your own. A calculation runs inside a [conversation](/concepts/scenarios). You provide values — in natural language or as structured input — and name what you want to solve for. TrueMath returns the result along with every variable's value, unit, and [provenance](/concepts/provenance). Adjusting an input creates a new [scenario](/concepts/scenarios), so you can explore what-if questions without losing prior work. You can use TrueMath three ways: * **The Playground** — an interface for entering prompts and exploring calculations. Results are presented visually rather than narrated in prose — tables as tables, charts as charts — with the full underlying data available on demand. * **The Builder** — an environment for authoring domains. * **The API** — call TrueMath over HTTP from your own application or agent. Run your first calculation in the Playground and through the API. Domains, activities, variables, provenance, and scenarios. What "deterministic execution" actually gives you. Authenticate, run calculations, and read domains over HTTP. # Arithmetic and powers Source: https://docs.truemath.ai/math/functions/arithmetic-and-powers Core arithmetic, percent change, modulo, powers and roots, and logarithmic and exponential functions. These cover core arithmetic, percent change, modulo, powers, roots, logarithms, and exponential functions. Every operation works on a number or a [table](/math/types/tables), returning the same shape it was given. For operator precedence and the comparison and logical operators, see [Operators and precedence](/math/functions/operators-and-precedence). ## Core arithmetic ``` value1 + value2 # add value1 - value2 # subtract value1 * value2 # multiply value1 / value2 # divide ``` Each operation accepts two numbers, a number and a table, or two tables — producing a new table when a table is involved. Two tables must be the **same length**, or an `Invalid dimensions` error is returned. Dividing by `0` returns a `Division by 0` error; `1 / value` returns the reciprocal of `value`. ``` 3 + 4 => 7 [3; 4; 5] + 3 => [6; 7; 8] [6; 9; 12] + [3; 4; 5] => [9; 13; 17] 3 - 4 => -1 [6; 9; 12] - [3; 4; 5] => [3; 5; 7] 3 * 4 => 12 [3; 4; 5] * 3 => [9; 12; 15] 3 / 4 => 0.75 [6; 9; 12] / [3; 4; 5] => [2; 2.25; 2.4] ``` Each of these operations accepts [unit numbers](/math/types/unit-numbers) as well as plain ones. Compatible units convert before adding, and multiplication and division combine dimensions: ``` 1m + 10cm => 1.1 m 5m * 3 => 15 m 10m / 2s => 5 m/s 3m * 4m => 12 m^2 ``` ## Percent change \[ch] ``` ch(value; change) ch(value; change; periods) ``` Returns `value` adjusted by a percentage `change` (entered as a decimal): a positive `change` adds, a negative `change` subtracts. This is the financial calculator equivalent of `value + change%` or `value - change%`, which a spreadsheet would write as `value ± (value * change%)`. With `periods`, it compounds the change over that many periods (compound annual growth rate). `value` can be a number or a table. ``` ch(100; 0.25) => 125 ch(100; -0.25) => 75 ch(100; 0.25; 3) => 195.3125 ch(100USD; 0.25) => 125 USD ``` ## Modulo \[mod] ``` mod(value1; value2) ``` The remainder of `value1` divided by `value2`. A `value2` of `0` returns a `Division by 0` error. Either argument can be a number or a table; if both are tables they must be the same length, or `Invalid dimensions` is returned. If `value1` or `value2` is too large to calculate then returns 'Parameter out of range' error. The remainder takes the sign of `value1`. ``` mod(3; 2) => 1 mod(2; 3) => 2 mod(3; 3) => 0 mod(-8; 5) => -3 mod([3;4;5]; 2) => [1; 0; 1] mod([3;7;9]; [2;4;5]) => [1; 3; 4] ``` **With units.** `mod` works on [unit numbers](/math/types/unit-numbers) as readily as on plain ones. Both arguments must be the same kind of value: two plain numbers, or two units of the same dimension. Compatible units convert before the division, and the remainder is reported in `value1`'s unit. Anything else returns `input.incompatible_units`, including a bare number paired with a unit value. ``` mod(7m; 3m) => 1 m mod(90min; 1hr) => 30 min ``` That makes a time-of-day calculation direct. A [date](/math/types/dates) is a count of seconds, so the remainder of a date divided by one day is the time elapsed since midnight, with no stripping units and putting them back: ``` mod(appointment; 1 d) # => the time of day, as a duration from midnight mod(appointment d; 1 d) # => the same time of day, as a fraction of a day ``` The second line follows from that rule. [Cast](/math/units#casting-to-a-specific-unit) the date to days before dividing and the remainder arrives in days, so noon reads as `0.5 d` rather than `43200 s`. That is the fraction-of-a-day figure a spreadsheet keeps time in. Because the sign follows `value1`, a date before 1970 is a negative value and gives a negative remainder. ## Powers and roots ### Power \[^] ``` value ^ x ``` Raises `value` to the `x` power. `value` can either be a number or table where the same is returned. Several forms are common: * `value ^ 2`, `value ^ 3` — squared, cubed. * `value ^ -1` — the reciprocal of `value` (see also division). * `value ^ (1/2)` — the square root, the same as `sqrt(value)`. * `value ^ (1/x)` — the `x`th root, the same as `root(value; x)`. ``` 4^2 => 16 -2^2 => -4 9^(1/2) => 3 4^-1 => 0.25 [3; 4; 5]^2 => [9; 16; 25] ``` **With a unit, the exponent binds to the unit, not to the value.** `m^2` is the unit *square metres*, so `2m^2` is two square metres — not `(2m)` squared. Parenthesize the value to raise it: ``` 2m^2 => 2 m^2 (2m)^2 => 4 m^2 ``` ### Square root \[sqrt] ``` sqrt(value) ``` The square root of `value`. `value` can either be a number or table where the same is returned. A root reduces the dimension along with the value: the square root of an area is a length. ``` sqrt(16) => 4 sqrt([9; 16; 25]) => [3; 4; 5] sqrt(16m^2) => 4 m ``` ### Root \[root] ``` root(value; x) ``` The `x`th root of `value`. `root(value; 2)` is the same as `sqrt(value)`. `value` can either be a number or table where the same is returned. ``` root(16; 2) => 4 root([9; 16; 25]; 2) => [3; 4; 5] root(27m^3; 3) => 3 m ``` ## Logarithms and exponentials These take a **dimensionless** value. An argument carrying a unit returns `input.incompatible_type` — convert it to a bare number first with [`unitvalue`](/math/functions/units#unit-value-unitvalue). ### Natural logarithm \[ln] ``` ln(value) ``` The natural logarithm (base *e*) of `value`. `value` can either be a number or table where the same is returned. ``` ln(10) => 2.3026 ln([2; 3; 4]) => [0.6931; 1.099; 1.3863] ``` ### Exponential \[exp] ``` exp(value) ``` The constant *e* (2.718281828459045) raised to `value`. `value` can either be a number or table where the same is returned. There is no `e` constant; write `exp(1)` when you need *e* itself. ``` exp(10) => 22026.4658 exp([0.5; 0.75; 2]) => [1.6487; 2.117; 7.3891] ``` ### Logarithm \[log] ``` log(value) log(value; base) ``` The logarithm of `value` in base 10, or in `base`. `value` can either be a number or table where the same is returned. `base` must be a whole number ≥ 2; real numbers are truncated, and values outside the range return a `Parameter out of range` error. ``` log(15) => 1.1761 log(15; 3) => 2.4650 log([10; 12; 14]; 4) => [1.661; 1.7925; 1.9037] ``` ### Antilogarithm \[alog] ``` alog(value) ``` The inverse (base 10) logarithm of `value`. `value` can either be a number or table where the same is returned. ``` alog(1.25) => 17.7828 alog([0.8; 1.2; 1.4]) => [6.3096; 15.8489; 25.1189] ``` # Conditionals and logic Source: https://docs.truemath.ai/math/functions/conditionals-and-logic Branch with if and choose, repeat with loop, and combine conditions with comparison and logical operators. These functions let an [activity](/concepts/activities) branch, repeat, and combine conditions. Comparisons and logical operators return a [boolean](/math/types/scalar-numbers#booleans) — `1` (true) or `0` (false); see [Operators and precedence](/math/functions/operators-and-precedence). ## If \[if] ``` if(comparison; true_statement; false_statement) ``` Evaluates `comparison`; returns `true_statement` if it is true, otherwise `false_statement`. Any expression can appear in any position, and `if` calls can be nested on either branch. `comparison` uses one of the comparison operators: | Operator | Meaning | | -------- | ------------------------------------ | | `==` | equal to (two equals signs, not one) | | `!=` | not equal to | | `<` | less than | | `<=` | less than or equal to | | `>` | greater than | | `>=` | greater than or equal to | ``` if(Sales < 100000; 4; 5) # => 4 when Sales is less than 100,000; 5 when Sales is greater than or equal to 100,000 if(Value == 25; 1; 0) # => 1 when Value is 25, otherwise 0 ``` Use `==` (comparison) inside conditions, not `=` (assignment). See [Operators and precedence](/math/functions/operators-and-precedence). `if` returns whichever branch it takes, with that branch's [unit](/math/units) — nothing converts one branch to match the other: ``` if(1; 5m; 10ft) => 5 m if(0; 5m; 10ft) => 10 ft ``` **The branches are not checked against each other.** `if(1; 5m; 10kg)` returns `5 m` without complaint, because only the branch taken is evaluated. Two branches that disagree dimensionally will work for every scenario that takes one of them and fail on the first scenario that takes the other. Keep both branches in the same dimension, and [round-trip](/authoring/testing-and-publishing#testing-a-draft) an activity through both. ### Combining conditions Combine comparisons with `&&` (and), `||` (or), and `!` (not). A good rule of thumb is to write the condition the way you would say it out loud. ``` if(Sales > 100000 && Sales < 500000; 0.05; 0.04) # both hold — exclusive bounds if(Sales >= 100000 && Sales <= 500000; 0.05; 0.04) # inclusive bounds — note >= and <= if(Sales < 100000 || Sales > 500000; 0.04; 0.05) # either holds if(Sales > 100000 && Sales < 500000 || Sales > 750000 && Sales < 900000; 0.05; 0.04) if(!(Sales >= 100000 && Sales <= 500000); 0.04; 0.05) # ! reverses the result ``` Strict (`>`, `<`) and inclusive (`>=`, `<=`) comparisons differ exactly at the boundary value — choose deliberately. Nested calls cover more than two outcomes — here, a different result for "more than 10 over," "1–10 over," and "not over": ``` if(Your_Speed > Posted_Speed + 10; Your_Speed * 3 + 100; if(Your_Speed > Posted_Speed; Your_Speed * 2 + 50; 0)) ``` ## Choose \[choose] ``` choose(index; expression1; ...; expressionN) ``` Returns the expression at position `index` (starting with 1) — a shorthand for a chain of `if` statements. `index` must be a whole number from 1 to N, an expression that evaluates to a whole number, or a `Parameter out of range` error is returned. ``` choose(index; 25; 40; 50) # => 25 when index is 1; 40 when 2; 50 when 3 choose(2; 5m; 10ft; 2yd) => 10 ft ``` Like [`if`](#if-if), `choose` returns the expression it lands on, carrying that expression's unit, and the alternatives are not compared with one another. ## Loop \[loop] ``` loop(from; to) + (expression) loop(from; to; step) + (expression) ``` Repeats `expression` from `from` to `to`, building a [table](/math/types/tables) with one row per iteration. It counts by `1` unless you pass a `step`, and ends when the index passes `to`. Two special variables are available inside the expression: * `index` — the current value of the loop counter. It starts at `from` and advances by `step`, so the default `loop(1; n)` runs `1, 2, … n` while `loop(1; 5; 2)` runs `1, 3, 5`. * `last` — the value from the previous iteration (`0` on the first). Any variable named `index` or `last` is ignored in favor of the loop's own. **Direction.** With no `step`, the loop counts by `1` toward `to` — downward when `to` is below `from`. An explicit `step` must point toward `to`, or an `input.out_of_range` error is returned: `loop(5; 1; -2)` runs, `loop(5; 1; 1)` does not. A fractional `step` is allowed; the last row is the final index that has not passed `to`, so `loop(5; 1; -1.1)` runs `5, 3.9, 2.8, 1.7`. ``` loop(1; 5) + (10) => [10; 10; 10; 10; 10] loop(1; 5) + (index) => [1; 2; 3; 4; 5] loop(1; 5) + (last) => [0; 0; 0; 0; 0] loop(1; 5) + (last + 2) => [2; 4; 6; 8; 10] loop(1; 5) + (last * 2 + index) => [1; 4; 11; 26; 57] loop(1; 5; 2) + (last * 3 + index) => [1; 6; 23] ``` ### Looping over a unit range `from`, `to`, and `step` each take a [unit value](/math/types/unit-numbers) as readily as a scalar, and `index` carries the unit into the expression. All of them must share a dimension: mixing a bare scalar with a unit value returns an `input.incompatible_units` error. A bound of `0` is the exception — `from` or `to` may be a bare `0` alongside unit-bearing arguments, and takes its unit from them. `index` is expressed in the unit of the increment — the `step`'s unit when you pass one, otherwise the unit of the first bound that carries one. Bounds given in other [units](/math/units) are converted to it, so a loop from `1mo` to `5yr` runs sixty monthly rows. A `mo` is a fixed [30.4375 days](/math/units#time-units-and-dates) and a `yr` a fixed 365.25 days, so a range in months rarely comes out even in another unit — 12 months is 52.18 weeks, not 52. Every `mo` step is that same fixed length, which is not what a calendar month does. To build a payment schedule or a monthly projection, loop over a plain count and move the date with [`adjdate`](/math/functions/dates#calendar-adjustment-adjdate). ``` loop(1yr; 5yr) + (index) => [1yr; 2yr; 3yr; 4yr; 5yr] loop(1yr; 5yr; 6mo) + (index) => [12mo; 18mo; 24mo; 30mo; 36mo; 42mo; 48mo; 54mo; 60mo] loop(0; 12mo) + (index) => [0mo; 1mo; 2mo; 3mo; 4mo; 5mo; 6mo; 7mo; 8mo; 9mo; 10mo; 11mo; 12mo] loop(1yr; 0; -3mo) + (index) => [12mo; 9mo; 6mo; 3mo; 0mo] loop(1mo; 5yr) + (index) # => sixty rows, 1mo through 60mo ``` ### Building a series Two loop patterns recur when projecting values over time. **Compound growth** — a value that grows at a fixed rate each period from a first-period base: ``` loop(1; ceil(periods)) + (base * (1 + growth_rate) ^ (index - 1)) ``` The `index - 1` exponent holds the first period at `base` (`^ 0` is `1`), and [`ceil`](/math/functions/rounding-and-numeric#ceiling-ceil) guards against a fractional `periods`. The result is one row per period. **Running total** — a cumulative sum across an existing table, with `last` as the accumulator: ``` loop(1; length(series)) + (last + item(series; index)) ``` Each row holds the total through that period, so the final row — `item(result; length(result))` — is the all-up total. See [`item`](/math/functions/tables#item-item) and [`length`](/math/functions/tables#length-length) in [Table functions](/math/functions/tables). # Dates Source: https://docs.truemath.ai/math/functions/dates Build a date from calendar components, read its parts, adjust it by calendar days, months, and years, and count the days between two dates. These functions build and read [dates](/math/types/dates). A date is a time value counted in seconds from the Unix epoch, so shifting a date — adding a duration, subtracting two dates — is ordinary [unit](/math/units) arithmetic, with no function involved. What the functions here provide is the calendar: constructing a date from components, reading those components back, and the two operations where calendar structure matters, [`adjdate`](#calendar-adjustment-adjdate) and [`ddays`](#day-counts-ddays). Every date argument below is an ordinary numeric value. The [supported range](/math/types/dates#supported-range-and-calendar-rules) runs from `1900-01-01 00:00:00` through `2200-12-31 23:59:59.999…`: building a date outside it returns `input.out_of_range`, while reading a value that falls outside it — one that arithmetic carried past either end — returns `math.out_of_range`. ## Building a date \[date] ``` date(year) date(year; month) date(year; month; day) date(year; month; day; hour) date(year; month; day; hour; minute) date(year; month; day; hour; minute; second) ``` A date built from **numeric components**. Omitted components take the start of their period, so `date(2026)` is midnight on January 1 and `date(2026; 7)` is midnight on July 1. `date()` has no string form — it takes numbers, not text like `"7/21/2026"`. Write text like that as a value you supply: it becomes a number before the calculation runs, so an equation only ever operates on the number. See [Dates](/math/types/dates). ``` date(2026) => date(2026; 1; 1; 0; 0; 0) date(2026; 7) => date(2026; 7; 1) date(2026; 7; 21) => date(2026; 7; 21; 0; 0; 0) ``` Construction is **strict**. Every component is range-checked against the calendar, and an out-of-range component returns `input.out_of_range` rather than rolling over into the neighboring month or year. Each of these is an error: `date(2026; 2; 29)` because 2026 is not a leap year, `date(2026; 13; 1)` because there is no thirteenth month, and `date(1899; 12; 31)` or `date(2201; 1; 1)` because the year is outside the supported range. To roll a value over on purpose, use [`adjdate`](#calendar-adjustment-adjdate). ## Date components ``` year(date) month(date) day(date) hour(date) minute(date) second(date) ``` The named component of `date`, as a bare [scalar](/math/types/scalar-numbers) rather than a unit number — so a component can be used directly in arithmetic and in `date()`. Passing all six back to `date()` reconstructs the value it came from. ``` year(date(2026; 7; 21)) => 2026 month(date(2026; 7; 21)) => 7 day(date(2026; 7; 21)) => 21 hour(date(2026; 7; 21; 14; 30)) => 14 minute(date(2026; 7; 21; 14; 30)) => 30 ``` `second` is the finest accessor. Recover milliseconds from its fractional part with [`fpart`](/math/functions/rounding-and-numeric#fractional-part-fpart) — `fpart(second(d))` is the sub-second remainder, in seconds. The accessors read one component at a time. For the whole **time of day** as a single quantity, take the remainder of the date divided by one day with [`mod`](/math/functions/arithmetic-and-powers#modulo-mod): `mod(d; 1 d)` is the time elapsed since midnight, and `floor(date; "d")` is the midnight it counts from. [Cast](/math/units#casting-to-a-specific-unit) the date to days first — `mod(date d; 1 d)` — and the same time of day comes back as a fraction of a day, `0.5 d` at noon. [`fpart`](/math/functions/rounding-and-numeric#fractional-part-fpart) produces that second figure without `mod`. Cast the date to the unit you want to split at and take the fractional part: `fpart(date d)` is the fraction of the day, `fpart(date hr)` the part-hour. [`ipart`](/math/functions/rounding-and-numeric#integer-part-ipart) gives the whole part alongside it. ## Day of week \[weekday] ``` weekday(date) ``` The day of the week as a number from `1` to `7`, where **`1` is Sunday** and `7` is Saturday — the same numbering as the default `WEEKDAY` in Excel and Google Sheets. ``` weekday(date(2026; 7; 28)) => 3 weekday(date(2030; 6; 21)) => 6 weekday(date(1955; 1; 3)) => 2 ``` Those are a Tuesday, a Friday, and a Monday. To test for a weekend, compare against `1` and `7`. ## Leap year test \[isleapyear] ``` isleapyear(date) ``` `true` if `date` falls in a leap year, `false` otherwise, under the Gregorian rules: divisible by 4, except century years, except century years divisible by 400. ``` isleapyear(date(2000)) => true isleapyear(date(1900)) => false isleapyear(date(2024)) => true isleapyear(date(2026)) => false ``` ## Calendar adjustment \[adjdate] ``` adjdate(date; days) adjdate(date; days; basis) adjdate(date; days; months; years) ``` **`adjdate` is the only calendar-correct way to step a date by months or years.** `date + 1 mo` and `date + 1 yr` shift by a fixed average — 30.4375 and 365.25 days — which lands on a different day of the month, or on the right day at the wrong time of day. `date(2026; 7; 21) + 1 yr` is July 21, 2027 at *06:00*, and nothing reports an error. See [`mo` and `yr` shift by an average](/math/types/dates#mo-and-yr-shift-by-an-average-not-a-calendar-step). A **calendar-correct** shift of `date` by whole days, months, and years. Each amount may be negative, and adjustments roll across month and year boundaries automatically. `basis` applies a [day-count convention](#day-counts-ddays) to the `days` amount and defaults to `0`, actual days. **The third argument is `basis`, not `months`.** Adjusting by months or years requires the four-argument form. `adjdate(d; 0; 1; 0)` is one month later; `adjdate(d; 0; 1)` shifts by zero days under day-count basis `1`, which leaves the date where it was. Amounts apply from the largest unit down: **years, then months, then days.** ``` adjdate(date(2026; 8; 11); 60) => date(2026; 10; 10) adjdate(date(2025; 12; 28); 5) => date(2026; 1; 2) adjdate(date(2026; 8; 11); 0; 2; 0) => date(2026; 10; 11) adjdate(date(2026; 8; 11); 0; 0; 1) => date(2027; 8; 11) ``` ### Month ends Adjusting by whole months or years keeps the **same day number** — the 15th stays the 15th. Two more rules apply at month ends, where the day number may not exist in the target month, or where the starting date is the last day of its own month. Both are read from the date, so nothing is carried between adjustments: * **The last day of a month maps to the last day of the target month.** A month-end date stays on month ends as you add months, even as the lengths of those months change. * **A day that does not exist in the target month clamps back** to that month's last day — the 31st of a 30-day month becomes the 30th, and the 30th of February becomes the 28th or 29th. ``` adjdate(date(2026; 1; 15); 0; 1; 0) => date(2026; 2; 15) adjdate(date(2026; 1; 31); 0; 1; 0) => date(2026; 2; 28) adjdate(date(2026; 1; 30); 0; 1; 0) => date(2026; 2; 28) adjdate(date(2024; 2; 29); 0; 0; 1) => date(2025; 2; 28) ``` Because "last day of the month" is read from the date each time rather than carried along, chaining is predictable. Once a date lands on a month end it keeps landing on month ends: ``` adjdate(date(2026; 1; 30); 0; 1; 0) => date(2026; 2; 28) adjdate(date(2026; 2; 28); 0; 1; 0) => date(2026; 3; 31) adjdate(date(2026; 3; 31); 0; 1; 0) => date(2026; 4; 30) ``` Note the second line: February 28 *is* February's last day in 2026, so a month later is March's last day, not March 28. The same rules mean a month forward and a month back does not always return the starting date. January 30 clamps to February 28, and February 28 is a month end, so stepping back lands on January 31: ``` adjdate(date(2026; 1; 30); 0; 1; 0) => date(2026; 2; 28) adjdate(date(2026; 2; 28); 0; -1; 0) => date(2026; 1; 31) ``` To generate a monthly schedule, put `adjdate` inside a [`loop`](/math/functions/conditionals-and-logic#loop-loop) and step the month count rather than the date, so every row is measured from the original date instead of from the previous row: ``` loop(1; term_months) + (adjdate(start_date; 0; index; 0)) ``` ## Day counts \[ddays] ``` ddays(date1; date2) ddays(date1; date2; basis) ``` The number of days from `date1` to `date2`, as a bare scalar. The count is **signed** — negative when `date2` is the earlier of the two. ``` ddays(date(1999; 9; 15); date(1999; 10; 20)) => 35 ddays(date(1999; 8; 30); date(1999; 12; 11)) => 103 ddays(date(1999; 9; 15); date(1999; 8; 10)) => -36 ``` `basis` sets the day-count convention, the same set the finance world uses for accruing interest over a partial period: | `basis` | Convention | Effect | | --------------- | --------------- | ----------------------------------------------------------------------------------- | | `0` *(default)* | Actual / actual | Actual calendar days. | | `1` | 30/360 | Every month counts as 30 days and every year as 360, with end-of-month adjustments. | | `2` | Actual / 360 | Actual calendar days, against a 360-day year. | | `3` | Actual / 365 | Actual calendar days, against a 365-day year. | Only basis `1` changes the day count itself. Bases `0`, `2`, and `3` all count actual days and return the same number from `ddays` — they differ in the year length a later step uses when expressing the span as a fraction of a year. ``` ddays(date(1999; 8; 30); date(1999; 12; 11); 1) => 101 ddays(date(1999; 9; 15); date(1999; 8; 10); 1) => -35 ddays(date(2026; 1; 1); date(2026; 12; 31); 1) => 360 ``` The 30/360 end-of-month adjustments follow the standard financial definitions. `ddays` reads the calendar date of each argument and ignores the time of day, so any two instants on the same day give the same count. In an [activity](/concepts/activities), `ddays` also runs the other way: either date can be marked [calculable](/authoring/writing-activities#calculable-variables) and calculated from the day count and the other date. This works under every basis, in both the two- and three-argument forms. Under basis `1`, several dates can give the same count, because the 30/360 adjustments read the 31st as the 30th: ``` ddays(date(1999; 8; 30); date(1999; 8; 31); 1) => 0 ddays(date(1999; 9; 1); date(1999; 8; 31); 1) => 0 ``` A date calculated from a 30/360 count is therefore *a* date that satisfies the count, not necessarily the one the count came from — re-deriving a date you already had can return a different one. Where a date has to round-trip exactly, count with the default basis. ## Snapping a date to a boundary [`floor`](/math/functions/rounding-and-numeric#floor-floor), [`ceil`](/math/functions/rounding-and-numeric#ceiling-ceil), and [`round`](/math/functions/rounding-and-numeric#round-round) take a time unit as a **quoted string** in place of a decimal place, which snaps a date to that boundary — the start of the day, the start of the month, the nearest hour. ``` floor(date(2026; 7; 21; 9; 30); "d") => date(2026; 7; 21) ceil(date(2026; 7; 21; 9; 30); "mo") => date(2026; 8; 1) ``` This is also how you compare dates at a coarser granularity than the instant: snap both sides to the same unit, then compare. See [Snapping a date to a boundary](/math/functions/rounding-and-numeric#snapping-a-date-to-a-boundary) for the units accepted and what each one zeroes. ## Common patterns The same operations with variable names in place of literal dates, as an [activity](/concepts/activities) would use them: ``` closing_date + 30 d # a date 30 days out adjdate(start_date; 0; term_months; 0) # the same day of the month, term_months later ddays(invoice_date; payment_date) # days outstanding, negative if paid early (payment_date - invoice_date) wk # the exact span, reported in weeks (ddays(invoice_date; payment_date) d) wk # the whole-day count, reported in weeks floor(start_date; "mo") # the first of the starting month adjdate(ceil(start_date; "mo"); -1) # the last day of the starting month mod(appointment; 1 d) # the time of day, as a duration from midnight weekday(delivery_date) == 1 # true when the delivery falls on a Sunday ddays(floor(report_date; "yr"); report_date) + 1 # the day of the year ``` Note the `d` inside the third line. `ddays` returns a bare count, so it needs a unit attached before it can be converted: casting a plain number straight to `wk` relabels it rather than converting it, turning a 14-day span into 14 weeks. The second and third lines also answer slightly different questions. Subtracting is exact to the second, while `ddays` counts calendar days and ignores the time of day, so the two agree only when both dates sit at the same time of day. Inside an activity equation, wrap the unit literals in backticks — `closing_date + 30`d\`\` — so they read as units rather than variable names. See [Units in equations](/authoring/writing-equations#units-in-equations). # Finance Source: https://docs.truemath.ai/math/functions/finance Time-value-of-money and amortization functions, uniform and single-payment series, interest-rate conversion, and cash-flow analysis — with their unit rules. These functions cover the time value of money, amortization, and cash-flow analysis. ## Conventions Across all of them: * Cash **inflows are positive** and **outflows are negative**. * Interest rates are entered and returned as **decimals** (`0.08`, not `8%`). * `periods_per_year` and `compounds_per_year` default to `12`; a value less than 1 returns a `Parameter out of range` error. The [cash flow analysis](#cash-flow-analysis) functions take neither — their rate is [per period](#periodic-rates). * `beginning_of_period` is `false` for end-of-period (the default) or `true` for beginning-of-period. ## Value and period units The time-value-of-money and amortization functions accept units on their value and period arguments, with these rules: * **Values** — `present_value`, `future_value`, and `payment` may each be a [currency value](/math/types/unit-numbers) (`375000 USD`) or a plain [scalar](/math/types/scalar-numbers) (`375000`). Within a single call they must all be the **same kind**. A value of `0` is the exception: it is accepted for any of these arguments regardless of the others. * **Periods** — `periods` may be a [time value](/math/units) (`30 yr`) or a scalar. A time value is converted to a number of periods by converting it to years and multiplying by `periods_per_year`. A scalar is used as the number of periods unchanged. * **Solving for periods** — when `periods()` solves for the number of periods, its return type mirrors the value arguments. If `present_value`, `future_value`, and `payment` are currency values, the result is a [time value](/math/units) in **years** (the period count divided by `periods_per_year`). If they are scalars, the result is a bare scalar count of periods. See [`periods`](#number-of-periods-periods). ## Time value of money The five core functions each solve for one variable given the others. Each accepts the optional tail `periods_per_year; compounds_per_year; beginning_of_period`. `periods` of `0` returns a result of `0`. ### Present value \[pv] ``` pv(future_value; payment; interest_rate; periods) pv(future_value; payment; interest_rate; periods; periods_per_year; compounds_per_year; beginning_of_period) ``` *Follows the shared [conventions](#conventions) and [value and period units](#value-and-period-units).* ``` pv(0; -2000; 0.05; 60) => 105981.4126 pv(0; -2000; 0.05; 60; 4; 4; false) => 84069.1836 pv(0; -3000 USD; 0.05; 360) => 558844.8511 USD pv(500000 USD; -2000 USD; 0.05; 360) => 260649.9363 USD ``` ### Future value \[fv] ``` fv(present_value; payment; interest_rate; periods) fv(present_value; payment; interest_rate; periods; periods_per_year; compounds_per_year; beginning_of_period) ``` *Follows the shared [conventions](#conventions) and [value and period units](#value-and-period-units).* ``` fv(50000; 500; 0.1; 240) => -746088.0997 fv(50000; 500; 0.1; 20; 1; 1; false) => -365012.4972 fv(-500000 USD; 2000 USD; 0.03; 360) => 62947.3366 USD ``` ### Payment \[pmt] ``` pmt(present_value; future_value; interest_rate; periods) pmt(present_value; future_value; interest_rate; periods; periods_per_year; compounds_per_year; beginning_of_period) ``` *Follows the shared [conventions](#conventions) and [value and period units](#value-and-period-units).* ``` pmt(300000; 0; 0.07; 360) => -1995.9075 pmt(300000; 0; 0.07; 30; 1; 1; false) => -24175.9211 pmt(-500000 USD; 0; 0.04; 360) => 2387.0765 USD ``` ### Interest rate \[rate] ``` rate(present_value; future_value; payment; periods) rate(present_value; future_value; payment; periods; periods_per_year; compounds_per_year; beginning_of_period) ``` Returns the interest rate as a decimal — a bare number, even when the values carry a currency. *Follows the shared [conventions](#conventions) and [value and period units](#value-and-period-units).* ``` rate(300000; 100000; -4000; 360) => 0.158088 rate(-150000; 100000; -4000; 30; 1; 1; 0) => -0.047224 rate(-500000 USD; 0; 2000 USD; 360) => 0.0259 ``` ### Number of periods \[periods] ``` periods(present_value; future_value; payment; interest_rate) periods(present_value; future_value; payment; interest_rate; periods_per_year; compounds_per_year; beginning_of_period) ``` *Follows the shared [conventions](#conventions) and [value and period units](#value-and-period-units).* The return type mirrors the present value, future value, and payment arguments. With **scalar** values, `periods()` returns a bare scalar count. With **currency** values, it returns a [time value](/math/units) in **years** — the period count divided by `periods_per_year` — because the result carries the period unit rather than a dimensionless count. ``` periods(300000; 0; -4000; 0.06) => 94.2355 periods(300000 USD; 0; -4000 USD; 0.06) => 7.853 yr ``` These are equivalent results: with `periods_per_year` at its default of `12`, the first call returns the count in periods (months) and the second returns it in years (`94.2355 periods / 12 periods per year = 7.853 yr`). To recover a scalar count from a currency-valued call, convert the result to the unit that matches one period and strip it with [`unitvalue`](/math/functions/units#unit-value-unitvalue). At the default of 12 periods per year, one period is a month, so convert to `"mo"`: ``` unitvalue(periods(300000 USD; 0; -4000 USD; 0.06); "mo") => 94.2355 ``` This `"mo"` form is exact only because `periods_per_year` is `12`. For any other cadence, recover the count by converting to years and multiplying by `periods_per_year` — `unitvalue(periods(...); "yr") * periods_per_year` — which holds regardless of how many periods fall in a year. Recovering the count matters when a downstream calculation multiplies it by a currency amount: a `yr`-typed count raises `input.incompatible_units` ("Cannot multiply Currency and Time"), while the recovered scalar count multiplies cleanly. ## Amortization These analyze a loan or investment over its schedule. They follow the same [value and period unit rules](#value-and-period-units) as the functions above, and the `begin` and `end` arguments follow the same rule as `periods` — a time value is converted to periods, a scalar is used unchanged. In addition, a fractional `begin` or `end` is reduced to its **integer part** (`1.5` → `1`, `11.3` → `11`). Each function also accepts a final `positive_result` argument: set it to `true` to always report the result as a positive number. `periods` of `0` returns `0` (or, for `amortization`, an empty table). ### End balance \[endbal] ``` endbal(end; present_value; future_value; payment; interest_rate; periods) endbal(...; periods_per_year; compounds_per_year; beginning_of_period) endbal(...; beginning_of_period; positive_result) ``` The balance remaining at the end of period `end`. *Follows the shared [conventions](#conventions) and [value and period units](#value-and-period-units); see [Amortization](#amortization) for the period and `positive_result` rules.* ### Principal paid \[prnpaid] ``` prnpaid(begin; end; present_value; future_value; payment; interest_rate; periods) prnpaid(...; periods_per_year; compounds_per_year; beginning_of_period) prnpaid(...; beginning_of_period; positive_result) ``` The total **principal** paid from period `begin` to period `end`, inclusive. *Follows the shared [conventions](#conventions) and [value and period units](#value-and-period-units); see [Amortization](#amortization) for the period and `positive_result` rules.* ### Interest paid \[intpaid] ``` intpaid(begin; end; present_value; future_value; payment; interest_rate; periods) intpaid(...; periods_per_year; compounds_per_year; beginning_of_period) intpaid(...; beginning_of_period; positive_result) ``` The total **interest** paid from period `begin` to period `end`, inclusive. *Follows the shared [conventions](#conventions) and [value and period units](#value-and-period-units); see [Amortization](#amortization) for the period and `positive_result` rules.* ### Amortization table \[amortization] ``` amortization(begin; end; present_value; future_value; payment; interest_rate; periods) amortization(...; periods_per_year; compounds_per_year; beginning_of_period) amortization(...; beginning_of_period; positive_result) ``` A [table](/math/types/tables) of amortization results for each period from `begin` to `end`, inclusive. Each row is one period; its four columns are, in order, the **payment**, the **principal paid**, the **interest paid**, and the **end balance**. To chart how a payment splits between principal and interest across the schedule, slice those two columns with [`columns`](/math/functions/tables#column-column-columns): `columns(schedule; 2; 3)` returns a two-column table ready to show as a [bar chart](/authoring/charts). A single column is taken the same way — `column(schedule; 4)` is the end-balance curve. *Follows the shared [conventions](#conventions) and [value and period units](#value-and-period-units); see [Amortization](#amortization) for the period and `positive_result` rules.* ## Uniform and single payment series These take an `interest_rate` **per compounding period** and a number of `periods`. `uspv()` calculates the present value of a series of $1 payments while `usfv()` calculates the future value of a series of $1 payments. `sppv()` calculates the present value of $1 while `spfv()` calculates the future value of $1. Because the rate is per period, divide an annual rate by the number of compounding periods per year — for a 6% annual rate compounding monthly, pass `0.06 / 12`. TrueMath stores percentages as a decimal value. Alternatively, you can write it as `rate / 100`. `periods` must be a plain [scalar](/math/types/scalar-numbers) count. Unlike the [time value of money](#time-value-of-money) and amortization functions, these four take no `periods_per_year` to convert a duration against, and the length of a period is implied by the rate rather than stated — so a [time value](/math/units) such as `360 mo` returns an `input.incompatible_units` error instead of a period count. ``` uspv(interest_rate; periods) # uniform series present value usfv(interest_rate; periods) # uniform series future value sppv(interest_rate; periods) # single payment present value spfv(interest_rate; periods) # single payment future value ``` ``` uspv(0.06 / 12; 360) => 166.7916 usfv(0.06 / 12; 360) => 1004.515 sppv(0.06 / 12; 360) => 0.166 spfv(0.06 / 12; 360) => 6.0226 ``` ## Interest rate conversion ``` effrate(nominal_rate; compounds_per_year) nomrate(effective_rate; compounds_per_year) ``` `effrate` converts a nominal rate to an effective annual rate; `nomrate` does the reverse. A `compounds_per_year` of `0` denotes continuous compounding; a negative value returns a `Parameter out of range` error. Both take and return decimals. `compounds_per_year` is a **count** of compounding periods within a year, not a span of time. A [time value](/math/units) such as `30 yr` returns an `input.incompatible_units` error. ``` effrate(0.06; 12) => 0.0617 effrate(0.06; 0) => 0.0618 nomrate(0.06; 12) => 0.0584 nomrate(0.06; 0) => 0.0583 ``` ## Cash flow analysis These functions evaluate an **uneven** series of cash flows held in a [table](/math/types/tables) — a project's outlay and the returns that follow it, a lease, an investment with irregular contributions. Where the [time value of money](#time-value-of-money) functions assume one level `payment` repeated every period, these read the flows period by period from the table. They share a table shape and a period model, described below. ### Reading the cash flow table The table has **one or two columns**: * **Column one — amounts.** One cash flow per row, [inflows positive and outflows negative](#conventions). Each cell is a [scalar](/math/types/scalar-numbers) or a [currency value](/math/types/unit-numbers); any other unit returns an incompatible-type error. Currency amounts must share a single currency — TrueMath supports `USD` today, and amounts in different currencies are not converted. * **Column two — frequencies (optional).** How many **consecutive periods** the amount on that row repeats: a frequency of `4` is four periods at that amount. Each cell must be a non-negative whole number, or an incompatible-type error is returned. A frequency of `0` drops the row, contributing no periods. With no second column, every row is one period. Further columns are ignored. The notations overlap — `[500; 500; 500]` and `[[500; 3]]` are the same three periods either way — so the second column is how you compress a long schedule: fifteen years of monthly flows at four distinct amounts is four rows rather than 180. Repeat counts lengthen the series, and the result follows: ``` npv([280; 1492.56; 380.11; 125.25; 100; 56.78; 21.34]; 0.05) => 2297.1354 npv([[280; 1]; [1492.56; 2]; [380.11; 2]; [125.25; 3]; [100; 1]; [56.78; 1]; [21.34; 6]]; 0.05) => 4151.0712 ``` The first call is seven periods, one per row. The second holds the same seven amounts but repeats them, spanning sixteen. **The first row is the flow at period 0.** It is the flow at the start, before any period has elapsed: it is not discounted, and it does not count as a period. The rows after it are periods 1, 2, 3, and so on — so a six-row table spans period 0 through period 5, and the series runs for five periods. Where the first row carries a frequency greater than 1, its first occurrence is period 0 and the rest fall in the periods after it: `[[-5000; 3]; ...]` is an outflow of 5,000 at periods 0, 1, and 2. Two consequences worth planning for. An initial investment belongs on the first row, where it is taken at face value; a series whose first flow arrives one period out needs an explicit `0` on the first row to place it correctly. And **trailing rows extend the series** — rows with an amount of `0` add periods without adding value, which leaves `npv`, `irr`, `payback`, and `profindex` untouched but moves `nfv`, `nus`, and `mirr`, all three of which depend on how many periods the series runs for: ``` npv([[-20000; 1]; [500; 4]; [1000; 4]; [2000; 4]; [3000; 4]]; 0.02) => 928.4409 npv([[-20000; 1]; [500; 4]; [1000; 4]; [2000; 4]; [3000; 4]; [0; 4]]; 0.02) => 928.4409 nfv([[-20000; 1]; [500; 4]; [1000; 4]; [2000; 4]; [3000; 4]]; 0.02) => 1274.5504 nfv([[-20000; 1]; [500; 4]; [1000; 4]; [2000; 4]; [3000; 4]; [0; 4]]; 0.02) => 1379.6144 ``` **What the results carry.** When the amounts carry a currency, the functions that return money return that currency: [`npv`](#net-present-value-npv), [`nfv`](#net-future-value-nfv), and [`nus`](#net-uniform-series-nus). The rest return a bare number whatever the table carries, because what they measure is not money — [`irr`](#internal-rate-of-return-irr) and [`mirr`](#modified-internal-rate-of-return-mirr) are rates per period, [`payback`](#payback-period-payback) is a count of periods, and [`profindex`](#profitability-index-profindex) is a ratio. ### Periodic rates `interest_rate` is the rate for **one period of the table**. These functions take no `periods_per_year` or `compounds_per_year` argument and make no assumption about how long a period is, so convert an annual rate yourself: for quarterly flows at an 8% nominal annual rate, pass `0.08 / 4`. ``` npv([[-20000; 1]; [500; 4]; [1000; 4]; [2000; 4]; [3000; 4]]; 0.08 / 4) => 928.4409 ``` `irr` and `mirr` **return** a rate on the same basis — per period. Multiply by the number of periods in a year to read it as a nominal annual rate. An `interest_rate` of `-1` or lower returns an out-of-range error. ### Net present value \[npv] ``` npv(table; interest_rate) ``` The value of the whole series at period 0: every later flow discounted back at `interest_rate`, plus the period-0 flow at face value. *Follows [Reading the cash flow table](#reading-the-cash-flow-table) and [Periodic rates](#periodic-rates).* ``` npv([-80000; 5000; 4500; 5500; 4000; 115000]; 0.105) => 4774.6328 npv([280 USD; 1492.56 USD; 380.11 USD; 125.25 USD; 100 USD; 56.78 USD; 21.34 USD]; 0.05) => 2297.1354 USD npv([[-50000; 1]; [5000; 3]; [10000; 4]; [0; 1]; [15000; 3]]; 0.09) => 6728.6266 ``` ### Net future value \[nfv] ``` nfv(table; interest_rate) ``` The value of the whole series at its **last period** — the net present value compounded forward over the periods the series runs for. *Follows [Reading the cash flow table](#reading-the-cash-flow-table) and [Periodic rates](#periodic-rates).* ``` nfv([-80000; 5000; 4500; 5500; 4000; 115000]; 0.105) => 7865.9533 nfv([[-50000; 1]; [5000; 3]; [10000; 4]; [0; 1]; [15000; 3]]; 0.09) => 17362.7257 ``` ### Net uniform series \[nus] ``` nus(table; interest_rate) nus(table; interest_rate; beginning_of_period) ``` The **level** flow with the same net present value as the uneven series — what the series is worth per period, and the figure to compare two series of different shapes. It repeats for the number of periods after period 0, at the end of each period by default; `beginning_of_period` of `true` places it at the start of each period instead. *Follows [Reading the cash flow table](#reading-the-cash-flow-table), [Periodic rates](#periodic-rates), and the `beginning_of_period` [convention](#conventions).* ``` nus([-80000; 5000; 4500; 5500; 4000; 115000]; 0.105) => 1275.6649 nus([280; 1492.56; 380.11; 125.25; 100; 56.78; 21.34]; 0.05) => 452.5758 nus([280; 1492.56; 380.11; 125.25; 100; 56.78; 21.34]; 0.05; true) => 431.0246 ``` ### Internal rate of return \[irr] ``` irr(table) ``` The rate **per period** at which the series' net present value is zero. A meaningful result requires the cash flows to change sign at least once; with no sign change, an out-of-range error is returned. When they change sign more than once, more than one rate can satisfy NPV = 0 and `irr` returns one of them. The same series always returns the same rate, but treat *which* root that is as unspecified: for a series that reverses sign more than once, prefer [`mirr`](#modified-internal-rate-of-return-mirr), which has a single solution by construction. *Follows [Reading the cash flow table](#reading-the-cash-flow-table) and [Periodic rates](#periodic-rates).* ``` irr([-80000; 5000; 4500; 5500; 4000; 115000]) => 0.1193 irr([-280 USD; 1492.56 USD; 380.11 USD; 125.25 USD; 100 USD; 56.78 USD; 21.34 USD]) => 4.59 irr([[-20000; 1]; [500; 4]; [1000; 4]; [2000; 4]; [3000; 4]]) => 0.0243 ``` The second series is quarterly, so its result is 2.43% per quarter — 9.72% nominal annual. ### Modified internal rate of return \[mirr] ``` mirr(table; interest_rate; risk_rate) ``` An internal rate of return with a separate rate for each side of the series: outflows discounted at the finance rate `interest_rate`, inflows compounded forward at the reinvestment rate `risk_rate`. Returns a rate per period. A series with no outflow returns an infinity error rather than a rate. *Follows [Reading the cash flow table](#reading-the-cash-flow-table) and [Periodic rates](#periodic-rates).* ``` mirr([-80000; 5000; 4500; 5500; 4000; 115000]; 0.105; 0.1054) => 0.1179 mirr([[-50000; 1]; [5000; 3]; [10000; 4]; [0; 1]; [15000; 3]]; 0.09; 0.1054) => 0.1095 ``` ### Payback period \[payback] ``` payback(table) ``` The number of periods from period 0 until the **cumulative, undiscounted** flows return to zero — how long an outlay takes to recover. No rate is passed; discounting plays no part. The result is fractional: within the period where the running total crosses zero it is interpolated linearly, so `4.53` puts the recovery a little past halfway through period 5. A series that opens with an inflow is measured the same way, reporting when the total falls back to zero. When the flows never return to zero, `payback` returns `0` — a `0` result means no payback, not immediate payback. *Follows [Reading the cash flow table](#reading-the-cash-flow-table).* ``` payback([-80000; 5000; 4500; 5500; 4000; 115000]) => 4.53 payback([[-50000; 1]; [5000; 3]; [10000; 4]; [0; 1]; [15000; 3]]) => 6.5 payback([-280; 1492.56; 380.11; 125.25; 100; 56.78; 21.34]) => 0.1876 ``` ### Profitability index \[profindex] ``` profindex(table; interest_rate) ``` The present value of the inflows divided by the present value of the outflows, both discounted at `interest_rate` — above `1` when the series returns more than it costs. A series with no outflow returns an infinity error rather than a number. *Follows [Reading the cash flow table](#reading-the-cash-flow-table) and [Periodic rates](#periodic-rates).* ``` profindex([-80000; 5000; 4500; 5500; 4000; 115000]; 0.105) => 1.06 profindex([[-50000; 1]; [5000; 3]; [10000; 4]; [0; 1]; [15000; 3]]; 0.09) => 1.1346 ``` # Function library Source: https://docs.truemath.ai/math/functions/index An index of TrueMath's built-in functions by category — arithmetic, trigonometry, statistics, finance, tables, and more. TrueMath provides a library of built-in functions you can call from any [activity](/concepts/activities) equation. Functions take arguments separated by semicolons and are case-insensitive: ``` round(monthly_payment; 2) pmt(loan_amount; 0; rate / 12; term * 12) ``` Many functions operate on a [table](/math/types/tables) as well as a single number, returning a table when given one. Many also accept [unit numbers](/math/types/unit-numbers), carrying units through the calculation; where a function requires a specific kind of unit — such as an angle for trigonometry — its page says so. ## Categories | Category | Functions | | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | [Arithmetic and powers](/math/functions/arithmetic-and-powers) | `+`, `-`, `*`, `/`, `^`, `mod`, `ch`, `sqrt`, `root`, `ln`, `log`, `alog`, `exp` | | [Trigonometry](/math/functions/trigonometry) | `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `sec`, `csc`, `cot`, and hyperbolic forms | | [Rounding and numeric](/math/functions/rounding-and-numeric) | `round`, `floor`, `ceil`, `abs`, `sign`, `ipart`, `fpart` | | [Unit functions](/math/functions/units) | `unit`, `unitvalue` | | [Statistics](/math/functions/statistics) | `sum`, `avg`, `min`, `max`, `median`, `stddev`, `variance`, `fact`, `perm`, `comb`, `rand`, and more | | [Finance](/math/functions/finance) | `pv`, `fv`, `pmt`, `rate`, `periods`, `amortization`, `npv`, `irr`, `effrate`, `nomrate`, and more | | [Dates](/math/functions/dates) | `date`, `year`, `month`, `day`, `hour`, `minute`, `second`, `weekday`, `isleapyear`, `adjdate`, `ddays` | | [Table functions](/math/functions/tables) | `length`, `width`, `item`, `row`, `column`, `subset`, `lookup`, `vlookup`, `rlookup`, `sort`, `append` | | [Conditionals and logic](/math/functions/conditionals-and-logic) | `if`, `choose`, `loop` | For operators and the order of operations, see [Operators and precedence](/math/functions/operators-and-precedence). # Operators and precedence Source: https://docs.truemath.ai/math/functions/operators-and-precedence TrueMath's arithmetic, comparison, logical, and assignment operators, and the order of operations used to evaluate them. TrueMath equations use the following operators. See [Writing equations](/authoring/writing-equations) for the surrounding grammar. ## Operators | Category | Operators | Notes | | ---------- | --------------------------- | ------------------------------------------------- | | Arithmetic | `+` `-` `*` `/` `^` | `^` is exponentiation; `1 / x` is the reciprocal. | | Comparison | `==` `!=` `<` `>` `<=` `>=` | Return `1` (true) or `0` (false). | | Logical | `&&` `\|\|` `!` | AND, OR, NOT. | | Assignment | `=` | Binds a value to a variable. | | Grouping | `( )` | Overrides the order of operations. | ## Order of operations TrueMath evaluates an expression in this order, from **highest precedence to lowest**: 1. Exponentiation (`^`) 2. Negation (`-x`) 3. Multiplication and division (`*`, `/`) 4. Addition and subtraction (`+`, `-`) 5. Comparison (`>`, `<`, `>=`, `<=`, `==`, `!=`) 6. Logical (`&&`, `\|\|`, `!`) 7. Assignment (`=`) — the lowest precedence, so the entire right-hand side is evaluated before it is assigned (`x = a + b * c` computes `a + b * c`, then assigns). Functions like [`sqrt`](/math/functions/arithmetic-and-powers) or [`pmt`](/math/functions/finance) aren't operators: a function call evaluates its arguments and resolves to a single value before any surrounding operator applies. ## Operators and units Every operator works on [unit numbers](/math/types/unit-numbers) as readily as on plain numbers. Comparison converts compatible units before comparing, so a value in metres and the same value in centimetres are equal; comparing across dimensions is rejected rather than answered: ``` 5m == 500cm => true 3m < 5m => true ``` `3m < 5kg` returns `input.incompatible_units` — there is no true or false for it. Arithmetic follows the same rules, deriving a dimension where one applies: see [Conversion behavior](/math/units#conversion-behavior) and [Combining types](/authoring/writing-equations#combining-types). ``` 3 + 4 * 5 => 23 (3 + 4) * 5 => 35 -2^2 => -4 ``` Because the exponent binds tighter than the leading minus, `-2^2` is `-(2^2) = -4`. Use parentheses whenever the intended grouping isn't obvious — TrueMath evaluates `(a + b) / c` exactly as written. `=` is **assignment**; `==` is **comparison**. Use `==` inside conditions: `if(term == 30; rate_30yr; rate_15yr)`. See [Conditionals and logic](/math/functions/conditionals-and-logic). ## Boolean results Comparison and logical operators produce `1` (true) or `0` (false), and a condition expects a [boolean](/math/types/scalar-numbers#booleans) — `true`/`false` or `1`/`0`. The condition must be a scalar. See [Booleans](/math/types/scalar-numbers#booleans) for how `true`/`false` relate to `1`/`0` and how other values are evaluated. # Rounding and numeric Source: https://docs.truemath.ai/math/functions/rounding-and-numeric Rounding, truncation, absolute value, sign, and integer/fractional-part functions. These functions adjust or inspect a number's value. Each accepts a number or a [table](/math/types/tables), returning the same shape, and a [unit](/math/units) on the value is carried through to the result. [`sign`](#sign-sign) is the exception: it reports a direction, so it returns a bare number. **Round only when you mean to change the value.** TrueMath carries values at [full precision](/concepts/units-and-precision) through every step. `round`, `floor`, and `ceil` permanently discard that precision, and a rounded value used in a later calculation introduces error that compounds across steps. They also change the value an [activity](/concepts/activities) returns, which can make it impossible to solve in reverse. To show fewer decimals without losing precision, set the [variable's](/concepts/variables) format instead of rounding. ## Round \[round] ``` round(value) round(value; places) round(date; "unit") ``` Rounds to `places` following standard rounding (digits 0–4 round down, 5–9 round up). Set `places` to `1`–`10` to round to that decimal place, or `0` to `-10` to round to a whole-number place; it defaults to `0`. `places` must be a whole number in the range -10 to 10 (real numbers are truncated); outside that range returns a `Parameter out of range` error. Given a [date](/math/types/dates) and a quoted time unit, `round` snaps to the nearest boundary of that unit instead — see [Snapping a date to a boundary](#snapping-a-date-to-a-boundary). ``` round(1234.56) => 1235 round(1234.56; 1) => 1234.6 round(1234.56; -2) => 1200 round(5.567m; 1) => 5.6 m round(1234.56USD; -2) => 1200 USD ``` ## Floor \[floor] ``` floor(value) floor(value; places) floor(date; "unit") ``` The largest value less than or equal to `value` at the given `places`. `places` follows the same rules as [`round`](#round-round). Given a [date](/math/types/dates) and a quoted time unit, `floor` snaps back to the boundary at or before the date — see [Snapping a date to a boundary](#snapping-a-date-to-a-boundary). ``` floor(23.456) => 23 floor(23.567) => 23 floor(-23.456) => -24 floor(-23.567) => -24 floor(1234.56; 1) => 1234.5 floor(1234.56; -2) => 1200 floor(5.9m) => 5 m ``` ## Ceiling \[ceil] ``` ceil(value) ceil(value; places) ceil(date; "unit") ``` The smallest value greater than or equal to `value` at the given `places`. `places` follows the same rules as [`round`](#round-round). Given a [date](/math/types/dates) and a quoted time unit, `ceil` snaps forward to the boundary at or after the date — see [Snapping a date to a boundary](#snapping-a-date-to-a-boundary). ``` ceil(23.456) => 24 ceil(23.567) => 24 ceil(-23.456) => -23 ceil(-23.567) => -23 ceil(1234.56; 1) => 1234.6 ceil(1234.56; -2) => 1300 ceil(5.1m) => 6 m ``` ## Snapping a date to a boundary When the first argument is a [date](/math/types/dates), all three functions accept a **quoted time unit** in place of `places`, which snaps the date to that unit's boundary — the start of the day, the first of the month, the nearest hour. ``` floor(date; "unit") ceil(date; "unit") round(date; "unit") ``` A boundary is the instant a period **starts** — the year boundaries are January 1 at midnight, the month boundaries are the 1st at midnight, the day boundaries are midnight. Snapping zeros every component finer than the unit named. Six units are accepted, one per calendar component: | `unit` | Boundary | `floor(date(2026; 7; 21; 9; 30; 45); "unit")` | | ------- | ------------------- | --------------------------------------------- | | `"s"` | whole second | 2026-07-21 09:30:45 | | `"min"` | start of the minute | 2026-07-21 09:30:00 | | `"hr"` | start of the hour | 2026-07-21 09:00:00 | | `"d"` | start of the day | 2026-07-21 00:00:00 | | `"mo"` | first of the month | 2026-07-01 00:00:00 | | `"yr"` | January 1 | 2026-01-01 00:00:00 | Those six are the whole set, because each names a calendar component that can be zeroed. `"wk"` is not among them — a week is not a component of a date — and a unit from another dimension, such as `"m"` or `"lb"`, returns `input.incompatible_units`. Because boundaries are period starts, `ceil` moves **forward to the start of the next period**, not to the last day of the current one: `ceil(d; "yr")` on any date in 2026 is January 1, 2027, and `ceil(d; "mo")` on July 21 is August 1. December 31 is not a year boundary — it is the last day inside the year, which is a different thing. To get the last day of a period, take the next boundary and step back a day with [`adjdate`](/math/functions/dates#calendar-adjustment-adjdate): ``` adjdate(ceil(date(2026; 7; 21; 9; 30); "mo"); -1) => date(2026; 7; 31) adjdate(ceil(date(2026; 7; 21; 9; 30); "yr"); -1) => date(2026; 12; 31) ``` A date already exactly on a boundary is returned unchanged by both `floor` and `ceil`, so applied to `date(2026; 8; 1)` that idiom gives July 31 — the previous period's last day. Snap or offset first if the input might land on a boundary. ``` floor(date(2026; 7; 21; 9; 30); "d") => date(2026; 7; 21) ceil(date(2026; 7; 21; 9; 30); "d") => date(2026; 7; 22) floor(date(2026; 7; 21; 9; 30); "mo") => date(2026; 7; 1) ceil(date(2026; 7; 21; 9; 30); "yr") => date(2027; 1; 1) round(date(2026; 7; 21; 9; 30); "d") => date(2026; 7; 21) round(date(2026; 7; 21; 13; 30); "d") => date(2026; 7; 22) ``` With no second argument at all, `floor` and `ceil` still act on the number, and for a date that means the whole second — `floor(d)` and `floor(d; "s")` agree. Snapping is also how dates are compared at a coarser granularity than the exact instant. Two dates are equal only when their values match to the second, so snap both sides to the same unit and compare the results: ``` floor(date(2026; 7; 21; 9; 30); "d") == floor(date(2026; 7; 21; 16; 45); "d") => true floor(date(2026; 7; 21; 9; 30); "mo") == floor(date(2026; 7; 28; 3; 15); "mo") => true ``` ## Absolute value \[abs] ``` abs(value) ``` The absolute (non-negative) value. ``` abs(34) => 34 abs(-34) => 34 abs([25; -34]) => [25; 34] abs(-3kg) => 3 kg ``` ## Sign \[sign] ``` sign(value) ``` `-1` if `value` is negative, `0` if zero, `1` if positive. The result is a bare number even when `value` carries a unit — it is a direction, not a quantity. ``` sign(34.567) => 1 sign(-34.567) => -1 sign(0) => 0 sign([25; -34; 0; 15]) => [1; -1; 0; 1] sign(-5USD) => -1 ``` ## Integer part \[ipart] ``` ipart(value) ``` The integer (whole-number) portion of `value`. ``` ipart(34.567) => 34 ipart([10.55; 8.7; -4.1; 3.25]) => [10; 8; -4; 3] ipart(5.75m) => 5 m ``` ## Fractional part \[fpart] ``` fpart(value) ``` The fractional (decimal) portion of `value`. ``` fpart(34.567) => 0.567 fpart([10.55; 8.7; -4.1; 3.25]) => [0.55; 0.7; -0.1; 0.25] fpart(5.75m) => 0.75 m ``` # Statistics Source: https://docs.truemath.ai/math/functions/statistics Aggregation, dispersion, and combinatorics functions: sum, average, min/max, median, quartiles, standard deviation, variance, factorial, permutations, and combinations. Statistics functions operate over a [table](/math/types/tables). ## Reading the table Most take a single-column table, or a multi-column table plus a `column` index to operate on. The first column is index `1`, and when `column` is omitted it defaults to `1`; a `column` out of range returns an `Invalid array dimensions` error. ### Units in a column Cells may carry [units](/math/units). Compatible units convert before the calculation; mixing dimensions — a length with a mass — returns `input.incompatible_units`. Where a result is a quantity, it keeps the unit: [`sum`](#sum-sum) and [`avg`](#average-avg-mean-average) report in the **first** cell's unit, while [`min`](#minimum-min-minimum) and [`max`](#maximum-max-maximum) report the winning cell as it was written. ``` sum([1m; 100cm]) => 2 m average([1m; 2m; 3m]) => 2 m min([5m; 3ft]) => 3 ft max([1m; 50cm; 2ft]) => 1 m stddev([1m; 2m; 3m]) => 1 m ``` Three results are a different dimension than the input, because of what they measure: [`variance`](#variance-variance-variancep), [`product`](#product-product), and [`sum2`](#sum-of-squares-sum2) are squared or multiplied quantities, and [`count`](#count-count) is always a bare number. ``` variance([1m; 2m; 3m]) => 1 m^2 product([2m; 3m]) => 6 m^2 count([1m; 2m]) => 2 ``` ## Aggregation *These read a table column — see [Reading the table](#reading-the-table).* ### Count \[count] ``` count(table) ``` The number of rows in `table`. ``` count([2; 3; 4]) => 3 ``` ### Sum \[sum] ``` sum(table) sum(table; column) ``` The sum of the first column of `table`, or of the given `column`. ``` sum([3; 4; 5]) => 12 sum([[3;10]; [4;20]; [5;30]]; 2) => 60 ``` ### Average \[avg, mean, average] ``` avg(table) avg(table; column) ``` The average of the values. `mean` and `average` are aliases. ``` avg([3; 4; 5]) => 4 avg([[3;10]; [4;20]; [5;30]]; 2) => 20 ``` ### Product \[product] ``` product(list) ``` The product of the values in a one-dimensional `list`. ## Range and extremes *These read a table column — see [Reading the table](#reading-the-table).* ### Minimum \[min, minimum] ``` min(table) min(table; column) min(valueA; valueB; ...) ``` The minimum value. Given several equal-length single-column tables, returns the row-wise minimum as a table. ``` min([3; 4; 5; 9]) => 3 min(3; 4; 5; 9) => 3 min([3;50]; [4;25]; [5;40]; [9;10]) => [3; 4; 5; 9] ``` ### Maximum \[max, maximum] ``` max(table) max(table; column) max(valueA; valueB; ...) ``` The maximum value, with the same forms as `min`. ``` max([3; 4; 5; 9]) => 9 max([[3;50]; [4;25]; [5;40]; [9;10]]; 2) => 50 ``` ### Range \[range] ``` range(table) range(table; column) ``` The difference between the maximum and minimum. ``` range([3; 4; 5; 9]) => 6 ``` ## Dispersion *These read a table column — see [Reading the table](#reading-the-table).* ### Standard deviation \[stddev, stddevp] ``` stddev(table) stddevp(table) stddev(table; column) stddevp(table; column) ``` `stddev` is the **sample** standard deviation; `stddevp` is the **population** standard deviation. ``` stddev([3; 4; 5; 6; 7; 8]) => 1.8708 stddevp([3; 4; 5; 6; 7; 8]) => 1.7078 ``` ### Variance \[variance, variancep] ``` variance(table) variancep(table) variance(table; column) variancep(table; column) ``` `variance` is the **sample** variance; `variancep` is the **population** variance. ``` variance([3; 4; 5; 6; 7; 8]) => 3.5 variancep([3; 4; 5; 6; 7; 8]) => 2.9167 ``` ### Median and quartiles \[median, quartile1, quartile3] ``` median(table) median(table; column) quartile1(table) quartile1(table; column) quartile3(table) quartile3(table; column) ``` The median, first quartile, and third quartile. ``` median([3; 4; 5; 5; 6]) => 5 quartile1([3; 4; 5; 6; 7; 8]) => 4 quartile3([3; 4; 5; 6; 7; 8]) => 7 ``` ## Sums of products *These read a table column — see [Reading the table](#reading-the-table).* ### Sum of products \[sumofproducts] ``` sumofproducts(table) sumofproducts(table; resultsAsTable) ``` Multiplies the items within each row, then sums those products. This is the operation for a weighted sum or dot product — a table of `(weight; value)` rows totals to `sumofproducts(table)`, where [`sum`](#sum-sum) reads only the first column. With `resultsAsTable` set to `true`, returns the per-row products as a table instead. ``` sumofproducts([[3;4]; [5;6]; [7;8]]) => 98 sumofproducts([[3;4]; [5;6]; [7;8]]; true) => [12; 30; 56] ``` ### Sum of squares \[sum2] ``` sum2(table) sum2(table; column) ``` The sum of the squares of the values. ``` sum2([3; 4; 5; 6; 7; 8]) => 199 ``` ### Sum XY \[sumxy] ``` sumxy(table; x_column; y_column) ``` The sum of each row's `x_column` value multiplied by its `y_column` value. ``` sumxy([[3;10]; [4;20]; [5;20]; [6;30]; [7;40]; [8;50]]; 1; 2) => 1070 ``` ## Probability *These take plain numbers. An argument carrying a unit returns `input.incompatible_type`.* ### Factorial \[fact] ``` fact(value) ``` The factorial of `value`, a whole number from 1 to 170. Values outside that range return a `Parameter out of range` error. ``` fact(5) => 120 fact([2; 3; 4]) => [2; 6; 24] ``` ### Permutations \[perm] ``` perm(n; r) ``` The number of permutations of `n` taken `r` at a time (order matters). `n` and `r` must be whole numbers with `n` ≥ `r` > 0. ``` perm(8; 3) => 336 perm([8;7;6]; 3) => [336; 210; 120] ``` ### Combinations \[comb] ``` comb(n; r) ``` The number of combinations of `n` taken `r` at a time (order does not matter). `n` and `r` must be whole numbers with `n` ≥ `r` > 0. ``` comb(8; 3) => 56 comb([8;7;6]; 3) => [56; 35; 20] ``` ### Random \[rand] ``` rand() rand(length) rand(lower; upper) rand(lower; upper; length) ``` A random real number between 0 and 1 inclusive, or a random whole number between `lower` and `upper` inclusive. With `length`, returns a table of that many random values. # Table functions Source: https://docs.truemath.ai/math/functions/tables Functions for measuring, accessing, searching, sorting, and combining table data. These functions read and reshape [tables](/math/types/tables). Rows and columns are **1-indexed** meaning the first row and first column are index `1`. ``` [3; 4; 5] # a one-dimensional table (3 rows) [[3; 100]; [4; 200]; [5; 300]] # a two-dimensional table (3 rows, 2 columns) ``` Every cell carries its own [unit](/math/units), so a table can hold a column of lengths beside a column of currency, and a value read out of one comes back with the unit it was stored with. Functions that report a **position or a size** — [`length`](#length-length), [`width`](#width-width), [`lookup`](#lookup-lookup) — return a bare number whatever the cells carry. Element-wise arithmetic converts compatible units and rejects incompatible ones, cell by cell. ``` item([1m; 2m]; 2) => 2 m length([1m; 2m]) => 2 lookup([1m; 2m; 3m]; 2m) => 2 [1m; 2m] * 2 => [2m; 4m] [1m; 2m] + [1ft; 2ft] => [1.3048m; 2.6096m] ``` ## Size ### Length \[length] ``` length(table) ``` The number of rows. ``` length([3; 4; 5]) => 3 length([[3;10]; [4;20]; [5;30]]) => 3 length([]) => 0 ``` ### Width \[width] ``` width(table) ``` The number of columns. ``` width([3; 4; 5]) => 1 width([[3;10]; [4;20]; [5;30]]) => 2 width([]) => 0 ``` ## Access ### Item \[item] ``` item(table; index) item(table; row; column) ``` A single value: from the first column at `index`, or at the intersection of `row` and `column`. An index outside the table returns an `Invalid dimensions` error. ``` item([3; 4; 5]; 3) => 5 item([[3;10]; [4;20]; [5;30]]; 3; 2) => 30 item([[1m; 2s]; [3m; 4s]]; 2; 1) => 3 m ``` ### Row \[row, rows] ``` row(table; index) rows(table; first; last) ``` A single row at `index`, or the rows from `first` to `last` inclusive. An index out of range returns an `Invalid dimensions` error; `last` less than `first` returns a `Parameter out of range` error. ``` row([[3;10;100]; [4;20;200]; [5;30;300]]; 1) => [3; 10; 100] rows([[3;10;100]; [4;20;200]; [5;30;300]]; 1; 2) => [[3;10;100]; [4;20;200]] ``` ### Column \[column, columns] ``` column(table; index) columns(table; first; last) ``` A single column at `index`, or the columns from `first` to `last` inclusive. Bounds behave as for `row`. ``` column([[3;10;100]; [4;20;200]; [5;30;300]]; 1) => [3; 4; 5] columns([[3;10;100]; [4;20;200]; [5;30;300]]; 1; 2) => [[3;10]; [4;20]; [5;30]] ``` ### Subset \[subset] ``` subset(table; firstRow; lastRow; firstColumn; lastColumn) ``` The rectangular region between the given rows and columns, inclusive. ``` subset([[3;10;100]; [4;20;200]; [5;30;300]]; 2; 3; 1; 2) => [[4;20]; [5;30]] ``` ## Search `lookup`, `vlookup`, and `rlookup` all search one column of a table for `value` and then return something from the **first matching row** — its position, a value from another column, or the whole row. ### Common parameters All three share the same three arguments: * **`value`** — the value to search for. * **the searched column** — which column to look in. This is the `column` argument (called `inputColumn` in `vlookup`). When it is omitted, the **first column** is searched. * **`method`** — how the match is made: `0` (the default) finds an exact match, `-1` finds the largest value ≤ `value`, and `1` finds the smallest value ≥ `value`. An exact-match search may be in any order; the `-1` and `1` methods require the searched column to be sorted in ascending order. The first row and column are index `1`. ### Lookup \[lookup] ``` lookup(table; value) lookup(table; value; column) lookup(table; value; column; method) ``` The **row index** (1-based) of the first row whose searched column matches `value`, or `0` when nothing matches — use it to find *where* a value is. `column` selects the column to search (the first column when omitted); `method` controls how the match is made. *Shares the `value`, searched-column, and `method` arguments — [details](#common-parameters).* ``` lookup([[1;50]; [5;45]; [30;40]]; 30) => 3 lookup([[1;50]; [5;45]; [30;40]]; 45; 2) => 2 lookup([[1;50]; [5;45]; [30;40]]; 1; 1; 0) => 1 lookup([[1;50]; [5;45]; [30;40]]; 20; 1; -1) => 2 lookup([[1;50]; [5;45]; [30;40]]; 20; 1; 1) => 3 ``` ### Value lookup \[vlookup] ``` vlookup(table; value; returnColumn) vlookup(table; value; inputColumn; returnColumn) vlookup(table; value; inputColumn; returnColumn; method) ``` The **value in `returnColumn`** from the first matching row — a spreadsheet-style lookup (find a row by one column, read a cell from another). With three arguments, `value` is searched in the first column; with four or more, `inputColumn` is the column searched and `returnColumn` the column read from. `method` controls how the match is made. No match yields a `Parameter out of range` error. *Shares the `value`, searched-column, and `method` arguments — [details](#common-parameters).* ``` vlookup([[1;50]; [5;45]; [30;40]]; 30; 2) => 40 vlookup([[1;50]; [5;45]; [30;40]]; 5; 1; 2) => 45 vlookup([[1;50]; [5;45]; [30;40]]; 5; 1; 2; 0) => 45 vlookup([[1;50]; [5;45]; [30;40]]; 20; 1; 2; -1) => 45 vlookup([[1;50]; [5;45]; [30;40]]; 20; 1; 2; 1) => 40 vlookup([[1m; 10USD]; [2m; 20USD]]; 2m; 2) => 20 USD ``` ### Row lookup \[rlookup] ``` rlookup(table; value) rlookup(table; value; column) rlookup(table; value; column; method) ``` The **entire matching row**, as a table — use it when you need several values from the matched row at once, rather than its position (`lookup`) or one cell (`vlookup`). `value` is the value to find; `column` selects the column to search (the first column when omitted); `method` controls how the match is made. No match yields a `Parameter out of range` error. *Shares the `value`, searched-column, and `method` arguments — [details](#common-parameters).* ``` rlookup([[1;50]; [5;45]; [30;40]]; 30) => [30; 40] rlookup([[1;50]; [5;45]; [30;40]]; 45; 2) => [5; 45] rlookup([[1;50]; [5;45]; [30;40]]; 5; 1; 0) => [5; 45] rlookup([[1;50]; [5;45]; [30;40]]; 20; 1; -1) => [5; 45] rlookup([[1;50]; [5;45]; [30;40]]; 20; 1; 1) => [30; 40] ``` ## Reshape ### Sort \[sort] ``` sort(table; columns) sort(table; columns; descending) ``` Sorts the rows by `columns`, ascending by default. Pass `true` for `descending`. All columns move together. `columns` is either a single column number or an array of column numbers, each 1-based. An array lists sort keys in precedence order — the first column is the primary key, the next breaks ties among rows equal on the first, and so on; rows equal on every listed column keep their original relative order. A single number is equivalent to a one-element array — `1` and `[1]` behave the same. `descending` sets one direction for the whole sort; per-column directions are not supported. A column number below `1` or greater than the table's column count — passed directly or inside the array — returns `Invalid dimensions`. ``` sort([5; 2; 3; 7]; 1) => [2; 3; 5; 7] sort([5; 2; 3; 7]; 1; true) => [7; 5; 3; 2] sort([[5;35]; [2;20]; [3;80]; [7;40]]; 2) => [[2;20]; [5;35]; [7;40]; [3;80]] ``` With an array, ties on the primary key fall through to the next column. Here the primary key is column 2, then 3, then 1, then 4: ``` sort([[9;3;16;1]; [9;6;16;1]; [4;3;16;7]; [9;3;5;1]]; [2;3;1;4]) => [[9;3;5;1]; [4;3;16;7]; [9;3;16;1]; [9;6;16;1]] sort([[9;3;16;1]; [9;6;16;1]; [4;3;16;7]; [9;3;5;1]]; [2;3;1;4]; true) => [[9;6;16;1]; [9;3;16;1]; [4;3;16;7]; [9;3;5;1]] ``` ### Append \[append] ``` append(table1; table2) append(table1; table2; asColumns) ``` Joins `table2` onto `table1` as new rows by default, or as new columns when `asColumns` is `true`. Appended tables must have matching dimensions on the joining edge, or `Invalid dimensions` is returned. Appending does not convert units — each cell keeps the unit it arrived with. ``` append([3; 4; 5]; [10; 20; 30]) => [3; 4; 5; 10; 20; 30] append([3; 4; 5]; [10; 20; 30]; true) => [[3;10]; [4;20]; [5;30]] append([1m]; [2ft]) => [1m; 2ft] ``` # Trigonometry Source: https://docs.truemath.ai/math/functions/trigonometry Trigonometric, inverse, and hyperbolic functions, including how angle units are interpreted. These functions take an angle and return a ratio, or take a ratio and return an angle. Each accepts a number or a [table](/math/types/tables), returning the same shape. ## Angle units A bare number is interpreted as **degrees**. Attach an angle [unit](/math/units) to be explicit: * `value` — degrees (the default) * `value deg` — degrees * `value rad` — radians The inverse functions return their result in **degrees**. To read one in radians, [cast](/math/units#casting-to-a-specific-unit) the result — `(asin(0.5))rad`. A unit on the *argument* of an inverse function is an error, since the argument is a dimensionless ratio. ## Sine, cosine, and tangent \[sin, cos, tan] ``` sin(value) cos(value) tan(value) ``` The sine, cosine, and tangent of an angle. *Angles use the shared [angle units](#angle-units) — degrees unless you append `rad`.* ``` sin(45deg) => 0.7071 sin((pi/4)rad) => 0.7071 cos(60) => 0.5 tan(45) => 1 ``` ## Secant, cosecant, and cotangent \[sec, csc, cot] ``` sec(value) csc(value) cot(value) ``` The secant, cosecant, and cotangent of an angle — the reciprocals of cosine, sine, and tangent. *Angles use the shared [angle units](#angle-units) — degrees unless you append `rad`.* ``` sec(60) => 2 csc(30) => 2 cot(45) => 1 ``` ## Inverse functions \[asin, acos, atan] ``` asin(value) acos(value) atan(value) ``` The arc-sine, arc-cosine, and arc-tangent. `asin` and `acos` take a dimensionless ratio between -1 and 1; `atan` takes any dimensionless ratio. All three return an angle in degrees. *Angles use the shared [angle units](#angle-units) — degrees unless you append `rad`.* ``` asin(0.5) => 30 deg acos(1) => 0 deg atan(1) => 45 deg (asin(0.5))rad => 0.5236 rad ``` ## Hyperbolic functions \[sinh, cosh, tanh, asinh, acosh, atanh] ``` sinh(value) cosh(value) tanh(value) asinh(value) acosh(value) atanh(value) ``` The hyperbolic sine, cosine, and tangent, and their inverses. **These read their argument as an angle too**, on the same [angle units](#angle-units) as the circular functions — so `sinh(1)` is the hyperbolic sine of one *degree*. Write `sinh(1rad)` for the value usually written as sinh(1). The inverses return degrees, so cast the result to read it in radians. ``` sinh(1rad) => 1.1752 cosh(1rad) => 1.5431 tanh(1rad) => 0.7616 sinh(1) => 0.0175 (asinh(1.1752))rad => 1 rad ``` Because angle units are unambiguous, `sin(45 deg)` and `sin((pi/4) rad)` refer to the same angle. See [Units](/math/units). # Unit functions Source: https://docs.truemath.ai/math/functions/units Read a value's unit with unit, and extract its numeric portion — optionally converted to a target unit — with unitvalue. These functions inspect the unit of a [unit number](/math/types/unit-numbers) and separate it from the numeric value. Each accepts a single value or a [table](/math/types/tables), returning the same shape. ## Unit \[unit] ``` unit(value) ``` Returns `value`'s unit as text, or an empty string when the value is unitless. Given a [table](/math/types/tables), returns a table of unit strings, one per cell. `unit` takes exactly one argument. ``` unit(34) => "" unit(34 ft) => "ft" unit([34ft; 15; 25 in]) => ["ft"; ""; "in"] ``` The unit comes back in [canonical form](/math/units), not as it was written: `unit(34')` returns `"ft"`, and squared units use a superscript — `unit(2 m^2)` returns `"m²"` and `unit(34 USD/ft^2)` returns `"USD/ft²"`. ## Unit value \[unitvalue] ``` unitvalue(value) unitvalue(value; unit) ``` Returns the numeric portion of `value` with its unit stripped. When `unit` is given, `value` is first converted to that unit and then the numeric portion is returned. Given a [table](/math/types/tables), each cell is converted and stripped, returning a table of numbers. ``` unitvalue(34 ft) => 34 unitvalue(34) => 34 unitvalue(4ft; "in") => 48 unitvalue([4ft; 2ft; 3ft]; "in") => [48; 24; 36] ``` `unit` must be a **quoted** unit string compatible with `value`'s dimension. A bare token (`unitvalue(4ft; in)`) is read as a variable, not a unit. A unit that doesn't match the value's dimension — including applying a unit to a unitless value (`unitvalue(34; "in")`) — returns an incompatible-units error. In a table, an incompatible cell carries the error in place while the other cells still convert. `unitvalue` returns a plain number with no unit attached. To convert a value but keep it a unit number, [cast](/math/units#casting-to-a-specific-unit) it instead — `(4ft) in` is `48in`, whereas `unitvalue(4ft; "in")` is the bare number `48`. ## When to strip a unit Keep units wherever you can. Dividing them out by hand discards the [dimensional safety](/introduction/guarantees#dimensional-safety) that stops a calculation from returning a confident but nonsensical result, and the unit itself is information the caller depends on. Reach for `unitvalue` only when a unit would otherwise make an operation meaningless — most often when a value is really a count that happens to carry a unit. This starts at variable design: model dimensional quantities as [unit-carrying values](/concepts/units-and-precision#model-quantities-as-unit-carrying-values), and you rarely need to strip at all. Scattered `unitvalue` and `unit` calls are usually a sign that a variable was declared as a bare number when it should have carried a unit. Some operations do genuinely need a scalar. Looping and totaling a yearly cost over a holding period are good examples. Here the period is acting as a count of years rather than a duration to carry forward: multiplying the amount by it directly would carry the period's unit into the product instead of giving a plain dollar figure and error, so you strip it to a number of years first: ``` total_cost = yearly_cost * unitvalue(holding_period; "yr") ``` Passing the explicit `"yr"` rather than a bare `unitvalue(holding_period)` makes the result independent of how the period was entered: whether the caller said months or years, it converts to years and returns that number. ### Strip for the equation, not for the domain Ideally, a stripped value is for use inside the equation that strips it only. The example above strips `holding_period` and multiplies it in the same expression — the bare number never outlives the line that needs it, and the result, `total_cost`, is still a [unit number](/math/types/unit-numbers) in currency. Storing a stripped scalar as a [variable](/concepts/variables) that *other* [activities](/concepts/activities) consume is where this goes wrong. The bare number has lost its dimension, so downstream it no longer combines with the dimensional values around it: multiply a payoff period that is now a plain `271.04` by a currency payment and there is nothing to validate the combination — you get an incompatible-units error, or worse, a confident answer that assumes a unit nobody can see. Either way the [dimensional safety](/introduction/guarantees#dimensional-safety) the strip discarded was exactly what would have caught the mistake. So when later activities use a value, keep it in its unit. If a downstream result is what you actually want, compute it with a function that returns it in units rather than reconstructing it from a stripped scalar — for a payoff total, total the payments directly instead of multiplying a unitless month count by a payment. Reserve `unitvalue` for the scalar a single equation genuinely needs, and let that scalar end at the equation that needed it. Stripping a unit is a deliberate, lossy step — like [rounding](/math/functions/rounding-and-numeric), it throws away something the engine was carrying for you. Strip only the value that genuinely needs to be unitless, at the point you use it, and leave the rest of the calculation in units. When you need a value in a different unit but still a [unit number](/math/types/unit-numbers), [cast](/math/units#casting-to-a-specific-unit) it instead. # Dates Source: https://docs.truemath.ai/math/types/dates How TrueMath represents a date — a time value counted in seconds from the Unix epoch — and the arithmetic, comparison, and calendar rules that follow from it. A **date** is not a separate value type in TrueMath. It is a [unit number](/math/types/unit-numbers) carrying a time unit: the count of seconds from `1970-01-01 00:00:00` — the Unix epoch — to that instant. The [date functions](/math/functions/dates) read that number as a point on the calendar. This is the spreadsheet model, where a date is a serial number and any number can be read as a date. What TrueMath adds is that the serial is a *time value* rather than a bare count, so a date combines with durations under the ordinary [unit rules](/math/units): `closing_date + 30 d` is a date because `30 d` is a quantity of time. ``` date(1970) => 0 s date(1970; 1; 1; 0; 0; 1) => 1 s date(1969; 12; 31; 23; 59; 59) => -1 s ``` ## What follows from the model * **Midnight on the epoch is `0`,** one day later is `86400`, and every instant before the epoch is negative. Dates before 1970 need no special handling. * **No hidden state.** A date carries no flags — nothing marks it as "has a time" or "is a month end." Two dates are equal exactly when their values are equal, and a function such as [`adjdate`](/math/functions/dates#calendar-adjustment-adjdate) works only from the value it is given. * **Wall clock, no zones.** A date is a plain wall-clock reading — the components *are* the value. Nothing in a calculation shifts a date between time zones or applies daylight-saving rules. * **Nothing in the math reads a clock.** Date functions are pure: the same arguments always give the same result, which is part of what makes a result [reproducible](/introduction/guarantees). A clock enters through the *values* a calculation is given — a value or default written as `today` or `now` becomes a number before the calculation runs, and two of them in the same calculation mean the same moment. `today` is midnight of the current day and `now` carries the time of day as well, read in the time zone the request supplies — the browser's in the [Playground](/playground/tour), the `time_zone` parameter over the API — and otherwise in the account's time-zone setting. See [Time zones](/api/calculate#time-zones). Only that number is used in the calculation. * **Sub-second precision** is held in the fractional part: `0.5` is half a second. The smallest named time unit is the millisecond (`ms`), and there is no millisecond accessor — read it from the fraction of `second(d)` with [`fpart`](/math/functions/rounding-and-numeric#fractional-part-fpart). * **Text is read on the way in, not in the math.** A date you *supply* can be written the way people write dates — `7/21/2026`, `today` — and it becomes a number before the calculation runs. Inside an *equation* there is no parsing: [`date()`](/math/functions/dates#building-a-date-date) builds a date from numeric components, so an equation never reads `"7/21/2026"`. ## Supported range and calendar rules * **Supported range: `1900-01-01 00:00:00` through `2200-12-31 23:59:59.999…`** — every instant of 1900 through every instant of 2200, since the limit is on the year. [`date()`](/math/functions/dates#building-a-date-date) rejects a year outside it with `input.out_of_range`. A value that arithmetic carries past either end is not itself an error — it is still just a number — but reading it with a date function returns `math.out_of_range`. * **Gregorian calendar throughout.** A leap year is divisible by 4, except century years, except century years divisible by 400 — `2000` is a leap year and `1900` is not. * **A day that does not exist is rejected,** not rolled over: there is no `2026-02-29`. * **Every day is exactly 86,400 seconds.** Leap seconds are not modeled, matching Unix and spreadsheets, which keeps the conversion between a serial and its calendar components pure arithmetic. ## Arithmetic Because a date is a number carrying a time unit, date arithmetic is ordinary unit arithmetic. | Expression | Result | | ----------------- | ----------------------- | | date `+` duration | a date, shifted later | | date `-` duration | a date, shifted earlier | | date `-` date | a duration | A **duration** is any value carrying a time unit: `ms`, `s`, `min`, `hr`, `d`, `wk`, `mo`, or `yr`. The fixed-length units — `ms` through `wk` — are exact, so `date + 1 wk` always lands on the same wall-clock time seven days later. A `date - date` difference comes back in seconds, the base unit of time; [cast](/math/units#casting-to-a-specific-unit) it to report the span in another unit. ``` date(2026; 7; 21) + 10 d => date(2026; 7; 31) date(2026; 7; 21) - 3 d => date(2026; 7; 18) date(2026; 8; 4) - date(2026; 7; 21) => 1209600 s (date(2026; 8; 4) - date(2026; 7; 21)) d => 14 d (date(2026; 8; 4) - date(2026; 7; 21)) wk => 2 wk (date(2026; 8; 4) - date(2026; 7; 21)) hr => 336 hr ``` The third line is the raw difference — seconds, because that is what both dates are counted in. The three after it are the same span [cast](/math/units#casting-to-a-specific-unit) to the unit you want to report in, which is how an elapsed span is reported in whichever unit the result should use. ### Durations The difference between two dates is a duration, and a duration is an ordinary [unit number](/math/types/unit-numbers). Durations add and subtract, scale by a plain number, and convert on cast: ``` 2hr + 30min => 2.5 hr (2hr + 30min) min => 150 min 3d + 12hr => 3.5 d (1wk - 2d) d => 5 d 8hr * 5 => 40 hr ``` Multiplied by a rate, the time cancels and leaves what you were pricing — the arithmetic behind a timesheet, a carrying cost, or a rental: ``` 40hr * 85USD/hr => 3400 USD 2500USD / (40hr) => 62.5 USD/hr ``` To split a duration into whole units and a remainder, cast it and take the [integer part](/math/functions/rounding-and-numeric#integer-part-ipart), then read the leftover with [`mod`](/math/functions/arithmetic-and-powers#modulo-mod): ``` ipart((9000s) hr) => 2 hr mod(9000s; 1 hr) min # => 30 min ``` ### `mo` and `yr` shift by an average, not a calendar step Adding `1 mo` or `1 yr` to a date does **not** move it to the same day of the next month or year. `mo` and `yr` are fixed average durations — 30.4375 days and 365.25 days — so the result lands wherever that many seconds falls. Use [`adjdate`](/math/functions/dates#calendar-adjustment-adjdate) to step a date by calendar months or years. The averages are deliberate, and they are what makes duration math exact: `12 mo` is precisely `1 yr`, `18 mo` precisely `1.5 yr`, and a rate of `1200 USD/yr` precisely `100 USD/mo`. A conversion between time units always round-trips. But that consistency is the opposite of what a calendar does, where a month is 28 to 31 days and a year is 365 or 366. Used to step a date, an average misses in one of three ways: ``` date(2026; 1; 15) + 1 mo => date(2026; 2; 14; 10; 30) date(2026; 1; 31) + 1 mo => date(2026; 3; 2; 10; 30) date(2026; 7; 21) + 1 yr => date(2027; 7; 21; 6) ``` * **The day slips.** Thirty and a bit days from the 15th is the 14th of the next month, at 10:30 in the morning. * **A month can be skipped entirely.** January 31 plus `1 mo` lands in March, stepping straight over February. * **And the miss can be invisible.** A year later *is* the right calendar day — and six hours past midnight, because a quarter of a day is left over. Shown as a date alone it reads `2027-07-21` and looks correct, while the value is not equal to `date(2027; 7; 21)`. Comparisons against it fail, day boundaries fall in the wrong place, and nothing raises an error. The third case is the one to watch. A wrong value that looks right in a result stays wrong in every calculation that consumes it, and no error marks the spot. This is why [`adjdate`](/math/functions/dates#calendar-adjustment-adjdate) exists: it is the calendar-correct form of all three, and it preserves the time of day it started with. ``` adjdate(date(2026; 1; 15); 0; 1; 0) => date(2026; 2; 15) adjdate(date(2026; 1; 31); 0; 1; 0) => date(2026; 2; 28) adjdate(date(2026; 7; 21); 0; 0; 1) => date(2027; 7; 21) ``` The same trap appears wherever `yr` stands in for a calendar year rather than an increment. The average year is 365.25 days, which is the length of no particular year, so measuring against it drifts by a quarter or a half day depending on the year's position in the leap cycle. To count days within a year, count from the start of that year: ``` ddays(floor(report_date; "yr"); report_date) + 1 # the day of the year ``` [`floor`](/math/functions/rounding-and-numeric#snapping-a-date-to-a-boundary) gives 1 January of that year and [`ddays`](/math/functions/dates#day-counts-ddays) counts actual days to the date, so the result is right in every year, leap or not. The rule of thumb: `mo` and `yr` are for **durations** — spans, rates, terms — and never for moving a date, and never as a stand-in for a calendar year. See [Time units and dates](/math/units#time-units-and-dates). ### Measuring a span in months or years The averages apply in reverse too. A `date - date` difference is itself exact, since it is a count of seconds, but expressing that difference in `mo` or `yr` divides by an average — so a one-month span does not come back as `1`: ``` (date(2026; 2; 1) - date(2026; 1; 1)) d => 31 d (date(2026; 2; 1) - date(2026; 1; 1)) mo => 1.0185 mo ``` Report an elapsed span in the fixed units — `hr`, `d`, `wk` — where the figure is exact, or count days with [`ddays`](/math/functions/dates#day-counts-ddays), whose `basis` argument covers the 30/360 conventions finance uses for month fractions. For a count of whole calendar months, read the components: ``` (year(end_date) - year(start_date)) * 12 + month(end_date) - month(start_date) ``` ### A bare number is not a duration Adding or subtracting a plain [scalar](/math/types/scalar-numbers) is the one operation a date rejects: `date(2026; 7; 21) + 30` returns `input.incompatible_units`. Write `date(2026; 7; 21) + 30 d` and state the unit you mean. This is the ordinary [unit rule](/authoring/writing-equations#combining-types) — `5 m + 3` is rejected for the same reason — but it is worth calling out, because a spreadsheet would have taken the bare `30` as 30 days. Here the serial counts seconds, so a bare `30` could only mean half a minute. Requiring the unit removes the ambiguity rather than guessing at it. Beyond that guard, a date behaves like any other number. Operations with no natural calendar meaning are not blocked — dividing a date by `2`, or adding two dates together, produces a number, exactly as it would in a spreadsheet. Whether the result means anything is yours to decide. ## Comparison Dates compare like numbers, and the comparison is over the **whole instant**. A date at midnight is not equal to the same calendar day at nine-thirty: ``` date(2026; 7; 21) < date(2026; 12; 25) => true date(2026; 7; 21) == date(2026; 7; 21; 9; 30) => false ``` To compare at a coarser granularity, snap both sides to the same boundary first — this is the idiom for "same day?" and "same month?" tests: ``` floor(date(2026; 7; 21; 9; 30); "d") == date(2026; 7; 21) => true ``` See [Snapping a date to a boundary](/math/functions/rounding-and-numeric#snapping-a-date-to-a-boundary). ## Writing dates in equations The examples above are written in the reference style, with a bare unit suffix. Inside an [activity](/concepts/activities) equation a unit literal is wrapped in backticks so it reads as a unit rather than a variable name — `` closing_date + 30`d` ``. See [Units in equations](/authoring/writing-equations#units-in-equations). A date variable holds an ordinary numeric value, so it takes a default and a format like any other [variable](/concepts/variables). Giving a variable the `date`, `datetime`, `time`, or `duration` kind is what makes its number render as a calendar value, and how its format and default are set is covered in [Authoring dates and durations](/authoring/dates-and-durations). For how a date is written on input, see [Dates, times, and durations](/concepts/input-formats#dates-times-and-durations). See [Combining types](/authoring/writing-equations#combining-types) for how time values combine with the other types. # Scalar numbers Source: https://docs.truemath.ai/math/types/scalar-numbers Plain numeric values with no unit — the simplest value type in TrueMath. A **scalar number** is a plain numeric value with no unit attached: a count, a ratio, a rate, or any dimensionless quantity. ## Writing scalars ``` 42 3.14 -7.2 1.5e-3 # scientific notation: 0.0015 ``` ## Behavior * Scalars combine with other scalars under the usual arithmetic. * A scalar applied to a [table](/math/types/tables) operates on every cell: `[10; 20; 30] * 1.1`. * A scalar combined with a [unit number](/math/types/unit-numbers) scales it: `3 * 5m` is a length. Scalars are held at full precision; see [Units and precision](/concepts/units-and-precision). How a scalar is displayed — as a plain number, a percentage, or without separators — is controlled by the [variable's](/concepts/variables) kind, not by the value itself. See [Combining types](/authoring/writing-equations#combining-types) for how scalars combine with other types. ## Booleans TrueMath has no separate boolean type — `true` and `false` are named scalar constants equal to `1` and `0`. They are interchangeable with the numbers, so you can write whichever reads best: ``` true # 1 false # 0 ``` [Comparison and logical operators](/math/functions/operators-and-precedence) produce a boolean (the number `1` or `0`) and expect one in a condition. Boolean flag arguments — such as `descending` in [`sort`](/math/functions/tables) or `beginning_of_period` in the [finance functions](/math/functions/finance) — accept `true`/`false` or `1`/`0`. Equality is exact: because `true` is `1`, `1 == true` is true but `2 == true` is false. Inside `if`, `&&`, `||`, and `!`, the condition must reduce to a scalar. Comparing unit values is fine — `3 m < 5 m` yields the scalar `1` — but a bare unit value as the condition (`if(5 m; …)`) raises an error. A scalar counts as **true** when its absolute value is at least `0.5`, and **false** otherwise — roughly, "rounds to a nonzero number." So `if(2; …)` takes the true branch (even though `2 != true`), `if(0.4; …)` takes the false branch, and `if(-1; …)` is true. In normal use you pass `true`/`false`, `1`/`0`, or a comparison result, where this never comes up. # Tables Source: https://docs.truemath.ai/math/types/tables One- or two-dimensional collections of values with per-cell units. How tables are written in equations, stored and returned, and combined with element-wise operations. A **table** is a collection of values arranged in rows and columns — a single column (one-dimensional) or a two-dimensional grid. In software terms, a table is TrueMath's array type: a one-dimensional table is a list (or vector), and a two-dimensional table is a matrix. Tables hold series and matrices: a list of cash flows, an amortization schedule, a lookup grid. They are **1-indexed** (the first row and column are index `1`) — unlike arrays in most languages — and each **cell holds a value with its own optional unit**. Cells may carry different units, or none. The same table can appear in three places — written in an equation, returned by the API, or sent as input. The notation differs; the table is the same. ## In equations In an [activity](/concepts/activities) equation, a table is a bracketed literal with **semicolons** separating entries (commas are never used as they can be confused with decimal or thousands separators). A bare list is a single column — `[3; 4; 5]` is three rows of one column — and nesting a bracketed list adds columns: ``` [3; 4; 5] # 3 rows, 1 column [[3; 100]; [4; 200]] # 2 rows, 2 columns ``` Cells may carry units, may mix units, and may be left empty: ``` [3 ft; 4 ft; 5 ft] # every cell a length [3 ft; 4 USD] # mixed units may coexist in a table [1; ; 3] # the middle cell is empty ``` Mixed units are allowed to *coexist* in a table, but arithmetic across cells with incompatible dimensions still errors — see [Combining types](/authoring/writing-equations#combining-types). ### Element-wise operations * **With a scalar** — applies to every cell: `[1; 2; 3] * 2` → `[2; 4; 6]`. * **With another table** — element-wise; shapes must match: `[1; 2] + [3; 4]` → `[4; 6]`. Read and reshape tables with [table functions](/math/functions/tables) (`item`, `row`, `column`, `subset`, `lookup`, `sort`, `append`, `length`, `width`) or aggregate them with [statistics functions](/math/functions/statistics) (`sum`, `avg`, `stddev`). Some functions read a table in a particular shape. The [cash flow analysis](/math/functions/finance#cash-flow-analysis) functions are the notable case: they expect amounts in the first column and an optional frequency count in the second, with the first row taken as the flow at period 0 — see [reading the cash flow table](/math/functions/finance#reading-the-cash-flow-table). ## As a stored value When a table is returned by the API or held in a [scenario](/concepts/scenarios), it is represented as an **array of strings** — one per cell, each combining the value and its unit. Because each cell includes its own unit, a table variable's own `unit` is empty. ```json theme={null} ["1500 USD", "1600 USD", "1700 USD"] // a 1-D table [["1500 USD", "12"], ["1600 USD", "12"]] // a 2-D table ["3", "4", "5"] // cells without units ``` ## As input To provide a table as input, write it in **structured text** — semicolons separate cells, and brackets or new lines separate rows, with optional per-cell or outer units. See [structured text](/concepts/input-formats#tables). ## Display In a [domain](/concepts/domains), a table-valued [variable](/concepts/variables) can be displayed as a table or as a [bar or pie chart](/authoring/charts). # Unit numbers Source: https://docs.truemath.ai/math/types/unit-numbers Numeric values that carry a unit. TrueMath converts compatible units automatically and rejects incompatible ones. A **unit number** is a numeric value with a unit attached — `30 yr`, `5.2 m`, `375000 USD`, `60 mph`. The unit is part of the value, which lets TrueMath convert between compatible units and catch dimensional mistakes. ## Writing unit numbers Write the value followed by a unit suffix. The space is optional — `5m` and `5 m` are identical: ``` 5 m 3.5 kg 400000 USD 45 deg 12 m/s ``` Both simple and compound suffixes work. The suffix must be a recognized unit; see [Units](/math/units) for the full catalog. Writing a unit *inside an activity equation* has one extra wrinkle — a bare suffix can collide with a variable name — covered in [Writing equations](/authoring/writing-equations). ## Behavior * **Automatic conversion.** Compatible units convert when combined: `1m + 10cm` yields a length, with no manual conversion. * **Dimensional safety.** Incompatible combinations are rejected: `5m + 3kg` is an error. See [Combining types](/authoring/writing-equations#combining-types). * **Derived dimensions.** Multiplying and dividing units produces compound units — a distance over a time is a velocity; a cost over an area is a cost-per-area. ## Precision Like all values, unit numbers are carried at full precision through every step; rounding happens only at display. A value's unit and decimal display are governed by the [variable's](/concepts/variables) format. See [Units and precision](/concepts/units-and-precision). # Units Source: https://docs.truemath.ai/math/units The catalog of units TrueMath supports across length, area, volume, mass, time, angle, information, and currency, plus compound units and conversion behavior. TrueMath attaches units to values and converts between compatible units automatically. This page catalogs the supported units by dimension and explains how compound units and conversions behave. For how units are written inside activity equations, see [Writing equations](/authoring/writing-equations); for how unit numbers behave as a data type, see [Unit numbers](/math/types/unit-numbers). ## Simple units | Dimension | Units | | ----------- | -------------------------------------------------------------------------------------------------------------------- | | Length | `m`, `cm`, `mm`, `km`, `in` (`"`), `ft` (`'`), `yd`, `mi`, `nmi` (`NM`) | | Area | `m^2`, `cm^2`, `mm^2`, `km^2`, `in^2`, `ft^2`, `yd^2`, `mi^2`, `ha`, `ac` | | Volume | `m^3`, `cm^3`, `mm^3`, `km^3`, `in^3`, `ft^3`, `yd^3`, `L`, `ml`, `tsp`, `tbsp`, `floz`, `c`, `pt`, `qt`, `gal` | | Mass | `mg`, `g`, `kg`, `t`, `oz`, `lb`, `tn` | | Time | `ms`, `s`, `min`, `hr`, `d`, `wk`, `mo`, `yr` | | Angle | `rad`, `deg` | | Information | `bit`, `kbit`, `Mbit`, `Gbit`, `Tbit`, `B`, `kB`, `MB`, `GB`, `TB`, `PB` (and binary forms `Kibit…Tibit`, `KiB…PiB`) | | Currency | `USD` | | Count | `ct` | Area and volume units also accept the typographic forms `m²` and `m³`. Time units can also be squared (`s^2`, `hr^2`, …); squared time is the time² denominator that appears in acceleration units such as `m/s^2`. Most units also accept their full names, singular or plural, including common spelling variants — `ft` can be written `foot` or `feet`, and `m` as `meter`, `meters`, `metre`, or `metres`. Unit suffixes are case-sensitive: `USD` is not `usd`, and `Gbit` is not `gbit` or `GBIT`. Use the exact casing shown above. ## Time units and dates Time is the one dimension that does double duty: a duration and a [date](/math/types/dates) are both time values. A date is the count of seconds from the Unix epoch, which is why `closing_date + 30 d` works with no date-specific arithmetic at all — it is the same unit addition as `5 hr + 30 min`. See [Dates](/math/types/dates). The fixed-length time units are exact. A `wk` is always seven days and a `d` always 24 hours, so converting among `ms`, `s`, `min`, `hr`, `d`, and `wk` loses nothing. **`mo` and `yr` are average durations, not calendar steps.** One `mo` is exactly 30.4375 days and one `yr` is exactly 365.25 days — the average length of a Gregorian month and year, leap days included. | Unit | Length | | ------ | --------- | | `1 mo` | 30.4375 d | | `1 yr` | 365.25 d | They are averages so that time conversions stay exact and reversible in both directions: `1 yr` is precisely `12 mo`, and `1200 USD/yr` is precisely `100 USD/mo`. What they are not is calendar operators. Stepping a [date](/math/types/dates) by an average lands wherever that many seconds falls: `date(2026; 1; 31) + 1 mo` is March 2 at 10:30, skipping February, and `date(2026; 7; 21) + 1 yr` is the right day of 2027 at *06:00* rather than midnight — a result that looks correct and compares unequal, with no error raised. For calendar-correct month and year math, use [`adjdate`](/math/functions/dates#calendar-adjustment-adjdate). To land on the first day of a month or year, use [`floor` or `ceil`](/math/functions/rounding-and-numeric#snapping-a-date-to-a-boundary). See [`mo` and `yr` shift by an average](/math/types/dates#mo-and-yr-shift-by-an-average-not-a-calendar-step) for the full picture. ## Compound units A compound unit combines a unit from one dimension with a unit from another. **Any** simple unit from each constituent dimension can be paired, so velocity covers `m/s`, `km/hr`, `ft/min`, and every other length-over-time combination, not only the named shorthands. TrueMath also recognizes a few shorthand suffixes for common pairings. | Kind | Form | Named shorthands | | -------------- | ----------------------------------------------------------- | ------------------------------------------- | | Velocity | length / time | `kph` (km/hr), `mph` (mi/hr), `kt` (nmi/hr) | | Acceleration | length / time² | — | | Flow rate | volume / time | `GPM` (gal/min) | | Area rate | area / time | — | | Bandwidth | information / time | — | | Cost | currency / length, area, volume, time, mass, or information | — | | Per-unit count | count / length, area, volume, time, mass, or currency | — | Cost and per-unit-count compounds are written directly from their constituent units, for example `USD/hr`, `USD/ft^2`, or `ct/m^2`. A compound unit cancels the way the algebra does. Multiply a rate by the quantity it is *per*, and the shared dimension drops out, leaving the unit you were pricing in: ``` 10USD/hr * 8hr => 80 USD 5USD/ft^2 * 100ft^2 => 500 USD 60mi/hr * 2hr => 120 mi 100USD / 4hr => 25 USD/hr ``` The last line is the same rule read backwards: dividing a currency by a time *derives* a rate. This is what makes a rate safe to carry in a domain — an hourly rate multiplied by a count of hours can only produce a currency, and multiplying it by an area instead is rejected rather than quietly wrong. ## Conversion behavior * **Compatible units convert automatically.** `1m + 10cm` produces a length; you never convert by hand. * **The result takes the first unit in the expression.** `5in + 10ft` is `125in`, and `5hr + 6min` is `5.1hr` — the leading unit sets the result and the rest convert into it. * **Multiplication and division derive dimensions.** A length over a time is a velocity; a currency over an area is a cost-per-area. * **Incompatible units are rejected.** `5m + 3kg` is an error rather than a wrong number. See [Combining types](/authoring/writing-equations#combining-types). ### Values written in more than one unit A quantity can be written across two or more units of the same dimension, the way it would be spoken: ``` 3ft 9in => 3.75 ft 5hr 30min => 5.5 hr 1yr 6mo => 1.5 yr ``` The parts are added, so the result keeps the **first** unit written, exactly as any other sum does. What comes back is a single value in a single unit, and it stays that way through the calculation and on display — there is no feet-and-inches pair to carry around. `3ft 4in` is `3.3333 ft`. ### Casting to a specific unit To choose the unit a result is reported in, append a target unit to a parenthesized expression: ``` (1m + 10cm) mm ``` This returns the sum expressed in millimetres. Casting changes only the reported unit, not the underlying value, and the target must be compatible with the result's dimension. Angles are interpreted by the unit you attach: `sin(45deg)` and `sin((pi/4)rad)` are equivalent. See [Trigonometry](/math/functions/trigonometry). # Input formats Source: https://docs.truemath.ai/playground/input-formats Enter Playground calculations as natural language or as structured text. The Playground accepts two input formats: **natural language** prose and **structured text** `key: value` lines. Use natural language to explore, and structured text when you know the exact [variables](/concepts/variables) and target you want. (The [API](/api/calculate) adds a third, JSON, for programmatic use.) For the full grammar of each format — values, units, tables, scenario selectors, and how omitted lines are inferred — see [Input formats](/concepts/input-formats). # Reading results Source: https://docs.truemath.ai/playground/reading-results Interpret Playground results: variable values, provenance, scenario index, and how values are formatted. # Playground tour Source: https://docs.truemath.ai/playground/tour A guided tour of the TrueMath Playground for running and exploring calculations. # What-if scenarios Source: https://docs.truemath.ai/playground/what-if-scenarios Explore alternative outcomes by adjusting inputs to create new scenarios in the Playground. # Quickstart Source: https://docs.truemath.ai/quickstart Run your first TrueMath calculation in the Playground, then make the same call through the API. # Changelog Source: https://docs.truemath.ai/resources/changelog Notable changes to TrueMath and its documentation. # Compliance and security Source: https://docs.truemath.ai/resources/compliance-and-security TrueMath's data segregation, access controls, and compliance posture. # FAQ Source: https://docs.truemath.ai/resources/faq Frequently asked questions about TrueMath. # Glossary Source: https://docs.truemath.ai/resources/glossary Canonical TrueMath terminology: domain, activity, variable, provenance, scenario, and the other terms used consistently throughout the documentation. The canonical terms used throughout TrueMath. These are the preferred terms — the documentation uses them consistently and avoids synonyms. ### Activity An equation unit within a domain that relates a set of variables. An activity can solve for different variables depending on which values are known. See [Activities](/concepts/activities). ### Array Not a TrueMath term — the documentation uses [table](/math/types/tables). A one-dimensional table is the equivalent of a list or vector; a two-dimensional table, of a matrix. See [Tables](/math/types/tables). ### Audit trail The record of how a result was reached — the inputs, their provenance, and the values produced. Built on the [provenance](/concepts/provenance) tracked for every value. ### Calculable A property of a variable within an activity indicating that the activity can solve for that variable. ### Conversation An ongoing session that holds one or more scenarios. Calculations run inside a conversation. See [Scenarios](/concepts/scenarios). ### Date A point in time, held as a [unit number](/math/types/unit-numbers) counting seconds from the Unix epoch. Not a separate value type. See [Dates](/math/types/dates). ### Duration A span of time, held as a [unit number](/math/types/unit-numbers) carrying a time unit. The difference between two dates is a duration. See [Dates](/math/types/dates#durations). ### Domain A collection of variables and activities that models a specific area of calculation, such as a mortgage or a construction estimate. See [Domains](/concepts/domains). ### Domain library TrueMath's catalog of pre-built, role-based domains across professional verticals. See the [Domain library](/domain-library/overview). ### Format How a variable's value is shown, drawn from the vocabulary its [kind](#kind) takes: a number of decimal places for a `number`, `percent`, or `no_separator`, and a date or duration format token for the four time kinds. Display only. See [Variables](/concepts/variables). ### Kind What a variable's value is — `number`, `percent`, `no_separator`, `duration`, `date`, `datetime`, `time`, `table`, `bar_chart`, or `pie_chart`. Each kind takes its own vocabulary of [formats](#format). See [Variables](/concepts/variables). ### Provenance The record of where a variable's value came from — user-stated, calculated, carried forward, or default. See [Provenance](/concepts/provenance). ### Scalar number A plain numeric value with no unit. See [Scalar numbers](/math/types/scalar-numbers). ### Scenario A distinct calculation state within a conversation — a set of variable values and the results derived from them. See [Scenarios](/concepts/scenarios). ### Table A one- or two-dimensional collection of values, 1-indexed and with a per-cell optional unit, supporting element-wise operations. TrueMath's array type. See [Tables](/math/types/tables). ### Unit number A numeric value that carries a unit and converts automatically across compatible units. See [Unit numbers](/math/types/unit-numbers). ### Variable A named value within a domain, carrying a [kind](#kind), a [format](#format), an optional default, units, and provenance. See [Variables](/concepts/variables). ### Versioning The draft-to-published lifecycle a domain moves through. Calculations run against the published version. See [Versioning](/concepts/versioning).