Skip to content

Modify a grid

You are viewing in-progress documentation for v2 (Beta). Switch to the stable version for the current production release.

You don’t have to rebuild a grid to change it. POST .../modifications applies rules to a grid’s current stored data and writes the result back under the same id — no re-fetching LANDFIRE, no new resource. Reach for it to reclassify a fuel model, mask fuel under roads and water, or scale a band, once the grid already exists.

The rules you submit are appended to the grid’s list and applied on top of whatever it already holds. To instead apply the same rules as you first build a grid, see Mask a fuel grid with features — same rule shape, applied at create time. For the concepts — conditions, actions, buffer_m, target, and the create-time-vs-in-place distinction — see About modifications.

  1. An API key: my-api-key.

  2. A domain in a projected CRS: your-domain-id. See Create a domain.

  3. A completed FBFM grid and a completed surface-fuel grid. Both come from Build a surface fuel grid from LANDFIRE — record your-fbfm40-grid-id (carries the fbfm band) and your-grid-id (carries fuel_load.*).

  4. Optional — a completed road and water feature in the same domain, for the spatial recipes. See Create road and water features from OpenStreetMap and record your-road-feature-id and your-water-feature-id.

Masking roads and water in one script:

Modify a grid (POST + poll)
import time
import requests
API_KEY = "my-api-key"
DOMAIN_ID = "your-domain-id"
# A completed surface-fuel grid (e.g. from grids/lookup/fbfm40).
GRID_ID = "your-grid-id"
ROAD_FEATURE_ID = "your-road-feature-id"
WATER_FEATURE_ID = "your-water-feature-id"
BASE = "https://api-v2-prod-nyvjyh5ywa-uw.a.run.app"
HEADERS = {"api-key": API_KEY}
def poll(grid_id: str) -> dict:
while True:
r = requests.get(
f"{BASE}/domains/{DOMAIN_ID}/grids/{grid_id}", headers=HEADERS
).json()
if r["status"] in ("completed", "failed"):
return r
time.sleep(5)
before = requests.get(
f"{BASE}/domains/{DOMAIN_ID}/grids/{GRID_ID}", headers=HEADERS
).json()
# Mask dead surface fuel under roads OR water: one rule per feature (a union
# needs multiple rules — conditions inside one rule are ANDed).
zero_dead_fuel = [
{"band": "fuel_load.1hr", "modifier": "replace", "value": 0},
{"band": "fuel_load.10hr", "modifier": "replace", "value": 0},
{"band": "fuel_load.100hr", "modifier": "replace", "value": 0},
]
grid = requests.post(
f"{BASE}/domains/{DOMAIN_ID}/grids/{GRID_ID}/modifications",
headers=HEADERS,
json={
"modifications": [
{
"conditions": [
{"source": "feature", "operator": "intersects",
"feature_id": ROAD_FEATURE_ID, "target": "cell"}
],
"actions": zero_dead_fuel,
},
{
"conditions": [
{"source": "feature", "operator": "intersects",
"feature_id": WATER_FEATURE_ID, "target": "cell"}
],
"actions": zero_dead_fuel,
},
]
},
).json()
print(grid["status"]) # -> pending
print(before["checksum"], "->", grid["checksum"]) # rotated immediately
grid = poll(GRID_ID)
print(grid["status"], len(grid["modifications"])) # -> completed 2

The rest of this page walks the individual recipes.

The simplest rule: match cells by band value, replace the value. Here every GR1 (101) cell in the FBFM grid becomes GR2 (102) — one condition, one action, no spatial filter, so it applies grid-wide.

POST grids/{id}/modifications
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/grids/your-fbfm40-grid-id/modifications' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"modifications": [
{
"conditions": [
{ "band": "fbfm", "operator": "eq", "value": 101 }
],
"actions": [
{ "band": "fbfm", "modifier": "replace", "value": 102 }
]
}
]
}'

The 200 comes back pending with a freshly rotated checksum — the edit runs server-side. Note the modifications array is still empty in this response; the submitted rule lands there once the job completes (see Poll to completion). On this domain the rule reclassifies all 21 GR1 cells, leaving zero 101 cells and 113 102 cells.

Add a spatial condition and the rule narrows to the intersection — the change now applies only where both conditions hold. This reclassifies GR1→GR2 only inside a polygon (here, the western half of the domain). Conditions in one rule are ANDed, so a cell must be GR1 and inside the polygon.

Reclassify GR1→GR2 inside a polygon
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/grids/your-fbfm40-grid-id/modifications' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"modifications": [
{
"conditions": [
{ "band": "fbfm", "operator": "eq", "value": 101 },
{
"source": "geometry",
"operator": "within",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[-114.085846, 48.054491],
[-114.075000, 48.054491],
[-114.075000, 48.070509],
[-114.085846, 48.070509],
[-114.085846, 48.054491]
]
]
},
"crs": { "type": "name", "properties": { "name": "EPSG:4326" } }
}
],
"actions": [
{ "band": "fbfm", "modifier": "replace", "value": 102 }
]
}
]
}'

Now only 17 of the 21 GR1 cells reclassify — the ones inside the polygon — leaving 4 GR1 cells to the east untouched. Dropping the fbfm condition would reclassify everything in the polygon; dropping the geometry condition reclassifies GR1 everywhere. Each condition narrows the last.

Roads and open water carry no surface fuel. To zero the dead-fuel bands wherever a road or a water feature touches a cell, use two rules — one per feature. This is the single most important pattern on the page.

Zero dead fuel under roads or water
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/grids/your-grid-id/modifications' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"modifications": [
{
"conditions": [
{ "source": "feature", "operator": "intersects", "feature_id": "your-road-feature-id", "target": "cell" }
],
"actions": [
{ "band": "fuel_load.1hr", "modifier": "replace", "value": 0 },
{ "band": "fuel_load.10hr", "modifier": "replace", "value": 0 },
{ "band": "fuel_load.100hr", "modifier": "replace", "value": 0 }
]
},
{
"conditions": [
{ "source": "feature", "operator": "intersects", "feature_id": "your-water-feature-id", "target": "cell" }
],
"actions": [
{ "band": "fuel_load.1hr", "modifier": "replace", "value": 0 },
{ "band": "fuel_load.10hr", "modifier": "replace", "value": 0 },
{ "band": "fuel_load.100hr", "modifier": "replace", "value": 0 }
]
}
]
}'

Each rule’s condition is a feature reference with target: "cell" — match every cell the polygon touches — and its actions zero fuel_load.1hr/10hr/100hr. On this domain the two rules together zero fuel_load.1hr in 286 additional cells (1,686 → 1,972 zero-valued cells of 3,127).

The ANDing that makes the road-or-water mistake a trap is exactly what you want when the conditions should compound. This zeroes the heavy 100-hour load only where it is both already high and inside a road buffer — leaving light roadside fuel alone.

Zero heavy 100-hr load inside a road buffer
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/grids/your-grid-id/modifications' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"modifications": [
{
"conditions": [
{ "band": "fuel_load.100hr", "operator": "gt", "value": 0.5 },
{ "source": "feature", "operator": "intersects", "feature_id": "your-road-feature-id", "buffer_m": 10, "target": "cell" }
],
"actions": [
{ "band": "fuel_load.100hr", "modifier": "replace", "value": 0 }
]
}
]
}'

Of the 195 cells whose fuel_load.100hr exceeds 0.5 kg/m**2, only the 87 that also fall within the 10 m road buffer are zeroed. The attribute condition and the spatial condition each cut the selection down.

The checksum rotates the instant you POST, but the data is written and the modifications list is updated only when the job completes. Poll the grid:

GET grid status
curl -X 'GET' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/grids/your-fbfm40-grid-id' \
-H 'accept: application/json' \
-H 'api-key: my-api-key'

Status walks pending → running → completed. When it lands, the submitted rule appears in modifications and the edited bands are in storage:

{
"id": "your-fbfm40-grid-id",
"domain_id": "your-domain-id",
"name": "FBFM40 fuel model (LANDFIRE 2024)",
"description": "",
"status": "completed",
"progress": {
"percent": 100,
"message": "Complete"
},
"created_on": "2026-07-09T19:26:17.776315Z",
"modified_on": "2026-07-09T19:26:26.563860Z",
"checksum": "1cbc63df401b49589bd00a0507278a23",
"source": {
"version": "2024",
"extent_buffer_cells": 0,
"remove_non_burnable": null,
"product": "fbfm40",
"name": "landfire",
"description": "LANDFIRE FBFM40 fuel model codes (Scott-Burgan 40 classification)",
"alignment": {
"target": "domain",
"method": null,
"resolution": null
}
},
"modifications": [
{
"conditions": [
{
"band": "fbfm",
"operator": "eq",
"value": 101
}
],
"actions": [
{
"band": "fbfm",
"modifier": "replace",
"value": 102
}
]
}
],
"bands": [
{
"key": "fbfm",
"type": "categorical",
"unit": null,
"index": 0,
"nodata": 32767
}
],
"georeference": {
"crs": "EPSG:32611",
"transform": [
29.749120343527053, 0.0, 717140.0, 0.0, -29.749120343523888,
5328279.198100268
],
"shape": [59, 53]
},
"error": null,
"chunks": {
"shape": [512, 512],
"count": 1,
"count_by_axis": {
"y": 1,
"x": 1
}
},
"tags": []
}

The rotated checksum is the staleness signal: any resource derived from this grid — a resample, an FBFM lookup, an export — can compare checksums and tell its input changed. To keep the pre-edit grid around, branch it first.

In-place modification edits the grid under its own id — the pre-edit state is gone. When you’d rather keep the original and explore a variant, duplicate first, then modify the copy.

POST .../duplicate makes a byte-for-byte clone: it copies the stored array (it does not re-derive from LANDFIRE), carrying the grid’s source, modifications, and checksum over verbatim.

POST grids/{id}/duplicate
curl -X 'POST' \
'https://api-v2-prod-nyvjyh5ywa-uw.a.run.app/domains/your-domain-id/grids/your-grid-id/duplicate' \
-H 'accept: application/json' \
-H 'api-key: my-api-key' \
-H 'Content-Type: application/json' \
-d '{
"name": "Masked fuelscape (scenario B)",
"tags": ["scenario-b"]
}'

The 201 mints a new grid id — record it: id-of-the-duplicated-grid. The copy starts pending while the background copy runs, but its checksum already equals the source’s and its modifications are carried over intact. Poll it to completed:

{
"id": "id-of-the-duplicated-grid",
"domain_id": "your-domain-id",
"name": "Masked fuelscape (scenario B)",
"description": "",
"status": "completed",
"progress": null,
"created_on": "2026-07-09T19:35:31.254943Z",
"modified_on": "2026-07-09T19:35:31.619784Z",
"checksum": "891609fa22734553b55f3fe620ebd4e4",
"source": {
"table": "fbfm40",
"name": "lookup",
"source_grid_checksum": "c07b83217e914891b027d8b3f3aa7b5c",
"source_grid_id": "your-fbfm40-grid-id",
"source_band": "fbfm"
},
"modifications": [
{
"conditions": [
{
"source": "feature",
"operator": "intersects",
"feature_id": "your-road-feature-id",
"buffer_m": null,
"target": "cell"
}
],
"actions": [
{
"band": "fuel_load.1hr",
"modifier": "replace",
"value": 0
},
{
"band": "fuel_load.10hr",
"modifier": "replace",
"value": 0
},
{
"band": "fuel_load.100hr",
"modifier": "replace",
"value": 0
}
]
},
{
"conditions": [
{
"source": "feature",
"operator": "intersects",
"feature_id": "your-water-feature-id",
"buffer_m": null,
"target": "cell"
}
],
"actions": [
{
"band": "fuel_load.1hr",
"modifier": "replace",
"value": 0
},
{
"band": "fuel_load.10hr",
"modifier": "replace",
"value": 0
},
{
"band": "fuel_load.100hr",
"modifier": "replace",
"value": 0
}
]
}
],
"bands": [
{
"key": "fuel_load.1hr",
"type": "continuous",
"unit": "kg/m**2",
"index": 0,
"nodata": null
},
{
"key": "fuel_load.10hr",
"type": "continuous",
"unit": "kg/m**2",
"index": 1,
"nodata": null
},
{
"key": "fuel_load.100hr",
"type": "continuous",
"unit": "kg/m**2",
"index": 2,
"nodata": null
},
{
"key": "fuel_load.live_herb",
"type": "continuous",
"unit": "kg/m**2",
"index": 3,
"nodata": null
},
{
"key": "fuel_load.live_woody",
"type": "continuous",
"unit": "kg/m**2",
"index": 4,
"nodata": null
},
{
"key": "fuel_depth",
"type": "continuous",
"unit": "m",
"index": 5,
"nodata": null
}
],
"georeference": {
"crs": "EPSG:32611",
"transform": [
29.749120343527053, 0.0, 717140.0, 0.0, -29.749120343523888,
5328279.198100268
],
"shape": [59, 53]
},
"error": null,
"chunks": {
"shape": [512, 512],
"count": 1,
"count_by_axis": {
"y": 1,
"x": 1
}
},
"tags": ["scenario-b"]
}

Same checksum, same source, same modifications — only the id, name, and tags differ. Now apply any recipe above to id-of-the-duplicated-grid and only the copy changes; the original keeps the checksum shown here. Two things to watch:

  • Using the copy before it’s completed. The clone is pending until the background copy finishes; its data endpoints 422 until then. Poll first.

    {
    "detail": "grids/id-of-the-duplicated-grid status is 'pending', expected 'completed'."
    }
  • Duplicating a non-completed grid. The source must be completed — there’s nothing stable to copy until it finishes building.

    {
    "detail": "grids/your-grid-id status is 'pending', expected 'completed'."
    }

If processing fails, the grid lands in status: failed with an error message, and its stored data is left unchanged — a failed in-place edit never partially rewrites the grid. The rules you submitted stay queued on the grid, so once you’ve addressed the cause you can POST the same (or a corrected) modification again to retry.

  • One rule for two features (AND vs OR). The number-one mistake. A road condition and a water condition in one rule match only cells inside both — almost none. Use one rule per feature, as in Mask fuel under roads or water.

  • Bare linestrings and target: "centroid". Persisted OSM road features are widened to polygons, so target: "cell" catches every cell they touch with no buffer. But an inline LineString has zero area, and the default target: "centroid" reduces each cell to its center point — a thin geometry slips between centroids and matches almost nothing. Use target: "cell" and a non-zero buffer_m for narrow shapes.

  • Referencing a band the grid doesn’t have. The rule is validated against the grid’s bands up front — a missing band fails fast with a 422, and the grid is left untouched:

    {
    "detail": "Source grid your-grid-id is missing required bands: ['fbfm']. Available bands: ['fuel_depth', 'fuel_load.100hr', 'fuel_load.10hr', 'fuel_load.1hr', 'fuel_load.live_herb', 'fuel_load.live_woody']"
    }
  • 3D voxel grids are rejected. In-place modification is a 2D operation; a 3D canopy (voxel) grid returns a 422. Edit the source tree inventory and re-voxelize instead:

    {
    "detail": "Modifications are not supported for 3D voxel grids. Apply modifications to the source tree inventory and re-voxelize instead."
    }