Skip to content

CLI commands

Every command and option of the maplibre-yaml CLI (@maplibre-yaml/cli 0.6). For a walkthrough of init, validate, preview and emit, see The CLI (mlym).

The CLI needs Node.js 20.9 or later. On an older Node it stops with “maplibre-yaml needs Node.js 20.9 or newer”.

It is one program with several names:

You typeWorks
npx maplibre-yaml@latest …Anywhere, with no install. npx downloads maplibre-yaml, a small package that runs @maplibre-yaml/cli
npx @maplibre-yaml/cli@latest …Anywhere, with no install: the full name
npx mlym …After npm install --save-dev @maplibre-yaml/cli in the project. Without the install it fails with a 404, because no npm package is called mlym
mlym … or maplibre-yaml …After npm install -g @maplibre-yaml/cli, in any folder; or in package.json scripts of a project that installed it

The examples on this page use npx maplibre-yaml@latest.

OptionAliasDescription
--help-hShow help for a command (npx maplibre-yaml@latest init --help)
--versionShow the CLI version

Create a new project from a template.

Terminal window
npx maplibre-yaml@latest init [folder] [options]
ArgumentRequiredDefaultDescription
folderNo./<name> (./my-map for basic)Folder to create. It must not exist yet, or be empty
OptionAliasTypeDefaultDescription
--template-tstringbasicbasic, story or astro
--name-nstringthe folder’s nameProject name, used for the page title and the map id
--listbooleanList the templates and exit

An unknown template or option (for example --dir, which init does not have) prints an error and exits with code 1, and nothing is created.

TemplateCreatesRun it with
basicindex.html (CDN import map, <ml-map> with a height, viewport meta tag; core loads from the CDN at the release line this CLI was built with) and map.yaml (a street basemap and one clickable point)npx serve . in the folder, then open the address it prints. Needs no install or build. Another local server works if it supports range requests (which PMTiles, FlatGeobuf, GeoParquet and COG files need); python -m http.server doesn’t
storyindex.html and a story file, story.yamlA story map with no build step: the page loads <ml-scrollytelling> from the CDN, at the release line this CLI was built with. Serve the folder (npx serve .) and scroll; see Story maps in plain HTML
astroAn Astro 7 site: a layout (with the viewport meta tag) and a page rendering public/configs/map.yaml with @maplibre-yaml/astro’s <Map>, the optional integration in astro.config.mjs with commented-out site/base lines for GitHub Pages, and a .github/workflows/deploy.yml that publishes it with withastro/action. Its @maplibre-yaml/core and @maplibre-yaml/astro ranges are the versions this CLI release was built with, and its package.json asks for Node.js 22.23.4 or later (Astro 7.4.2’s own requirement)npm install, then npm run dev. For GitHub Pages, set site and base in astro.config.mjs and push (see Publishing & subpaths)

Every YAML file the templates write starts with the schema modeline, so editors with the YAML extension complete and check it as you type. init prints the next steps for the template it used.

Terminal window
npx maplibre-yaml@latest init my-map # ./my-map from the basic template
npx maplibre-yaml@latest init my-site --template astro # an Astro site
npx maplibre-yaml@latest init --list # list the templates

Check map documents and story files, written in YAML, JSON or TOML, against the format. It reports problems with their path in the document, a message, and the line and column.

Terminal window
npx maplibre-yaml@latest validate <patterns> [options]
ArgumentRequiredDescription
patternsYesOne or more file paths, folders or glob patterns. All of them are validated

A pattern can be:

  • a file: map.yaml;
  • a folder: maps/ (every .yaml and .yml file under it, and the .json and .toml files under it that are map or story documents; below);
  • a glob: "maps/**/*.yaml". Quote it, so the CLI sees the pattern rather than your shell’s expansion of it. A quoted pattern with a space in a folder name, "My Maps/*.yaml", works;
  • several of these: "src/**/*.map.yaml" "maps/*.yaml".

Every argument is checked and reported. A file name that doesn’t exist prints File not found: …, and a pattern that matches nothing prints No files match "…". Both make the command exit with code 2 unless a file also has errors (code 1). Files excluded by validate.ignorePatterns in the project settings file are skipped, even when named directly.

OptionAliasTypeDefaultDescription
--format-fstringhumanOutput format: human, json, sarif, vscode (below)
--strict / --no-strictbooleanon in CI, off otherwiseTreat warnings as errors. Without either flag, strict mode is on when the CI environment variable is set (as CI services do) and off on your machine
--strict-deprecationsbooleanfalseAlso treat deprecation warnings as errors (--strict leaves them as warnings)
--watch-wbooleanfalseKeep running and re-validate each file when it changes. Only changed files are checked again
--verbosebooleanfalseList the .json and .toml files a folder search skipped because they are not map or story documents

validate reports three kinds of finding:

  • errors: the document is invalid; the map won’t render as written.
  • warnings: valid, but probably not what you meant, such as a layer naming a source that isn’t declared, or a map with no basemap. In strict mode they count as errors.
  • notes: how a valid value is read, such as a basemap written as just a .pmtiles file. A note never makes a file invalid, even in strict mode.

A folder of maps often holds other JSON too: GeoJSON, a package.json, a style.json. So under a folder argument, a .json or .toml file is validated only if its first 64 KB (for a larger JSON file, also its last 64 KB, where sorted keys put the root type) show it is a document: it names a type of map or scrollytelling, has a top-level pages key, or carries a $schema (or a TOML #:schema line) under https://docs.maplibre-yaml.org/schema/. The check reads the text and doesn’t parse it, so a document with a syntax error, or one that can’t be read, still joins the run and fails there. The project settings file, maplibre-yaml.config.json, is always skipped. A file you name, or a glob matches, is validated whatever it contains.

--verbose lists each file the folder search skipped. A folder with no document in it is reported, and the command exits with code 2:

✗ No map or story documents found in maps (looked for .yaml and .yml files, and .json and .toml files that declare a map or story; --verbose lists the files skipped)
Terminal window
npx maplibre-yaml@latest validate map.yaml
npx maplibre-yaml@latest validate maps/ --verbose
npx maplibre-yaml@latest validate "maps/**/*.yaml" --watch
npx maplibre-yaml@latest validate "**/*.yaml" --format sarif > results.sarif

human (the default) prints each file with a mark, then its findings and a summary:

⚠ map.yaml
warning layers.0.source: Layer "quakes" uses source "quake", which is not declared under sources:. Did you mean "quakes"?
at line 13, column 13
⚠ 1 file(s) with warnings: 1 warning(s)

A note prints as ℹ note, and a file with only notes is ✓ valid. With more than 10 files, the human and vscode formats show a progress bar.

json prints one object: valid, a files array (each with its absolute file path, valid, errors and warnings), and a summary of counts. Each finding has path, message, line, column and severity; notes are in warnings with "severity": "note" and are counted in summary.noteCount, not warningCount. The full shape is in Using AI agents › The JSON output.

sarif prints SARIF 2.1.0, which GitHub code scanning reads (recipe below). Notes have the SARIF level note.

vscode prints one line per finding, file:line:column: severity: [path] message, where severity is error, warning, or info for a note. VS Code’s problem matcher reads it: see Validate on save.


Start a local server that shows one document (a map or a story map, in YAML, JSON or TOML) on a page of its own, and reloads it when you save the file.

Terminal window
npx maplibre-yaml@latest preview <file> [options]
ArgumentRequiredDescription
fileYesThe map document or story file to show (.yaml, .yml, .json or .toml)
OptionTypeDefaultDescription
--portnumber3000Port for the server
--no-openbooleanDon’t open a browser tab (by default preview opens one)
--debugbooleanfalseShow a panel with the parsed document as JSON. Maps only

A mistake in the file shows as an overlay with the field path and message, in place of the map. A status bar at the bottom shows whether the server is connected and which file it shows. Data paths in the YAML resolve against the YAML file’s folder, which the server serves.

preview shows the document alone, not the HTML page you wrote around it. To see your own page, serve the folder instead (for example npx serve .).

Terminal window
npx maplibre-yaml@latest preview map.yaml
npx maplibre-yaml@latest preview map.yaml --port 8080 --no-open

Export a map document to a standalone MapLibre style.json: a file that opens in Maputnik and renders in a plain new maplibregl.Map({ style }), with no maplibre-yaml on the page. Only type: map documents export, in format v1 or v2. Export to style.json explains how to use the result, and Export classes what happens to each part of the document.

Terminal window
npx maplibre-yaml@latest emit <file> [options]
ArgumentRequiredDescription
fileYesPath to a type: map document in YAML, JSON or TOML
OptionAliasTypeDefaultDescription
--out-ostringstdoutWrite the style to this file. A folder (one that exists, or a path ending in /, like dist/) gets style.json inside it
--with-fallbacksbooleanonExport what has a style.json fallback (markers become a symbol layer with pin images; fitTo becomes a fixed camera) and print a warning for each loss
--strictbooleanoffRefuse the export (exit 1) if anything would change what the map shows. Cannot be combined with --with-fallbacks
--targetstringThe maplibre-gl version the style is for, e.g. 5.6.0. Features that version lacks (such as state: below its minimum, or color-relief below 5.6) are reported
--truststringDeprecated, and has no effect: it is no longer listed in --help, and passing it prints a note. Trust gates raw !html in popups and click.emit host events, and neither is part of a style.json, so it never changed the exported file. A later release will remove it
--sprite-basestringAbsolute URL where the folder holding the style will be served. Required, with --out, when the document generates sprite images (markers, images:), because MapLibre rejects relative sprite URLs

Warnings go to stderr, so npx maplibre-yaml@latest emit map.yaml > style.json keeps the JSON clean. A part of the document with no style.json form prints a contract warning, and a part whose look changes prints a lossy warning, which --strict refuses (Reading the report).

Some documents need their data at export time. emit reads it the way the published site would serve it: an https:// URL is fetched, and a local path resolves against the YAML file’s folder, then the current folder and its public/ folder.

  • A classify: layer with computed breaks (method:) has its source’s data loaded, and the breaks are written into the style. If the data can’t be loaded, the export fails and says why.
  • A GeoJSON url: source is written as data: <url>, as the style spec expects, so the exported style loads it.
  • Large-data files: FlatGeobuf and GeoParquet are converted to inline GeoJSON; PMTiles and COG sources, and a Protomaps basemap, become pmtiles:// and cog:// URLs that the page showing the style must register. See Large-data files in an exported style.
Terminal window
npx maplibre-yaml@latest emit map.yaml # print the style
npx maplibre-yaml@latest emit map.yaml --out dist/ # write dist/style.json
npx maplibre-yaml@latest emit map.yaml --strict --out dist/ # fail on any loss (CI)
npx maplibre-yaml@latest emit map.yaml --out dist/ --sprite-base https://maps.example.org/dist/

The last line exports a document with markers: it writes style.json and the sprite files (mlym.json, mlym.png and their @2x versions) into dist/, and the style points at them under https://maps.example.org/dist/.


Rewrite a map or story document in another format: YAML, JSON or TOML. The output is the document as written, not a filled-in copy: no defaults are added, nothing is expanded except what the target can’t hold, and expressions stay inline where they fit.

Terminal window
npx maplibre-yaml@latest convert <file> --to <yaml|json|toml> [options]
ArgumentRequiredDescription
fileYesThe document to convert (.yaml, .yml, .json or .toml). Its extension names its format; with another extension, the content decides
OptionAliasTypeDefaultDescription
--tostringRequired. The format to write: yaml, json or toml. The input’s own format reprints the file
--out-ostringstdoutWrite to this file instead. It may not be the input file
--strictbooleanoffExit 1 and write nothing if anything would be lost
--format-fstringtextjson prints one JSON object on stdout instead (below)

What the output can’t keep is listed on stderr, so npx maplibre-yaml@latest convert map.yaml --to json > map.json keeps the file clean. A loss is a comment, an anchor or merge key (written out in full), or a null left out of TOML where leaving the key out means the same thing. With losses the command still writes the output and exits 0, unless --strict is set. The schema line becomes the target’s own form, and !html becomes {"$html": …} and back, so neither counts as a loss.

The command exits 1 and writes nothing when the input is not a valid document (warnings don’t stop it), when a value can’t be written in the target (a null inside an expression going to TOML, inf or nan going to JSON), or when --out names the input. A file that doesn’t exist exits 2.

Terminal window
npx maplibre-yaml@latest convert map.yaml --to toml # print the TOML
npx maplibre-yaml@latest convert map.yaml --to json --out map.json # write a file
npx maplibre-yaml@latest convert story.yaml --to toml --strict # fail on any loss

JSON & TOML documents › Convert between formats shows a loss report.

With -f json the command prints one JSON object on stdout whatever the outcome, with the same exit codes and nothing on stderr, so a script or an agent reads the losses instead of parsing prose:

{
"success": true,
"from": "yaml",
"to": "toml",
"text": "type = \"map\"\n…",
"losses": [
{ "kind": "comment", "message": "the comment on line 5 is dropped: # whole world", "line": 5, "column": 11 }
],
"errors": []
}
  • text is the converted document; with --out it is written to the file and out (its path) replaces text.
  • Each loss has a kind (comment, anchor, merge or null), a message, and a path or a line and column.
  • On failure success is false and there is no text. errors carries the validation errors, or the values the target can’t hold, with path, message, line and column as validate -f json reports them, plus a code (convert-null, toml-syntax, …). A --strict refusal is one error of code strict, beside the losses that caused it.
Terminal window
npx maplibre-yaml@latest convert map.yaml --to toml -f json

Print the JSON Schema for a block, read from the installed @maplibre-yaml/core package, so it matches the version validate checks against. The schemas describe format v1; there is no JSON Schema for format v2.

Terminal window
npx maplibre-yaml@latest schema [block] [options]
ArgumentRequiredDefaultDescription
blockNomapOne of map, scrollytelling, root, any
OptionAliasTypeDescription
--out-ostringWrite the schema to a file instead of stdout
Terminal window
npx maplibre-yaml@latest schema map # print the map schema
npx maplibre-yaml@latest schema any --out any.schema.json # write a file for offline use

A modeline then points at the written file:

# yaml-language-server: $schema=./any.schema.json

See Editor setup and Using AI agents for what the schemas are used for.


Regenerate the pattern images a Mapzen-classic preset references. The classics are pure style spec, so their whole look rides on *-pattern images; bake derives them reproducibly instead of leaving them as unexplained binaries.

Terminal window
npx maplibre-yaml@latest bake [preset] --out <dir>
PresetWritesFrom
crosshatchearth.png, landuse.png, water.pngTangram’s MIT-licensed crosshatch textures, vendored unmodified in the CLI package (assets/tangram/, with provenance). The hatches are pre-filtered for MapLibre, which samples patterns without mipmaps: twice the period, softened strokes, 32 tones
crosshatch-wallsbuilding-hatch.pngTangram’s light pen hatch for a fill-extrusion-pattern, smoothed along the stroke so walls seen edge-on stay steady. The crosshatch document itself draws plain walls
blueprintgrid.pngA generated drafting grid

With no preset, bake lists them. The docs gallery’s committed images are exactly this output — a test pins them pixel for pixel:

Terminal window
npx maplibre-yaml@latest bake crosshatch --out docs/public/classics/crosshatch
npx maplibre-yaml@latest bake blueprint --out docs/public/classics/blueprint

Compute classify: breaks for a field from a whole local data file, and print them ready to paste. In the browser, computed breaks (method:) need the whole dataset: a FlatGeobuf or PMTiles source read by view never has it, so its layer refuses method: and points here. breaks reads every feature of the file, computes the breaks with the same code the browser uses for a GeoJSON source, and prints a breaks: line to put under classify: in place of method: and classes:.

Terminal window
npx maplibre-yaml@latest breaks <file> --field <name> [options]
ArgumentRequiredDescription
fileYesA local GeoParquet (.parquet), FlatGeobuf (.fgb) or GeoJSON (.geojson, .json) file. The extension names its format. A GeoParquet overview file is read at its full-detail level
OptionAliasTypeDefaultDescription
--fieldstringRequired. The numeric property to classify, as field:
--methodstringquantilequantile, equal or jenks, as method:
--classesnumber5How many classes, 2 to 12, as classes:
--normalizestringDivide the field by this property to break a rate, as normalize:
--multiplynumberMultiply the rate (100000 for “per 100,000”), as multiply:. Needs --normalize
--format-fstringtextjson prints one JSON object on stdout instead: success, file, format, field, method, classes, breaks, min, max, count and noData, or success: false and errors (as convert’s: one { path, message, code } per problem, code naming the exit code, e.g. file-not-found)

The output is a comment naming what was computed, the field’s range, and the breaks:

Terminal window
npx maplibre-yaml@latest breaks districts.fgb --field population --method jenks --classes 5
# population, jenks, 5 classes, from districts.fgb (48 features)
# range: 2063 to 24402
breaks: [4548, 9185, 15975, 20657]

Paste it under the layer’s classify:, beside field::

- id: districts-fill
type: fill
source: districts
classify:
field: population
# population, jenks, 5 classes, from districts.fgb (48 features)
# range: 2063 to 24402
breaks: [4548, 9185, 15975, 20657]
palette: YlGnBu

Features whose value is missing, null, empty or not a number are no data: the comment counts them, and they don’t move the breaks. When ties leave fewer distinct classes than asked (40% of the features at 0 cannot fill five equal-count classes), a third comment line says how many the breaks make.

The command exits 1 when --field is missing, names a property no feature has (it lists the ones they have), or names a text field (it says so and suggests categories:); when a flag is out of range; and when the file is not one of the three formats, including a PMTiles archive, whose tiles hold a generalised subset of the data: run breaks on the file the archive was built from. A file that can’t be read exits 2.

Terminal window
npx maplibre-yaml@latest breaks tracts.parquet --field median_income # quantile, 5 classes
npx maplibre-yaml@latest breaks tracts.geojson --field cases --normalize population --multiply 100000
npx maplibre-yaml@latest breaks points.fgb --field value --method equal --classes 4 -f json

Look at a data file and say what to do with it. prepare reports the file’s facts, prints the one recommended route from the Large data guide’s size-to-format table with its commands (file names filled in), and the YAML source block to paste. It only reads: it writes no data and runs nothing.

Terminal window
npx maplibre-yaml@latest prepare <file> [options]
ArgumentRequiredDescription
fileYesA local path or an http(s) URL: GeoParquet (.parquet), FlatGeobuf (.fgb), GeoJSON (.geojson, .json) or PMTiles (.pmtiles). The file’s first bytes decide its format; the extension is the fallback
OptionAliasTypeDefaultDescription
--format-fstringtextjson prints one JSON object on stdout instead: success, file, facts and recommendation (verdict, route, summary, steps, yaml, alternative, notes), or success: false and errors (as breaks prints them)

What it reads:

  • GeoParquet and FlatGeobuf: only the footer or header, never the features: rows or features, size, geometry, CRS, the bbox column, row groups and rows per group, sort order, and a GeoParquet overview file’s levels. A FlatGeobuf whose header doesn’t record its feature count (some streaming writers leave it out) is routed by its size instead: up to 10 MB it is read whole; above that, the ogr2ogr command re-creates it with an index and the count, to read by view.
  • GeoJSON: the whole file up to 200 MB, to count its features. Above that it reads the first 8 MB and estimates the count from them, and the report calls it an estimate.
  • PMTiles: the header and metadata: tile type, zooms, bounds and the tile layers that source-layer: names. An archive is already tiles, so the answer is “no change needed” and the type: vector (or raster) source.
  • A URL is read with range requests, as a map reading the file by view reads it. A server that ignores them (python -m http.server) is refused rather than downloaded whole; the message says to host the file where range requests work, or to run prepare on a downloaded copy. Each request waits up to 30 seconds for an answer.

The route, from the number of features at full detail:

FeaturesRouteWhat prepare prints
Under 50,000Use it as it is”No change needed” and the source. For a FlatGeobuf without a spatial index or a GeoParquet file not sorted by location, also the command that would make it readable by view, if you want that
50,000 to 250,000Read it by viewFor a sorted GeoParquet file with a bbox column or an indexed FlatGeobuf, “no change needed” and the source with load: viewport. Otherwise the gpio sort hilbert command (GeoParquet), the ogr2ogr index command (FlatGeobuf) or the gpio convert command (GeoJSON) first. For polygons, PMTiles as the alternative
Over 250,000PMTiles (polygons), or an overview GeoParquet file (points and lines)The tylertoo command and the source: type: vector for PMTiles, load: viewport for the overview file, which is experimental. The other route as the alternative

A file in a projected CRS gets the ogr2ogr -t_srs EPSG:4326 command first, and a GeoParquet file with GeoArrow geometry the WKB rewrite; the rest of the route then applies to the file those write. The commands are the ones the map’s own notices print, so a notice and prepare name the same command.

Terminal window
npx maplibre-yaml@latest prepare parcels.geojson
parcels.geojson: GeoJSON, 300 MB
features about 1,480,000 (an estimate, from the first 8.0 MB: the file is over the 200 MB it parses in full)
geometry Polygon
CRS EPSG:4326 (longitude/latitude)
About 1,480,000 polygons, over 250,000: PMTiles draws it at every zoom, citywide included.
Run:
Convert it to PMTiles with tylertoo (the parcel recipe; MapLibre draws zoom 14’s tiles further in):
uvx --from geoparquet-io==1.7.0 gpio convert geoparquet parcels.geojson parcels.parquet
uv run --with tylertoo==0.8.0 python -c "import tylertoo as t; t.overview('parcels.parquet', 'parcels-ov.parquet', min_zoom=0, max_zoom=14, collapse_square=True); t.export_pmtiles('parcels-ov.parquet', 'parcels.pmtiles', layer_name='parcels', tile_size_limit=None)"
Then paste:
sources:
parcels:
type: vector
url: parcels.pmtiles
# layers draw it with source-layer: parcels
…

The block pastes into the document’s sources:, and the url: is relative to the document (for a URL argument, the URL itself):

sources:
parcels:
type: vector
url: parcels.pmtiles
# layers draw it with source-layer: parcels

The command exits 1 with no file argument, a bad --format, a file of none of the four formats (a Parquet file without geometry metadata included), or a server without range requests. A path that can’t be read (missing, a folder, no permission) or a URL that answers 404, can’t be reached, or doesn’t answer within 30 seconds exits 2.

Terminal window
npx maplibre-yaml@latest prepare data/trees.fgb
npx maplibre-yaml@latest prepare https://data.example.org/lots.parquet
npx maplibre-yaml@latest prepare lots-overviews.parquet -f json

A maplibre-yaml.config.json file in the folder you run the CLI from sets default options for validate and preview. A .yaml, .yml, .js or .mjs file of the same name works too.

{
"validate": {
"format": "human",
"strict": false,
"ignorePatterns": ["**/node_modules/**", "**/dist/**", "**/*.backup.yaml"]
},
"preview": {
"port": 4000,
"open": false
}
}

An option you type on the command line wins, then the settings file, then the built-in default. With the file above, preview map.yaml serves on port 4000 without opening a browser, and preview map.yaml --port 5000 serves on 5000. The preview settings need CLI 0.6.1 or later; 0.6.0 ignores them.

SettingTypeDefaultDescription
validate.formatstringhumanOutput format
validate.strictbooleanon in CITreat warnings as errors
validate.ignorePatternsstring[]["**/node_modules/**", "**/dist/**"]Glob patterns to skip. Setting it replaces the default, so list those two again if you still want them skipped
preview.portnumber3000Server port
preview.openbooleantrueOpen a browser tab
preview.debugbooleanfalseShow the debug panel

CodeMeaningCommands
0Success. For emit, the style was written; warnings, if any, are on stderrAll
1Invalid input: validation or syntax errors, warnings in strict mode, an unknown template or option, a non-empty init folder, an emit --strict or convert --strict refusal, a breaks field that is missing or not numeric, a file prepare doesn’t read or a server without range requestsvalidate, emit, convert, init, schema, bake, breaks, prepare
2File not found, no file matched a pattern, a folder held no map or story document, or a prepare URL answered 404, couldn’t be reached or didn’t answer in 30 svalidate, preview, emit, convert, breaks, prepare
4Unexpected error (for emit, for example, a basemap or image that could not be fetched)All

When CI is set (GitHub Actions and other CI services set it), validate runs in strict mode unless you pass --no-strict.

Set it to any value to print debug messages and the stack trace of an unexpected error, for a bug report:

Terminal window
DEBUG=1 npx maplibre-yaml@latest validate "**/*.yaml"

Disable colored output:

Terminal window
NO_COLOR=1 npx maplibre-yaml@latest validate map.yaml

With the CLI installed in the project (npm install --save-dev @maplibre-yaml/cli), scripts can use the short name:

{
"scripts": {
"validate": "mlym validate 'maps/**/*.yaml'",
"validate:watch": "mlym validate 'maps/**/*.yaml' --watch",
"preview": "mlym preview map.yaml",
"export": "mlym emit map.yaml --strict --out dist/"
}
}

validate is strict in CI, so warnings fail the job too:

name: Validate maps
on: [push, pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npx maplibre-yaml@latest validate "**/*.yaml"

SARIF output shows each finding as an annotation on the pull request:

name: Code scanning
on: [push, pull_request]
jobs:
analyze:
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npx maplibre-yaml@latest validate "**/*.yaml" --format sarif > results.sarif
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif

With husky, check the YAML files staged for a commit (it does nothing when there are none):

#!/bin/sh
git diff --cached --name-only --diff-filter=ACM -- '*.yaml' '*.yml' \
| xargs -r npx maplibre-yaml@latest validate

mlym and maplibre-yaml exist only after an install, and npx mlym fails with a 404 without one. Use npx maplibre-yaml@latest, or install the CLI (see Running the CLI). After a global install, check that your PATH includes npm’s global bin folder (npm prefix -g).

  • Quote the pattern, "maps/**/*.yaml", so your shell doesn’t expand it.
  • Patterns are relative to the folder you run the command in.
  • Check validate.ignorePatterns in the project settings file: validate prints Skipped …: excluded by validate.ignorePatterns for a file it skipped.
  • Check that the file matches the pattern you are watching.
  • On Linux, a large tree can exceed the file-watcher limit; raise it (fs.inotify.max_user_watches) or watch a smaller pattern.

The CLI reads maplibre-yaml.config.* from the folder you run it in, not from the YAML file’s folder. An option typed on the command line overrides it.


The command names are listed under Running the CLI.