niva

text-pipeline grammar for QGIS geoprocessing
v0.42.0
load roads.gpkg | reproject EPSG:6346 | buffer 100m dissolve | save corridors.gpkg
built-in engine-handled verbs: load save run filter sql split each call describe search docs show info catalog assess metadata notify email project style remove alias 45 friendly verbs → QGIS native:* algorithms run run <id> KEY=val → any of ~769 algorithms

Built-in verbs

verbwhat it does
loadread a file, URI, or @conn
savewrite layer; mode=create|replace|append
runescape hatch: run <id> KEY=val
filterattribute filter — QGIS expression
sqlSELECT pipes; DDL/DML terminal
splitkeep one geom type: point|line|polygon
eachbatch: once per item in dir/glob/container
callinclude another .niva file
describeshow verb/algorithm signature
searchfuzzy-find verbs + algorithms
docsfull reference for search matches
showlist loadable layers at path/URL/connection
infoQGIS environment report
cataloginventory directory → Markdown report
assessprofile layer data quality (pass-through)
metadatastamp title/abstract/keywords (pass-through)
notifyntfy push notification (pass-through)
emailSMTP email (pass-through)
projectcreate/convert/repoint QGIS projects
styleapply/save .qml|.qmd sidecars (pass-through)
removedelete output + sidecars (safety gate)
figurequick map image (PNG/JPG) of a layer (pass-through)
mapcomposed layout → PDF/PNG (legend, scale, N-arrow)

Vector geometry (alias)

verbkey args / options
buffer<dist> · dissolve · cap=round|flat|square · segments=N · join=
clip<overlay> · clip input to overlay extent
intersect<overlay> · overlap + merge attributes
difference<overlay> · keep parts outside overlay
symdifference<overlay> · XOR of two layers
union<overlay> · split + full attributes
dissolve[field] · separate · merge features
selectloc<overlay> · predicate=intersect|within|…
spatialjoinwith= · predicate= · method=
joinwith= · field= · field2= · fields=
reproject<crs> · convert_curved · transform_z
centroidpolygon → point at center
convexhullminimum convex hull
boundingboxaxis-aligned envelope per feature
minrectminimum-area oriented bounding box
voronoiVoronoi polygons from points
delaunayDelaunay triangulation
simplify<dist> · method=area|distance
smooth[iterations] · offset=
fixgeomrepair invalid geometries
explodemulti-part → single-part features
collectaggregate → multi-geometry
promoteconvert multi-part to single
subdivide<max_verts>
densify<dist> · interval=
offset<dx> <dy> · shift geometry (not buffer)
snap<ref> <tol>
swapxyswap X/Y coordinates
forcerhrforce right-hand-rule orientation
pointonsurfaceguaranteed interior point per polygon
verticesextract all vertices as points
pointsalong<dist> · points at intervals on line
zonalstatsraster= · stats=mean|min|max|…
sample<raster> · extract values at points
countpointscount points in each polygon

Attribute & raster

Attribute
renamefield<old> <new>
dropfields<field>,<field>…
keepfields<field>,<field>…
Raster (GDAL-backed)
warp<crs> · resolution= · resampling=near|bilinear|average
clipraster<mask> · clip raster to polygon
hillshadez_factor= · azimuth= · altitude=
slopepercent flag · from DEM
aspectaspect from DEM
polygonize[field=] · vectorize classified raster

Value types

typeexamples
distance100m 1.5km 500ft 10 (CRS units)
crsEPSG:6346 EPSG:4326
fieldzoning parcel_id
fieldspop,income (comma-sep)
expression"area > 1000" (QGIS expr, quoted)

Run escape hatch

run native:slope INPUT=dem.tif Z_FACTOR=2
Enum params use integer index (0,1,2). GRASS/PDAL use named output params (terminal). Raw LiDAR (LAS/LAZ): run pdalcli:to_raster|height_above_ground|classify_ground|boundary — COPC-free. PDAL needs pdal_wrench (+ QGIS_WRENCH_EXECUTABLE) — bundled in QGIS on Win/macOS, conda install -c conda-forge pdal pdal_wrench on Linux (setup). pdal: needs COPC input; pdal:exportraster FILTER_EXPRESSION drops == (use "Classification > 1.5 and Classification < 2.5"). Learn a verb offline with describe <verb> (mapping, options+defaults, QGIS params, example); for a run <id> algorithm look up docs/algorithms/<provider>.md (describe <id> needs QGIS). Providers: native → gdal → GRASS (last); no SAGA/OTB by default.

Syntax & connections

Syntax per stage
verbfirst token
positionalsordered args
key=valueoptions
flagsbare words, boolean ON
Database connections (@conn)
Credentials stay in QGIS — niva only passes the name.
@connsql @conn "SELECT …"
@conn.tableload @conn.table
@conn.schema.tablesave @conn.schema.table mode=replace
@"name"dotted name quoting

Multi-layer GeoPackage

load "data.gpkg|layername=parcels" | clip aoi.gpkg

Mode values (save/write)

createfail if exists (default, safe)
replacedrop + recreate
appendinsert into existing

Comments & continuation

# this is a comment load roads.gpkg | buffer 100m | save out.gpkg

Common patterns

Buffer roads
load roads.gpkg | buffer 100m dissolve | save corridors.gpkg
Clip to AOI
load parcels.gpkg | clip aoi.gpkg | save clipped.gpkg
Filter + reproject
load roads.gpkg | filter "class = 'primary'" | reproject EPSG:6346 | save primary.gpkg
Spatial join
load incidents.gpkg | spatialjoin with=districts.gpkg predicate=within | save tagged.gpkg
DEM → slope
load dem.tif | slope percent | save slope_pct.tif
DB read → process
sql @pg "SELECT * FROM parcels WHERE area > 5000" | centroid | save centers.gpkg
Cleanup pipeline
load messy.gpkg | fixgeom | reproject EPSG:6346 | explode | save clean.gpkg
Overlay analysis
load parcels.gpkg | intersect floodzone.gpkg | save at_risk.gpkg

Batch & database

Batch with each

each "in/*.shp" | reproject EPSG:6346 | save collected.gpkg
each "rasters/*.tif" | warp EPSG:6346 | save "out/{name}.tif"
Failed items skip; batch continues. {name} per-item output.

SpatiaLite SQL

sql @sl "SELECT id, ST_Buffer(geometry, 50) AS geometry FROM wells"

PostGIS SQL

sql @pg "SELECT a.id, a.geom FROM parcels a JOIN flood b ON ST_Intersects(a.geom, b.geom)"

Write results to DB

load parcels.gpkg | clip aoi.gpkg | save @pg.public.parcels_clip mode=replace

Gotchas & fixes

symptomfix
disk quota exceededset NIVA_TMPDIR to disk-backed dir
could not import QGISrun with QGIS's own Python
segfault after tests passharmless QGIS teardown — check OK/FAILED
table already existsuse mode=replace or mode=append
no such connectionverify name in QGIS Browser / info
unknown optiondescribe <verb> for valid options
unknown verb / invented verbverbs are a closed set — check reference.md §5. stats/contour/index/dtm are NOT verbs → use run <id>
a stage must start with a verbbreak flows between stages with a trailing |; one stage's options stay on ONE line (\ is not continuation)
notify needs a topicset NIVA_NTFY_TOPIC or add to=<topic>
EPSG:4326 + buffer 100= 100 degrees! add unit: buffer 100km

Quick tips

Lint offline (no QGIS): niva validate <flow.niva> — grammar + closed-set verbs (did-you-mean) + alias args/options/enums + run <id> params, then a MockBackend dry-run; errors + style warnings with line numbers. Exit 0 clean / 1 on error. (niva "<flow>" --explain / --dry-run are lighter inline checks.)
run <id> params: look up offline in docs/algorithms/<provider>.md
• Cartography: map → PDF layout (legend/scale/N-arrow), figure → quick PNG (pass-through, chains after save)
• Learn a verb offline: niva describe <verb> — options, defaults, QGIS-param mapping, example
• Log run: NIVA_LOG=./log niva "<flow>"
• Read a specific GPKG layer: "file.gpkg|layername=x"
• Reports to file: describe <verb> to=file.md
save is pass-through — mid-pipe save works
each eats item failures; FlowError aborts
• Offscreen QGIS: QT_QPA_PLATFORM=offscreen

niva cheat sheet — v0.42.0 · github.com/johnzastrow/niva Full docs: docs/guide/reference.md · docs/guide/cookbook.md (90 recipes) · docs/algorithms/ (878 run ids)