The command line
stxt is the official STXT command: it validates, formats and describes documents
from a terminal, a Makefile or a continuous-integration job. It is an interface
over the TypeScript library @stxt-lang/core —it carries no
parser of its own—, so it reports the same errors, with the same codes, as the
VS Code extension, the playground and the Java and Python libraries. It is also
the only command line in the ecosystem: the libraries do not ship their own.
This page is the reference of the commands. To see them at work on a real project,
with its .stxt/ directory and VS Code next to it, read
The working environment; for the other tools, Tools.
Installation
It is published on npm as
@stxt-lang/cli and needs
Node 20 or newer:
npm install -g @stxt-lang/cli
stxt --version
stxt 0.7.1 (@stxt-lang/core 0.7.1)
--version prints two versions: the command's and that of the parser inside it.
Without installing anything permanently, every command works through npx:
npx @stxt-lang/cli validate --recursive docs/
To update, npm update -g @stxt-lang/cli; to remove it,
npm uninstall -g @stxt-lang/cli. Until 1.0, a minor release can change the
command surface; the version number and the release notes are in the
repository.
Synopsis
stxt [--version | --help]
stxt validate <file|dir>... [--recursive] [--format text|json] [--warn-schema | --no-schema]
stxt format <file|dir>... [--recursive] [--tabs | --spaces] [--write | --check] [--clean]
stxt describe <file>
stxt schemas [path]
stxt install <file> [--local | --user | --system | --root <dir>] [--force] [--ignore-non-definitions]
| Command | What it does |
|---|---|
stxt validate |
Parses and validates documents against the grammars it resolves; silent when all pass |
stxt format |
Rewrites documents in canonical form, keeping comments |
stxt describe |
Emits the logical tree of a document as JSON (STXT-TREE-SPEC) |
stxt schemas |
Shows the resolution chain and which grammar applies to each namespace |
stxt install |
Validates a grammar and installs it into a level of the resolution chain |
Interface conventions, common to every command:
- Every option has a single long form, with a double dash (
--recursive). Only four have a short alias, and they are the near-universal Unix conventions:-v(--version),-h(--help),-r(--recursive) and-w(--write). --versionand--helpare honoured in any position and take precedence over everything else:stxt validate --helpprints the general help (there is no per-command help).- An unknown option —single-dash ones included— or a non-existent command are
usage errors: they exit with code
2without doing anything. - Results (findings, JSON, formatted documents, listings) go to standard output; usage and read errors go to standard error.
- No command rewrites files unless asked to:
formatonly writes with--write, andinstallonly overwrites with--force.
Exit codes
The contract is the same for every command and is designed for scripts: a CI job must be able to tell the documents have errors from the command was misused.
| Code | Meaning |
|---|---|
0 |
All good: the documents parse and validate, nothing needs reformatting, the grammar was installed… |
1 |
Document failure: syntax or grammar errors, files that --check would change, an unreadable file, a definition that cannot be installed |
2 |
Misuse: unknown command or option, missing argument, incompatible options, a directory without --recursive |
So stxt validate docs/ && make deploy does exactly what it looks like, and an
unexpected 2 in CI points at a broken script, not at a broken document.
stxt validate
stxt validate <file|dir>... [--recursive] [--format text|json] [--warn-schema | --no-schema]
For each document: it parses it, resolves its grammar chain —the .stxt/
directories of its folder and of all its ancestors, then ~/.stxt and /etc/stxt,
or whatever STXT_PATH says; see Grammar resolution, below— and validates it
against the grammar of every namespace it uses. When everything passes it writes
nothing and exits with 0.
| Option | Effect |
|---|---|
--recursive, -r |
Descends into directories and validates all their *.stxt, sorted by name; .stxt/ directories are skipped |
--format text |
One finding per line, plus a summary (default) |
--format json |
The same findings as a JSON array, for machines |
--warn-schema |
Grammar errors are reported as warnings and do not fail; syntax errors still do |
--no-schema |
Syntax only: no grammar is resolved or applied |
A directory as argument requires --recursive; without it, it is a usage error
(2), so that nobody validates "the whole project" by accident. Files and
directories can be mixed in the same call.
The findings
With the project of The working environment —the com.acme.book
template in .stxt/ and docs/book.stxt—, a document with a badly written date
and without the mandatory ISBN produces:
stxt validate docs/book.stxt
/home/ana/libros/docs/book.stxt:6: [INVALID_VALUE] Published: Invalid date (1 de octubre de 2025) (error)
/home/ana/libros/docs/book.stxt:1: [INVALID_NUMBER] 0 nodes of 'com.acme.book:isbn' and min is 1 (error)
2 error(s), 0 warning(s)
Each line is file:line: [CODE] message (severity), with the absolute path of the
file, and a count at the end. The code is stable, upper-case and the same in
every tool of the ecosystem, so it can be used to filter in a script or to search
the specifications. The most common ones:
| Code | What it is about |
|---|---|
MIXED_INDENTATION, INDENTATION_LEVEL_NOT_VALID, INVALID_LINE |
Syntax (STXT-SPEC) |
INVALID_VALUE, INVALID_NUMBER, CHILD_NOT_DECLARED, NODE_NOT_EXIST_IN_SCHEMA |
Grammar (STXT-SCHEMA-SPEC) |
SCHEMA_NOT_FOUND |
The document uses a namespace the chain does not define |
DISCOVERY_DUPLICATE_NAMESPACE, DISCOVERY_NOT_A_DEFINITION |
The resolution chain itself (STXT-DISCOVERY-SPEC) |
FILE_NOT_READABLE |
The file does not exist or cannot be read |
A cardinality error is reported on the line of the parent (Book, line 1),
because it is the parent that has zero ISBN. Errors of the resolution chain
itself —a broken grammar in .stxt/, two definitions of the same namespace at the
same level— are reported as findings on line 0 of the offending file, not of
the document, and they fail the validation: the tool never silently picks one of
the two.
stxt validate docs/book.stxt
/home/ana/libros/.stxt/otro/copia.stxt:0: [DISCOVERY_DUPLICATE_NAMESPACE] Duplicate definition for namespace 'com.acme.book' at level /home/ana/libros/.stxt: already defined in /home/ana/libros/.stxt/@stxt.template/com.acme.book.stxt (error)
1 error(s), 0 warning(s)
Without a grammar, or with the grammar as a warning
Grammars are optional. A document without a namespace, or whose namespace no
grammar of the chain defines, is not wrong: it just cannot be validated. That is
why SCHEMA_NOT_FOUND is only reported when the document's chain does have
grammars and none of them covers the namespace it uses —the case of a misspelled
namespace—; if the chain is empty, nothing is said. A document that is a
definition (@stxt.schema or @stxt.template) is always checked against its
meta-schema, even when there is no other grammar at all.
Two options change what counts as a failure. --warn-schema downgrades grammar
errors to warnings, useful while a grammar is being introduced into a project with
older documents:
stxt validate --warn-schema docs/book.stxt
/home/ana/libros/docs/book.stxt:6: [INVALID_VALUE] Published: Invalid date (1 de octubre de 2025) (warning)
/home/ana/libros/docs/book.stxt:1: [INVALID_NUMBER] 0 nodes of 'com.acme.book:isbn' and min is 1 (warning)
0 error(s), 2 warning(s)
It exits with 0: there are warnings but no errors. And --no-schema checks the
syntax only, resolving and applying no grammar: the same document passes silently.
JSON output
With --format json the output is an array with one object per finding —file,
line, code, message, severity—, and [] when everything passes. There is
no summary or extra text, so it can be piped straight into jq or read from
another program. The exit code is the same as in text mode.
stxt validate --format json docs/book.stxt
[{"file":"/home/ana/libros/docs/book.stxt","line":6,"code":"INVALID_VALUE","message":"Published: Invalid date (1 de octubre de 2025)","severity":"error"},{"file":"/home/ana/libros/docs/book.stxt","line":1,"code":"INVALID_NUMBER","message":"0 nodes of 'com.acme.book:isbn' and min is 1","severity":"error"}]
# How many findings of each code there are in the whole project
stxt validate --format json --recursive docs/ | jq -r '.[].code' | sort | uniq -c
stxt format
stxt format <file|dir>... [--recursive] [--tabs | --spaces] [--write | --check] [--clean]
Rewrites documents in their canonical form: indentation normalised to tabs (or
to four spaces with --spaces), a single space after the colon, no trailing
whitespace. It works line by line: it re-renders the lines that open a node
and keeps everything the tree does not describe —comments, blank lines and the
content of >> blocks, which is only re-indented—, plus the original line ending
(CRLF included) and the absence of a final newline. The namespace is written only
where the source wrote it.
Three modes, mutually exclusive:
| Mode | What it does | Exits with 1 if… |
|---|---|---|
| (default) | Prints the result to standard output; never touches the disk | Some document does not parse |
--check |
Lists the files that would change (<file>: would be reformatted); writes nothing |
Some would change, or does not parse |
--write, -w |
Rewrites each file in place, only when it really changes, and says so (Formatted <file>) |
Some document does not parse |
And two more options:
| Option | Effect |
|---|---|
--tabs / --spaces |
Indent with tabs (default) or with four spaces |
--recursive, -r |
As in validate: descends, sorts by name, skips the .stxt/ directories |
--clean |
Switches engine: re-serialises the logical tree, so comments and blank lines are lost |
--check is the idea of gofmt -l or prettier --check, and it is what belongs
in CI. --clean exists to obtain the pure canonical document; since it loses
information, it is never the default and has to be asked for. A document with
syntax errors is reported and not reformatted, in any mode. format has no
grammar mode: rewriting a document has nothing to do with whether it validates.
A document written with spaces, a comment, a blank line and some extra spaces:
# Book record
Book (com.acme.book):
Title: Arquitectura de software moderna
Authors:
Author: María Pérez
ISBN: 978-84-123456-7-8
stxt format docs/book.stxt
# Book record
Book (com.acme.book):
Title: Arquitectura de software moderna
Authors:
Author: María Pérez
ISBN: 978-84-123456-7-8
The comment and the blank line are still there; the indentation is now tabs and the extra spaces are gone. The other two modes on the same file:
stxt format --check docs/book.stxt
/home/ana/libros/docs/book.stxt: would be reformatted
# exit code 1
stxt format --write docs/book.stxt
Formatted /home/ana/libros/docs/book.stxt
stxt format --check docs/book.stxt
# writes nothing any more: exit code 0
Combining --write with --check, or --tabs with --spaces, is a usage error
(2): stxt format: --write and --check cannot be combined.
stxt describe
stxt describe <file>
Parses one document and writes its logical tree to standard output in the
canonical JSON of STXT-TREE-SPEC: an array with the root nodes,
and for each node name, canonicalName, the effective namespace, the form
("inline" with value and children, or "block" with lines). It includes
no positions, comments or derived data. It is the way to hand an STXT document
to any program that speaks JSON without writing a parser.
describe resolves no grammar and validates nothing: those results are not
part of the logical tree. If the document has syntax errors it emits no partial
tree: it reports the errors on standard error and exits with 1.
Book (com.acme.book):
Title: Arquitectura de software moderna
Authors:
Author: María Pérez
ISBN: 978-84-123456-7-8
Chapter: Introducción
Content >>
Conceptos básicos y objetivos del libro.
stxt describe docs/book.stxt
[
{
"name": "Book",
"canonicalName": "book",
"namespace": "com.acme.book",
"form": "inline",
"value": "",
"children": [
{
"name": "Title",
"canonicalName": "title",
"namespace": "com.acme.book",
"form": "inline",
"value": "Arquitectura de software moderna",
"children": []
},
{
"name": "Authors",
"canonicalName": "authors",
"namespace": "com.acme.book",
"form": "inline",
"value": "",
"children": [
{
"name": "Author",
"canonicalName": "author",
"namespace": "com.acme.book",
"form": "inline",
"value": "María Pérez",
"children": []
}
]
},
{
"name": "ISBN",
"canonicalName": "isbn",
"namespace": "com.acme.book",
"form": "inline",
"value": "978-84-123456-7-8",
"children": []
},
{
"name": "Chapter",
"canonicalName": "chapter",
"namespace": "com.acme.book",
"form": "inline",
"value": "Introducción",
"children": [
{
"name": "Content",
"canonicalName": "content",
"namespace": "com.acme.book",
"form": "block",
"lines": [
"Conceptos básicos y objetivos del libro."
]
}
]
}
]
}
]
# The book's title, with jq
stxt describe docs/book.stxt | jq -r '.[0].children[] | select(.canonicalName == "title") | .value'
stxt schemas
stxt schemas [path]
Shows the resolution chain of a document or a directory —by default, the
current directory— and, for each namespace, which definition is active and which
file it comes from. It is the way to answer "what is this being validated
against?" before fighting a SCHEMA_NOT_FOUND.
stxt schemas docs
Resolution chain for /home/ana/libros/docs:
/home/ana/libros/.stxt
Namespaces:
com.acme.book <- /home/ana/libros/.stxt/@stxt.template/com.acme.book.stxt
The chain lists the levels in order of precedence: the .stxt/ directories of the
document and of its ancestors, then the user and system ones if they exist. If the
project sits inside another with its own .stxt/ (a monorepo), both appear, and
for each namespace the closest one wins. When there is nothing:
stxt schemas
Resolution chain for /home/ana/notas:
(empty — no .stxt directory found)
No namespaces resolved.
Errors of the chain are shown in an Errors: block at the end, with their code.
The typical case is a copy of the same grammar in two places of the same level:
the namespace is left without an active definition until one is removed, and
validate fails with the same error.
stxt schemas docs
Resolution chain for /home/ana/libros/docs:
/home/ana/libros/.stxt
No namespaces resolved.
Errors:
DISCOVERY_DUPLICATE_NAMESPACE /home/ana/libros/.stxt/otro/copia.stxt: Duplicate definition for namespace 'com.acme.book' at level /home/ana/libros/.stxt: already defined in /home/ana/libros/.stxt/@stxt.template/com.acme.book.stxt
schemas always exits with 0, even with errors in the chain: it informs, it does
not judge; validate is the one that fails.
stxt install
stxt install <file> [--local | --user | --system | --root <dir>] [--force] [--ignore-non-definitions]
Installs the definitions —schemas and templates— of a file into a level of the
resolution chain. It is not a copy: it first checks that the file has the
.stxt extension, that it parses and that each of its root nodes is a definition
that validates against its meta-schema (the same check the resolver performs when
loading a level), and only then it writes, all or nothing. Each definition is
written separately, in canonical form, as
<level>/@stxt.schema/<namespace>.stxt or <level>/@stxt.template/<namespace>.stxt,
named after the namespace it defines, not after the source file; a file with
several definitions is split into several.
That naming is a convention of the CLI, not of the language:
STXT-DISCOVERY-SPEC gives no meaning to file names or to the
subdirectories of a level, so by hand a grammar can be placed wherever you like.
The convention is worth following because it makes obvious, with an ls, which
namespaces each level defines.
| Level | Option | Where it writes |
|---|---|---|
| Project | --local |
./.stxt (the current directory; default) |
| User | --user |
~/.stxt (%USERPROFILE%\.stxt on Windows) |
| System | --system |
/etc/stxt (%ProgramData%\stxt on Windows) |
| Any | --root <dir> |
The given directory, whether it exists or not |
| Option | Effect |
|---|---|
--force |
Overwrites a definition already installed for that namespace (path clash or namespace clash) |
--ignore-non-definitions |
Installs the definitions of the file and skips the other root nodes, instead of failing |
stxt install book-template.stxt
Installed com.acme.book (@stxt.template) to /home/ana/libros/.stxt/@stxt.template/com.acme.book.stxt
What makes the installation fail (code 1), and what it says:
stxt install book-template.stxt
stxt install: /home/ana/libros/.stxt/@stxt.template/com.acme.book.stxt already exists (use --force to overwrite)
stxt install docs/book.stxt
stxt install: docs/book.stxt:1: root node 'Book' belongs to 'com.acme.book', not to @stxt.schema or @stxt.template (use --ignore-non-definitions to install only the definitions)
stxt install broken.stxt
stxt install: broken.stxt: invalid @stxt.template definition: Type not valid: NOTATYPE
stxt install notes.txt
stxt install: not an STXT document (.stxt expected): notes.txt
A file that mixes a template with example documents is installed with
--ignore-non-definitions: the template goes in, the examples are ignored. And to
share a grammar among every project on a machine, stxt install --user grammar.stxt.
Grammar resolution and STXT_PATH
The three commands that apply grammars —validate, schemas and install— use
the same resolution chain as the VS Code extension and the libraries, the one
of STXT-DISCOVERY-SPEC: for a document, every .stxt/
directory from its folder upwards, then ~/.stxt and finally /etc/stxt. Inside
each level every .stxt file is loaded, recursively, and each must be a
definition. Precedence is per namespace: for each one the closest level that
defines it wins, and the other levels keep contributing the ones it does not
define. Two definitions of the same namespace at the same level are an error, and
that namespace is left without a definition.
The STXT_PATH environment variable replaces the whole chain with a list
of directories separated by : (; on Windows), in order of precedence; the
entries need not be called .stxt, and one that does not exist simply
contributes nothing. It is the way to keep a CI job independent of whatever the
machine running it has in ~/.stxt or /etc/stxt:
STXT_PATH=./.stxt stxt validate --recursive docs/
A STXT_PATH that is set but empty leaves the chain empty: documents are checked
for syntax only, and schemas shows it as (empty — no .stxt directory found).
The CLI adds no rule of its own to this chain; the whole policy lives in the
library, which is why it is identical in every tool.
Recipes
Continuous integration. Two steps: the documents validate and are well
formatted. Fails with 1 if something does not pass, and with 2 if the command
itself is misspelled.
npx @stxt-lang/cli validate --recursive docs/
npx @stxt-lang/cli format --check --recursive docs/
A Makefile.
check:
stxt validate --recursive docs/
stxt format --check --recursive docs/
fmt:
stxt format --write --recursive docs/
Formatting a whole project for the first time, switching it to spaces:
stxt format --write --spaces --recursive .
The .stxt/ directories are skipped; format the grammars separately if you want.
The findings as data —for instance, only the errors of one specific code—:
stxt validate --format json --recursive docs/ | jq '.[] | select(.code == "SCHEMA_NOT_FOUND")'
A document as JSON for another program:
stxt describe config.stxt > config.json
Limits
What the command line does not do, so you do not go looking for it:
- It does not read from standard input: every command works on files and directories.
- It does not convert to other formats: the only structured output is the JSON of
describe(the STXT-TREE-SPEC tree) and that ofvalidate --format json. - There is no per-command help:
stxt --helpis all the help, and this page is the reference. - There is no parser inside. A parsing or validation error belongs to the
@stxt-lang/corelibrary —and is therefore shared by every tool—; the place to report it is stxt-js. Errors in options, messages or behaviour of the command go to stxt-cli.