Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Lexical structure

A ThoughtML document is a sequence of lines. Each line is classified before anything else happens.

Line kinds

LineRule
BlankEmpty or whitespace-only. Ignored.
CommentFirst non-space character is #. Ignored.
HeaderZero indentation, non-blank. Starts a new record.
BlockIndented (≥ 1 space), non-blank. Belongs to the open record.

A leading UTF-8 BOM is stripped. Both \n and \r\n line endings work.

Indentation

  • Indentation is spaces. A tab in the leading whitespace is an error (tab indentation is invalid; v0 requires spaces).
  • Block lines should be indented two or more spaces; one space earns a warning.
  • A column-0 line closes every open record and starts a new top-level one.
  • A more-indented line nests under the line above it; a less-indented line closes records back to the matching level. (Nesting only carries meaning inside a scope — see there.)
focus a            # header, column 0
  kind claim       # block line, indent 2 — belongs to `a`
  Some prose.      # block line — also belongs to `a`
focus b            # header again — closes `a`, opens `b`

Comments

A # at the start of a line (after optional indentation) makes the whole line a comment. There are no end-of-line comments — a # partway through a line is just text.

# This whole line is a comment.
focus a
  This sentence has a # but it is part of the body, not a comment.

Identifiers

Identifiers (record ids, relation names, references) are lowercase kebab-case: they start with a lowercase letter and contain only lowercase letters, digits, and hyphens.

cache-is-safe       ✓
conversation-2026-06-09   ✓
Metric              ✗  (uppercase)
-x                  ✗  (leading hyphen)
9x                  ✗  (leading digit)

For imports, an identifier may be namespace-qualified with dots: base.capacity-budget — each dot-separated segment is itself a valid kebab identifier.

Values

The value after a field keyword is classified into one of a small fixed set of forms:

FormExampleNotes
Number0.72, 1, -3Plain base-10. No exponents, inf, or nan.
Range0.25..0.70low..high, inclusive.
Unknown?The explicit “not stated” marker.
Time2026-06-09, 2026-06-14T14:05+00:00Loose ISO-8601.
Refcache-is-safeA bare identifier — a reference to another record.
Symbolopen, highSame lexical form as a ref; intent depends on the field.
URIuri:https://example.org/xA uri:-prefixed value.
Lista, b, cComma-separated identifiers/symbols.
Text"quoted string", or any multi-word valueFree text. A quoted string is always text.

A single-token value is classified by the table above; a multi-word value (more than one whitespace-separated token) is Text, unless it’s a comma list of identifiers. Quotes force Text.

Why this matters for provenance. A value like 0.9 assumed is two tokens, so it would classify as Text. ThoughtML peels a trailing basis keyword (measured/estimated/assumed) off first, then re-classifies the remaining 0.9 as a Number. That’s how confidence 0.9 assumed parses correctly.