Quickstart — first 5 minutes with CX
One bracket form is your data, your query and your program, so the first file you write is also the first thing you query, validate, hash and serve. Python set the standard for a language that is productive on day one; CX is built to that standard and past it where data work hurts (cx-vs-python). The whole idea in one replayed fixture — a document, a query that is a pattern over it, and the live output:
input.cx
[users
[user active=true [name Alice] [email a@x.com]]
[user active=false [name Bob] [email b@x.com]]
[user active=true [name Carol] [email c@x.com]]]
prog.cx
[?for [user @active=true [email $e]] [yield $e]]
$ cx --data=input.cx prog.cx
'a@x.com'
'c@x.com'
Fixture program-for-002-active-emails in deps/cx-core-code/conformance/code.cxd — replayed against this cx when the page was built.
The rest of this page is the shortest path from "I have never seen CX" to "I have parsed, hashed, queried, and validated my first document, and called CX from my host language." Eight steps, five minutes if your toolchain is warm. If you only have time for one section, read this one — then jump to next-steps.
Install
CX ships as a single self-contained binary cx plus a
shared library libcx for host-language bindings. Every
release publishes the four builds of it for macOS on Apple
Silicon and for Linux on arm64, and the installer picks the
one for your machine:
curl -sSL https://cxhome.org/install | sh
cx --version
The installer resolves the latest release, verifies the
tarball against the release's SHA256SUMS.txt and installs
the platform build under ~/.local by default (PREFIX=
overrides); leaner builds install with CX_PROFILE=data,
CX_PROFILE=embed, or CX_PROFILE=cli. Every tarball is
linked from the guide's downloads page. On a machine with no
prebuilt build — Linux on x86_64, an Intel Mac, Windows — the
installer says so and points at the source build.
Build from source — the V toolchain comes from the
vendored fork at third_party/v, a git submodule, so clone
with --recursive. The build fetches its pinned repositories
with a released cx: make deps-sync finds one on PATH or
at ~/.local/bin/cx, and CX_BIN= names another.
git clone --recursive https://github.com/cx-home/cx
cd cx
make -C third_party/v # the vendored V toolchain, once
make deps-sync # the pinned repositories, under deps/
make build-vcx # libcx + cx, at deps/cx-core-code/vcx/target/cx
make install-cli # /usr/local/bin/cx; PREFIX= overrides
cx --version
Per-platform notes — loader paths, editor wiring, the WebAssembly bundle — are in §0.10.
If cx --version prints a version line, you are ready.
If you see command not found, your PATH does not include
the install location — see faq for recovery steps.
Hello, CX
Every interactive CX session starts the same way: pipe a CX
literal into the cx binary and see what comes out.
echo '[hello greeting="world"]' | cx
The output is the same shape you typed:
[hello greeting=world]
Three things to notice. First, the value evaluated to itself — data and code share one syntax, and a bare data element is its own value (see homoiconicity). Second, the double quotes are gone — canonical output drops quotes entirely when a token is bare-safe, and attribute order is preserved as written; attributes are never re-sorted, not even for hashing (see canonical). Third, no closing tag duplication — every CX element is a single bracketed form. That is the whole syntax.
Try a tiny program too. The [?for] directive iterates and
yields, one line per yielded item:
echo '[?for [in $x (1, 2, 3)] [yield [n $x]]]' | cx
[n 1]
[n 2]
[n 3]
Your first file
Real CX work lives in files. Create users.cx with three
users:
[users
[user id=1 email='ada@example.com' active=true]
[user id=2 email='grace@example.com' active=false]
[user id=3 email='linus@example.com' active=true]]
Parse it (round-trips through the canonicalizer; useful as a syntax check):
cx fmt users.cx [# lossless canonical — one line, double quotes, comments kept #]
cx canonical users.cx [# strict canonical bytes — indented, quotes dropped where bare-safe #]
Hash it — the SHA-256 of the strict canonical bytes is the document's identity (see hash):
cx hash users.cx
[# → sha2-256:3ace03ec...7a13c9e6 (tagged; 64 hex chars) #]
The hash is stable across machines, OS versions, and bindings. Two documents with the same hash are byte-identical in canonical form.
Convert JSON ↔ CX
CX speaks JSON (and YAML, TOML, XML, Markdown) as
projections. JSON, YAML, TOML, and XML round-trip losslessly
under the --lossless flag; the default JSON projection is
idiomatic and can drop CX-only detail such as mixed content;
Markdown and CSV are lossy by design. Start from JSON:
cat > users.json <<'EOF'
{"users":[
{"id":1,"email":"ada@example.com","active":true},
{"id":2,"email":"grace@example.com","active":false}
]}
EOF
cx --from=json --to=cx users.json > users.cx
users.cx now contains the CX projection of that JSON.
Convert back and verify the round-trip:
cx --from=cx --to=json users.cx > users.roundtrip.json
diff <(jq -S . users.json) <(jq -S . users.roundtrip.json)
[# → no output: byte-identical after canonicalization #]
The document's identity is also stable through a lossless
projection round-trip — cx hash users.cx equals
cx --from=json --to=cx users.json | cx hash — because
hashing is computed on canonical bytes (see
canon-cross-format). The lossy projections (default JSON
for mixed content, Markdown, CSV) are the named exceptions.
Select with CXPath
CXPath is the universal selector — for reading, for iteration, for update focus. Same vocabulary everywhere. The simplest query asks: "which users are active?"
A CX program is just another document, so the quickest CLI
query appends one to your data and runs the result — the
program's // paths resolve against the document that
precedes it:
echo '[?for [in $u //user[= $_@active true]] [yield $u@email]]' \
| cat users.cx - | cx
Read the predicate as: "from anywhere in the document (//),
find a user element whose @active attribute equals true,
then take its @email attribute." Predicates are ordinary
prefix CX code over the context node $_ — [= $_@active
true]. If you have written XPath, the alignment is
deliberate — see the CXPath sections of
spec/03-approved/core/code.md
and the conformance corpus conformance/xpath_31_parity.cxd
for what carries over and what does not.
The same path drives iteration in a program:
[users
[user email='ada@example.com' active=true]
[user email='bob@example.com' active=false]]
[?for [in $u //user[= $_@active true]] [yield
[active-user email=$u@email]]]
Validate with a schema
A CX schema (.cxs) is a CX document that describes the
allowed shape of another CX document. Write
users.cxs:
[schema of=users mode=strict]
[users [elem user [card "1..*"]]]
[user
[attr id::int [req]]
[attr email::string [req]]
[attr active::bool [req]]]
Run the validator:
cx validate users.cx --schema=users.cxs
[# → prints nothing; exit code 0 #]
Break it on purpose to see a diagnostic. Remove active
from one user and re-run:
cx validate users.cx --schema=users.cxs
[# → users.cx:2:4: error: S002: missing required attribute 'active' on <user> #]
Schema modes are open / strict / closed — strict
rejects unknown attributes, closed also rejects unknown
child elements. See schema for the full constraint
vocabulary.
Hello from your host language
CX ships four Tier-1 host-language bindings — V (the native reference), Python, Go, and Rust. The Layer-1 method set is identical across all four (see layer-1). One install, one shape:
Python:
pip install cxlib
import cxlib
with open('users.cx', 'rb') as f:
doc = cxlib.Doc.parse(f.read())
print(doc.hash()) [# tagged sha2-256:… #]
for user in doc.select_all('//user[= $_@active true]'):
print(user.attr('email'))
V (native — imports the V core directly):
import os
import native as cx
doc := cx.parse_doc(os.read_file('users.cx')!)!
println(doc.hash()!)
for user in doc.select_all('//user[= $_@active true]')! {
println(user.attr('email') or { continue })
}
Go:
import "github.com/cx-home/cx/lang/go/cxlib"
src, _ := os.ReadFile("users.cx")
doc, _ := cxlib.ParseDoc(src)
h, _ := doc.Hash()
fmt.Println(h)
users, _ := doc.SelectAll("//user[= $_@active true]")
for _, u := range users {
fmt.Println(u.Attr("email"))
}
Rust:
use cxlib::code::Doc;
let doc = Doc::parse(&std::fs::read("users.cx")?)?;
println!("{}", doc.hash()?);
for user in doc.select_all("//user[= $_@active true]")? {
println!("{}", user.attr("email").unwrap());
}
The identity-critical operations — hashing,
canonicalization, program eval — are one implementation (the
V core that libcx is compiled from), so their results are
bit-identical across bindings. Parsing and several emitters
are implemented natively per binding and held to parity by
the conformance corpus: the Layer-1 parity gate in
spec/03-approved/misc/bindings.md,
tracked per binding in
spec/03-approved/misc/parity-matrix.md.
Where to go next
You now have CX installed, you can write data, hash it, convert it, query it, validate it, and call it from your host language. From here, follow your interest:
- Learn the data language end-to-end. intro walks the design philosophy; data is the full reference for scalars, collections, sigils, and mixed content.
- Learn the code language. code covers the directive registry, CXPath in depth, patterns, builtins, services / workers / async, and resilience.
- Browse the modules. CX ships 92
modules, each declared once in
registry/modules.cxd; the nav lists every bundled one under the ring or group that owns it. - Wire a binding into a real project. bindings has per-language quickstarts and the Layer-1 / Layer-2 method set.
- Pick a tooling integration. tooling covers
the LSP, tree-sitter grammar, VS Code, Neovim, Helix,
cx lint,cx diff, and the playground. - Migrate an existing codebase. migration covers prior-version migration and intake from JSON / YAML / TOML / XML.
- Compare against alternatives. comparison states CX's case against JSON, YAML, TOML, XML, Protobuf, MessagePack, Pkl, Dhall, Parquet, and Arrow, starting beside Python — the standard CX is built to meet and exceed (cx-vs-python).
- Look up a term. glossary defines every CX-specific term used in this guide; faq covers the first-week-of-CX questions.
The community lives at https://github.com/cx-home/cx — file
bugs and read the spec: approved documents live under
spec/03-approved/, in-flight work under spec/02-working/.
The guide ships an in-browser playground (make guide renders
it to docs/guide/playground.html) that runs CX with no
install.
Install — per-platform detail
Per-platform install instructions. Section §0.1 covers the installer and source paths at a glance; this section carries the full per-platform breakdown including loader path and editor wiring.
Release status
Every release publishes the four builds (platform, cli,
embed, data) as tarballs for macOS on Apple Silicon and
for Linux on arm64, plus a conformance bundle, the VS Code
extension and SHA256SUMS.txt over all of them; the
installer one-liner curl -sSL https://cxhome.org/install | sh
picks the tarball for your machine. A Homebrew tap and a
Docker image are not published. Production users should pin
to a release tag.
macOS
curl -sSL https://cxhome.org/install | sh
Installs the prebuilt Apple Silicon platform build;
CX_PROFILE=data|embed|cli selects a leaner one. No Intel
build is published; on an Intel Mac the installer says so and
points at the source build (§0.10.6). A Homebrew tap is not
published.
Linux
curl -sSL https://cxhome.org/install | sh
On arm64 the installer installs the prebuilt Linux build, which the release pipeline compiles in ubuntu-22.04. No x86_64 build is published; there, build from source (§0.10.6).
Windows
No Windows build is published, and the installer refuses an operating system other than macOS or Linux (the V toolchain's Windows support is still on the roadmap — see faq).
WebAssembly bundle
The libcx-wasm bundle powers the live evaluator in
the playground; it exports the
_cx_code_eval family plus _cx_code_diagram and
_cx_code_tree. It is not a release asset: make guide-wasm
builds it from source with emscripten (which devbox.json
pins) and renders the guide around it.
Build from source
git clone --recursive https://github.com/cx-home/cx
cd cx
make -C third_party/v # the vendored V toolchain, once
make deps-sync # the pinned repositories, under deps/
make build-vcx # libcx + cx, at deps/cx-core-code/vcx/target/cx
make install-cli # /usr/local/bin/cx; PREFIX= overrides
The build compiles with the vendored V fork at
third_party/v, and make deps-sync fetches the
repositories deps.cxd pins with a released cx — one on
PATH, at ~/.local/bin/cx, or named by CX_BIN=. Check out
the latest release tag to build a specific release.
Verify the install
cx --version
cx --json - <<< "[hello world]"
A successful run prints the version and a JSON document
on stdout. The cx binary is self-contained — it does not
link libcx. If a host-language binding cannot find
libcx, point it at the library with LIBCX_PATH (full
path to the library file) or LIBCX_LIB_DIR (its
directory).
Twenty-chapter walkthrough
This walkthrough walks CX from the smallest piece to the
fullest expression in twenty linked chapters. Each
chapter introduces one shape, shows it in context, and
ends with two or three exercises you can run with cx
--ast or cx --json. Pace yourself. Assumed background:
familiarity with at least one of JSON, YAML, TOML, or XML
— CX assumes you know what an object, array, scalar, and
attribute are.
Why CX looks the way it does
Most projects need two kinds of file: typed configuration in nested structures, and documents with prose plus structure. JSON, YAML, and TOML are optimized for the first; Markdown, HTML, and XML for the second. Projects end up with multiple file formats and multiple parsers. CX is one syntax for both. Five design constraints picked the shape: one construct, no indentation semantics, optional quoting, types first-class, lossless conversion to existing formats.
Elements
Every CX node is a bracket pair with a name and an optional body. Editors balance brackets natively; code folding works without parser hooks. The opening name appears once; nothing repeats at close.
[p Hello, World]
Exercises. Save as hello.cx and run cx --ast
hello.cx. Add a child element and re-run. Then run cx
--json on the same file.
Attributes
Attributes are name=value pairs after the element name.
Bare values auto-type: 8080 becomes int, 3.14
becomes float, true becomes bool, 2026-01-01
becomes date. Leading-zero tokens stay strings —
02134 stays the string 02134 — to avoid the YAML
Norway problem.
[server host=api.example.com port=8080 ssl=true]
Exercises. Add three more attributes and check what type cx assigns each. Quote a value containing a space and inspect the AST.
Types
CX auto-types most values; for the rest, a glued ::T
ascription pins the value or body type. Sized variants
carry range constraints that validate at parse time.
[port::u16 8080]
Exercises. Try setting the ::u16 value to 70000 and observe the error. Compare [n 12] vs [n::decimal 12] in cx --ast.
Element bodies
A body holds Text, Scalar, child Element, or collection literal in source order. Mixed content is first-class — text and structure interleave the way they do in XML.
[para Try the [b Margherita] — our [em best seller], hand-tossed daily.]
Triple-quoted bodies preserve multi-line content verbatim. Exercises. Write an article with h1, p, em, a children; convert with cx --md.
Comments
Two comment forms, both valid at document level and inside
element bodies. # starts a line comment; [; … ] is a
block comment that can span lines.
# top pick this week
[shop port=8080
[; the menu is trimmed for the tutorial ]
[menu [pizza name=Margherita]]]
Exercises. Check what cx fmt preserves and what cx canonical drops.
Sigils
Five sigils give CX structural reach beyond name and
attribute. @ is an attribute prefix in path
expressions; in body position it appears only inside the
[ref @id] reference form. &
declares an anchor; * merges from an anchor; #
declares an id; : pins a data type.
[defaults &shared timeout=30 retries=3]
[server *shared name=api]
Collection literals
Three first-class collection shapes. Sequence with parens flattens; array with square brackets preserves nesting; map with braces gives named key-to-value entries. Collections live in an element body — attributes hold a single scalar, never a collection.
[pizza [toppings (cheese, basil, oil)]]
[pizza [prices {small: 9, medium: 12, large: 15}]]
Typed tables
A [table[…]] block declares column names and types.
The cells that follow partition into rows by the declared
column count — newlines are ordinary whitespace, so one
row per line is a readability convention, not a rule.
Cells parse against the declared column types, and the
table round-trips to CSV with types preserved via the
header row. In the binary ast-bin form, the table record
stores rows row-major; the column-major layout is the
separate CXCol format (see analytics).
[orders [table[item::string qty::u32 paid::bool when::date]]
Margherita 2 true 2026-05-09
Hawaiian 1 false 2026-05-09]
Multi-document files
A single CX file can hold multiple top-level documents
separated by three dashes on a line by themselves.
Same separator as YAML; each side parses as an
independent document, and cx --json emits the file as
an array of documents.
Six surface dialects
The same tree parses and emits as any of six document dialects — CX, XML, JSON, YAML, TOML, or Markdown. Each dialect captures the subset of the data model it can represent. Beyond the six, the delimited surfaces (CSV / TSV / PSV) and the columnar binary surface (CXCol) convert through the same codec layer — see analytics.
cx --yaml pizza.cx
CX code — directives inside data
CX code is CX that does things. A directive is an
element whose name starts with ?. Directives parse
as regular CX and evaluate against the surrounding
tree when cx runs the document.
[page title='Menu — New Haven']
[?let [= $t //page/@title] [h1 [$string $t]]]
Path expressions (CXPath)
CXPath is the query language inside CX code. Paths
separated by /; predicates in […] are prefix CX code
over the context node $_; axes navigate by
relationship.
[menu [pizza name=Margherita price=12]
[pizza name=Marinara price=9]]
//pizza[> $_@price 10]
Let and for
[?let] binds a name once and reuses it. [?for]
iterates a path expression result.
[?let [= $tax 0.22] [= $price 12] [* $price [+ 1 $tax]]]
Filters
Filter functions transform values. Compose them with
[?pipe] — bare stages, threaded left to right — or nest
them.
[?lib 'cx-stdlib/strings']
[?pipe ' margherita ' [$strings:trim] [$strings:upper]]
Multi-branch conditional
[?if] is strictly two-branch — a condition, a [then],
an [else]. Multi-way dispatch on a value is [?match]:
[case] arms carry a literal or pattern plus a result,
tried in order, and [else] is the catch-all.
[?let [= $size 'medium']
[?match $size
[case 'small' 9] [case 'medium' 12] [case 'large' 15]
[else 0]]]
Named templates
[?def] defines a reusable fragment; call it like any
other function — [$line …] — with positional args.
[?def line ($item) [tr [td $item/@name] [td $item/@qty]]]
Includes
[?cx include=PATH] splices another file's children into
the parent document at parse time. Resolution is opt-in:
without an include root the directive is preserved
untouched; with --include-root every include resolves
during the parse. Paths are relative-only and bounded by
the root — no absolute paths, no URLs, no escaping the
root — and include depth defaults to 8. Errors are the
data-parse codes cx-err:E901–E911.
[users
[?cx include=lib/admins.cx]
[user name=carol]]
cx --xml --include-root=. config.cx
Identity
Every CX document has a canonical form: presentation
stripped, attribute order preserved as written, anchors
resolved and erased (the merge is materialized).
cx hash returns the SHA-256 of those
canonical bytes. Two documents with the same data
have the same hash, in any dialect.
cx hash menu.cx
Putting it all together
Every shape composes naturally with every other. The whole-shop example at §14.50 and the whole-kitchen CX code example at §15.50 exercise every shape in this tutorial inside a single working document.