Skip to content

Steps by example

This page runs every step type on the Pondsworth sample data and shows what comes out. Each example is a pipeline in pipelines/pondsworth.yaml (from duck-soup tutorial) with the same name as its section, using the shared source list and working_crs: EPSG:32631. The YAML below shows only the parts that differ.

How to read the diagrams:

base features (the rows flowing through) the other source the step uses a feature with no match input shown for comparison

The tables are the actual output, with NULL for empty values. In the editor, every step here is a card in the add step gallery; see Steps in the editor.

Joins: add columns from another source

spatial_join

Copies columns from the feature that a base feature touches, contains or lies within. Use it to answer "which district is each place in?".

base: places
steps:
- type: spatial_join
  source: districts
  predicate: intersects
  fields: {district: district_name}

BeforeAfterOld PuddletonMallard QuayFeatherhillReedmoor

After: each place now carries the name of the district it's in. The Old Lighthouse, outside every district, gets none.

name district
Mallard Museum Old Puddleton
Puddle Lane Café Old Puddleton
Quackington Library Old Puddleton
Soggy Crust Café Mallard Quay
Duckling Swim School Mallard Quay
Breadcrumb Hill Featherhill
Featherhill Library Featherhill
Museum of Rubber Ducks Reedmoor
Reedmoor Pond Café Reedmoor
Old Duck House Featherhill
Old Lighthouse NULL
  • fields maps new column name → column in the joined source: here, the district's district_name arrives as district.
  • Every base feature is kept. The Old Lighthouse is outside every district, so its district is NULL (the red ring). Add a filter afterwards if you only want matches.
  • predicate is read from the base feature's side: intersects (touches or overlaps), within (base feature inside the source feature), contains (base feature contains the source feature). For points in polygons, intersects and within give the same result.

When a feature matches several: on_multiple

Central Puddle Park overlaps all four districts. By default (match: first) a base feature still gets exactly one match, and on_multiple decides which:

base: parks
steps:
- type: spatial_join
  source: districts
  fields: {district_first: district_name}
- type: spatial_join
  source: districts
  on_multiple: largest_overlap
  fields: {district_largest: district_name}

BeforeAfterfirstlargest_overlap

After: the district each option picks for Central Puddle Park. first takes Old Puddleton (dashed), largest_overlap takes Mallard Quay (filled).

name district_first district_largest
Central Puddle Park Old Puddleton Mallard Quay
Willow Wallow Featherhill Featherhill
Kingfisher Meadow Mallard Quay Mallard Quay
Bulrush Fields Reedmoor Reedmoor
  • on_multiple: first (the default) takes the matching district that comes first in the source file: Old Puddleton. It's deterministic, but not meaningful.
  • on_multiple: largest_overlap takes the district with the biggest shared area: most of Central Puddle Park is in Mallard Quay.
  • In the editor, this is the spatial join's when several match field.

Keeping every match: match: all

base: parks
steps:
- type: spatial_join
  source: districts
  match: all
  fields: {district: district_name}
name district
Bulrush Fields Reedmoor
Central Puddle Park Featherhill
Central Puddle Park Mallard Quay
Central Puddle Park Old Puddleton
Central Puddle Park Reedmoor
Kingfisher Meadow Mallard Quay
Willow Wallow Featherhill

With match: all, a base feature is repeated once per match, so Central Puddle Park appears four times. The geometry isn't cut: each copy is the whole park. To split it along the district boundaries, use intersect_overlay instead.

attribute_join

Looks up a value in another table by matching columns, like a spreadsheet VLOOKUP. No geometry is involved, so the other source can be a plain CSV.

base: places
steps:
- type: attribute_join
  source: categories
  left: category
  right: code
  fields: {category_label: label}
name category category_label
Mallard Museum MUS Museum
Puddle Lane Café CAF Café
Quackington Library LIB Library
Soggy Crust Café CAF Café
Duckling Swim School SCH School
Breadcrumb Hill PRK Park
Featherhill Library LIB Library
Museum of Rubber Ducks MUS Museum
Reedmoor Pond Café CAF Café
Old Duck House '' NULL
Old Lighthouse LND NULL
  • A row matches when left (evaluated on the base row) equals right (a column in the lookup source). left is a SQL expression, so upper(category) or a literal like 'MUS' also work.
  • Rows without a match keep NULL: the Old Duck House's category is empty, and LND isn't in the CSV.
  • Each row picks up at most one match. If the lookup column has duplicates, which one is used isn't defined, so make it unique.
  • Matching is exact, including case. For a single translated column, a file codelist is an alternative that can ignore case.

nearest_neighbor

Copies columns from the closest feature in another source, whether or not they touch.

base: places
steps:
- type: nearest_neighbor
  source: stops
  max_distance: 300
  distance_field: stop_distance_m
  fields: {nearest_stop: stop_name}

BeforeAfter

After: each place is linked to its nearest stop. The Old Lighthouse has no stop within 300 m.

name nearest_stop stop_distance_m
Mallard Museum Crumb Market 108.2
Puddle Lane Café Nest Gate 143.2
Quackington Library Nest Gate 153
Soggy Crust Café Webfoot Lane 108.2
Duckling Swim School Webfoot Lane 126.5
Breadcrumb Hill Feather Road 156.2
Featherhill Library Feather Road 197
Museum of Rubber Ducks Reed Loop 122.1
Reedmoor Pond Café Paddle Boat Pier 164
Old Duck House Reed Loop 200
Old Lighthouse NULL NULL
  • distance_field adds a column with the distance, in working-CRS units (metres here): how far each duck has to waddle to its stop.
  • max_distance limits the search: the Old Lighthouse's nearest stop is over 300 m away, so it gets NULL. Setting a radius also makes the step much faster on large data, because without it every base feature is compared with every source feature.
  • Distances need a projected working CRS; duck soup refuses to measure in degrees.

intersect_overlay

Cuts base features along the boundaries of another source, producing one row per overlapping pair with the shared area as its geometry.

base: parks
steps:
- type: intersect_overlay
  source: districts
  fields: {district: district_name}

BeforeAfter

After: Central Puddle Park is cut into four pieces along the district borders. The other parks each lie in one district and stay whole.

name district area_m2
Bulrush Fields Reedmoor 36000
Central Puddle Park Featherhill 5600
Central Puddle Park Mallard Quay 16800
Central Puddle Park Old Puddleton 9600
Central Puddle Park Reedmoor 9800
Kingfisher Meadow Mallard Quay 34000
Willow Wallow Featherhill 22400
  • Central Puddle Park becomes four pieces, one per district, each carrying that district's name. Use this when you need "how much park is in each district?".
  • Base features that overlap nothing are dropped, and pairs that only touch along an edge are ignored.
  • The row count can grow quickly when many features overlap.

line_overlay

Cuts lines where other lines lie on top of them, and copies those lines' attributes onto the shared stretches. Everything else is kept, so the whole network comes out again and can go into the next line_overlay.

Here the footpath network collects attributes from two datasets: the lamp register (lit_paths, which has gaps) and the boardwalk survey (boardwalks, just a few stretches).

base: paths
steps:
- type: line_overlay
  source: lit_paths
  fields: {lamps: lamps}
  tolerance: 0.1
- type: line_overlay
  source: boardwalks
  fields: {material: material}
  tolerance: 0.1

BeforeAfter

footpaths lit (lit_paths) boardwalk (boardwalks)

Each gap in After is where one row ends and the next begins: 10 pieces in all. Mill Lane and Waddle Bridge only end on Riverside Walk, so it has no gap there.

name lamps material length_m
Mill Lane gas NULL 266
Reed Way LED NULL 256
Reed Way NULL NULL 563.1
Reed Way NULL recycled plastic 204.8
Riverside Walk NULL NULL 153.6
Riverside Walk LED NULL 307.1
Riverside Walk LED oak 204.8
Riverside Walk LED NULL 102.5
Riverside Walk NULL NULL 255.9
Waddle Bridge NULL oak 140
  • Riverside Walk is lit in the middle, and part of that lit stretch is also a boardwalk, so it comes out as five pieces: unlit, lit, lit and oak, lit, unlit. Each piece is its own row, and neighbouring pieces meet exactly.
  • Every metre of the network is still there: the pieces add up to the original paths. The geometry is always the footpath's own; the other two datasets only decide where the cuts go and what each piece gets.
  • Waddle Bridge ends on Riverside Walk and Reed Way, but they are not cut there: lines that only meet or cross don't count as lying on each other.
  • tolerance: 0.1 is needed here. The lamp register and the boardwalk survey were digitised separately, and their lines lie a few centimetres off the footpaths. Without a tolerance nothing would match, and nothing would get lamps or material. With it, lines within 10 cm of each other count as the same line, and a lamp line stopping 3 cm before the end of Mill Lane still lights all of it.

Geometry: change the shapes

make_points

Makes point geometry from two coordinate columns, so a table without geometry can be joined, buffered and mapped like any other layer.

base: sightings
steps:
- type: make_points
  x: lon
  y: lat
  crs: EPSG:4326
  rejects: unplaced_sightings
- type: spatial_join
  source: districts
  fields: {district: district_name}
species seen district
Mallard 2026-04-02 Old Puddleton
Mute swan 2026-04-03 Reedmoor
Teal 2026-04-05 Mallard Quay
Mallard 2026-04-09 Featherhill
  • The sightings come from a CSV with lon and lat as text. make_points casts them to numbers and builds the points in EPSG:4326, reprojected to the working CRS, so the spatial_join after it can find each sighting's district.
  • The Goosander came without coordinates. It gets no point, and with rejects it goes to the unplaced_sightings layer instead of on down the chain.
  • A CSV whose coordinates are already numbers doesn't need this step: set x_field and y_field on the source. make_points is for coordinates that turn up partway through a pipeline, say from an http_lookup geocoder.

buffer

Grows each geometry by a distance (or shrinks polygons, with a negative distance).

base: stops
steps:
- {type: buffer, distance: 150}

BeforeAfter

After: each stop is a 150 m circle.

  • The distance is in working-CRS units: 150 m here, roughly how far a duck will waddle for a bus. Like other distance operations, it needs a projected working CRS.
  • Points become circles and lines become corridors. Attributes are unchanged.
  • To buffer a copy of the data and keep the originals, use a snapshot branch or a derived source with buffer:.

centroid

Replaces each geometry with its centre point.

base: parks
steps:
- {type: centroid}

BeforeAfter

After: each park is replaced by its centre point. The dashed outlines are the parks they came from.

Useful for labelling, or for joining polygons to other polygons by "where is its middle?" instead of by overlap. For a curved or C-shaped polygon, the centroid can fall outside the shape itself.

clip

Keeps only the part of each feature that lies inside a mask.

derived_sources:
- {id: old_puddleton, from: districts, where: "district_name = 'Old Puddleton'"}
base: parks
steps:
- {type: clip, source: old_puddleton}

BeforeAfter

After: only the corner of Central Puddle Park inside Old Puddleton (teal, dashed) is left. The dashed grey parks are dropped.

name area_m2
Central Puddle Park 9600
  • Here the mask is a derived source: the districts source filtered to Old Puddleton, created without adding a step to the main chain.
  • Only Central Puddle Park reaches into Old Puddleton, so only that corner of it survives. Features outside the mask are dropped entirely.
  • If a feature overlaps several mask features, it's clipped by the first one it matches. To clip to an area made of several polygons, use a mask that is a single feature: filter it down to one (as here), or dissolve it in a separate pipeline and use that output layer as the mask source.

erase

Cuts away the part of each feature that lies inside another source. It's the opposite of clip.

base: parks
steps:
- {type: erase, source: river}

BeforeAfter

After: the river's strip is cut out of Central Puddle Park, leaving it in two parts. The other parks don't touch the river.

name area_m2
Central Puddle Park 28600
Willow Wallow 22400
Kingfisher Meadow 34000
Bulrush Fields 36000
  • The River Waddle runs through Central Puddle Park, so the park loses that strip (its area drops from about 41,800 m² to 28,600 m², the missing third was river all along) and may become a multi-part polygon.
  • Features that don't touch the river pass through unchanged. A feature erased completely is dropped.
  • Unlike clip, erase subtracts all the matching features at once.

dissolve

Merges features that share the same values, combining their geometries into one.

base: districts
steps:
- type: dissolve
  by:
  - ward
  aggregates:
  - {from: population, how: sum}
  - {to: districts, from: district_name, how: all}

BeforeOld PuddletonMallard QuayFeatherhillReedmoorAfterUpstreamDownstream

After: the four districts are merged into their two wards.

ward population districts area_ha
Downstream 10500 Featherhill, Reedmoor 40
Upstream 20500 Mallard Quay, Old Puddleton 40
  • The four districts become the two wards, Upstream and Downstream. Leave by out to merge everything into a single feature (a town boundary, for example).
  • Only the by keys and the aggregates are kept, since the other columns have no single value per ward. An aggregate says how to combine them: how: sum adds up the districts' populations, and how: all lists their names. most_common, average, min, max, count and others work the same way, and so does a SQL aggregate in expr. See Keeping other columns.
  • A key can also be a SQL expression with a name, such as {to: initial, expr: "left(district_name, 1)"}, to group by a computed value.

combine_lines

Joins lines that meet end to end into longer lines, one per connected chain with the same values.

base: paths
steps:
- type: line_overlay
  source: lit_paths
  fields: {lamps: lamps}
  tolerance: 0.1
- type: line_overlay
  source: boardwalks
  fields: {material: material}
  tolerance: 0.1
- type: combine_lines
  by:
  - name
  - lamps
  aggregates:
  - {to: pieces, how: count}
  - {to: materials, from: material, how: all}

BeforeAfter

Before is what the step gets: the 10 pieces of the two line_overlay steps, a gap between each. After, pieces of the same path with the same lighting that meet end to end are one line again: 7 lines.

name lamps pieces materials length_m
Mill Lane gas 1 NULL 266
Reed Way LED 1 NULL 256
Reed Way NULL 2 recycled plastic 767.9
Riverside Walk NULL 1 NULL 153.6
Riverside Walk LED 3 oak 614.3
Riverside Walk NULL 1 NULL 255.9
Waddle Bridge NULL 1 oak 140
  • The two line_overlay steps cut the paths into 10 pieces, by lighting and by boardwalk. Combining by name and lamps rejoins the pieces only the boardwalks split, giving 7 lines. The lit stretch of Riverside Walk is one line again, made of 3 pieces.
  • Riverside Walk's two unlit stretches stay two features: they don't touch. dissolve would have merged them into one multi-part line.
  • The aggregates are computed per chain, so pieces counts the pieces in each line, and materials lists the boardwalks it crosses.
  • Lines are joined only where one ends and the next begins, the way they run. Set preserve_direction: false to let a line be reversed to fit.

split_lines

Cuts lines where they cross or touch each other, so every junction becomes an end point: the noding a network needs before routing or counting segments.

base: paths
steps:
- {type: split_lines, tolerance: 0.1}

BeforeAfter

Each gap in After is a cut: Riverside Walk where Mill Lane and Waddle Bridge meet it, and Reed Way where the bridge ends on it.

name length_m
Riverside Walk 307.2
Riverside Walk 307.2
Riverside Walk 409.6
Mill Lane 266
Reed Way 614.3
Reed Way 409.6
Waddle Bridge 140
  • Mill Lane ends on Riverside Walk, and Waddle Bridge starts on it, so Riverside Walk is cut in two places and comes out as 3 pieces. The lines that only end there aren't cut.
  • Waddle Bridge was meant to end on Reed Way, but after the trip through longitude and latitude its end misses by millimetres, as separately digitised lines usually do. tolerance: 0.1 cuts Reed Way there anyway; without it, Reed Way stays in one piece.
  • Each piece keeps its line's attributes, and the pieces are the line's own geometry, so neighbouring pieces share their end points exactly. Give source to cut at another layer's lines or polygon edges instead (paths split at district borders, say).

clean_coverage

Makes polygons that should fit edge to edge actually fit. The reed beds were surveyed zone by zone: Sedge Shallows reaches 1.2 m into Cattail Corner, and Cattail Corner stops up to a metre short of Reedy Reach, leaving a thin gap between them.

base: reedbeds
steps:
- {type: clean_coverage, max_gap_width: 3}
name area_m2 overlap_m2
Sedge Shallows 19263 0
Cattail Corner 18842 0
Reedy Reach 19495 0
  • The overlap is gone (overlap_m2 is the area each zone shares with the others), and the gap, narrower than max_gap_width, is filled. The three zones now cover their 57 600 m² exactly once.
  • A gap is a hole enclosed by the polygons; an inlet open to the outside isn't filled. snap_distance also snaps edges that are nearly, but not exactly, on top of each other.

simplify

Removes vertices, keeping every edge within tolerance of the original.

base: reedbeds
steps:
- {type: clean_coverage, max_gap_width: 3}
- {type: simplify, tolerance: 6, shared_boundaries: true}

BeforeAfter

The dots are the vertices. After simplifying, each wiggly edge keeps a fraction of them, and the edge two zones share is still a single edge.

name vertices area_m2
Sedge Shallows 18 19261
Cattail Corner 31 18855
Reedy Reach 18 19484
  • The reed beds' wiggly edges have a vertex every 5 metres. Simplified with a 6 m tolerance, each zone keeps a fraction of them.
  • shared_boundaries: true simplifies the zones together, so an edge two of them share is simplified once and stays shared: the areas still add up to 57 600 m², with no gaps or slivers between the zones. Simplified one at a time, the two sides of a shared edge would come out differently.
  • Shared-boundary simplification needs polygons that don't overlap, which is why clean_coverage runs first. keep_outer_boundary: true would leave the outer edge of the reed beds as it is.

snap_to_grid

Rounds every vertex to a grid, here a coarse 50 m one so the jump is visible.

base: places
steps:
- {type: snap_to_grid, grid_size: 50}

BeforeAfter

After: each place has moved to the nearest point of a 50 m grid. The grey dots are where they were.

name x y
Mallard Museum 200 650
Puddle Lane Café 300 700
Quackington Library 400 600
Soggy Crust Café 650 700
Duckling Swim School 800 650
Breadcrumb Hill 150 200
Featherhill Library 350 100
Museum of Rubber Ducks 700 150
Reedmoor Pond Café 900 200
Old Duck House 450 250
Old Lighthouse 1250 500
  • Each place moves to the nearest grid point; the grey dots are where they were. A grid of 0.01 (centimetres) is the usual choice, to drop the spurious decimals a reprojection leaves behind.
  • The geometry stays valid. A polygon narrower than a grid cell collapses to an empty geometry, and its row is kept.

fill_holes

Removes the holes in polygons, or with max_area only the small ones.

base: districts
steps:
- {type: erase, source: parks}
- {type: fill_holes, max_area: 30000}

BeforeAfter

Before: the parks (teal) are erased from the districts. After: the small hole in Featherhill is filled (dashed); the two larger ones stay. The square in the middle isn't a hole: Central Puddle Park took a corner off each of four districts.

district_name holes area_ha
Old Puddleton 0 19.04
Mallard Quay 1 14.92
Featherhill 0 19.44
Reedmoor 1 15.42
  • Erasing the parks punches a hole in three districts (Central Puddle Park, on the corner, only takes a bite out of the edges). With max_area: 30000, Willow Wallow's 22 400 m² hole in Featherhill is filled; Kingfisher Meadow's and Bulrush Fields' are larger and stay.
  • Without max_area, every hole is filled.

eliminate_slivers

Merges small polygons into a neighbour. Overlaying the parks with the districts cuts Central Puddle Park, which straddles the town centre, into four pieces; the one in Featherhill (5 600 m², teal below) is too small to keep.

base: parks
steps:
- type: intersect_overlay
  source: districts
  fields: {district: district_name}
- {type: eliminate_slivers, max_area: 6000}

BeforeAfter

Before: the pieces the overlay cuts the parks into, the sliver in teal. After: it's part of the Old Puddleton piece above it.

name district area_m2
Bulrush Fields Reedmoor 36000
Central Puddle Park Mallard Quay 16800
Central Puddle Park Old Puddleton 15200
Central Puddle Park Reedmoor 9800
Kingfisher Meadow Mallard Quay 34000
Willow Wallow Featherhill 22400
  • The sliver merges into the neighbour it shares the longest border with: the Old Puddleton piece, which grows from 9 600 to 15 200 m². The sliver's row is gone; the neighbour keeps its attributes. merge_into: largest picks the largest neighbour instead.
  • where picks the polygons by a condition, alone or together with max_area. A sliver with no neighbour to merge into stays as it is.

explode

Splits multipart features into a row per part.

base: parks
steps:
- type: dissolve
  by:
  - kind
- {type: explode, part_field: part}
kind part area_m2
meadow 1 36000
meadow 2 34000
park 1 22400
park 2 41800
  • The dissolve by kind makes two multipolygons: the two meadows and the two parks, none of them touching. explode turns each into its parts again, with the feature's attributes, and part_field numbers them.
  • A feature that is already a single part stays one row.

Rows: keep some, add more, or set a copy aside

filter

Drops the rows where a SQL condition is false.

base: places
steps:
- type: spatial_join
  source: districts
  fields: {district: district_name}
- {type: filter, where: "district IS NOT NULL AND bread_crumbs >= 20000"}

BeforeAfter

After: the places that pass the condition. The grey ones are dropped.

name district bread_crumbs
Mallard Museum Old Puddleton 52000
Quackington Library Old Puddleton 75000
Breadcrumb Hill Featherhill 30000
Featherhill Library Featherhill 21000
  • The condition is any DuckDB SQL expression that returns true/false, using the columns available at that point in the chain, including ones pulled in by earlier steps (district here).
  • Rows where the condition is NULL are dropped too. Nobody has counted the crumbs at the Duckling Swim School, so bread_crumbs >= 20000 is NULL rather than false, and the row goes.
  • To split rows into several outputs rather than drop them, keep them all and give each output layer its own filter.

remove_duplicates

Keeps the first row of each set of duplicates.

base: places
steps:
- type: remove_duplicates
  by:
  - category
  rejects: second_in_category
name category
Mallard Museum MUS
Puddle Lane Café CAF
Quackington Library LIB
Duckling Swim School SCH
Breadcrumb Hill PRK
Old Duck House ''
Old Lighthouse LND
  • One place per category: the first in row order. The four others (two more cafés, a second library and the Museum of Rubber Ducks) go to the second_in_category layer.
  • geometry: true compares the shapes instead of, or as well as, the columns: two features with the same geometry are duplicates whatever vertex they start at.

merge

Appends the rows of another source to the chain.

base: places
steps:
- {type: merge, source: new_places}

BeforeAfter

Before: the two new places (teal) and the chain's own. After: all of them are rows of the chain.

name category opened bread_crumbs
Mallard Museum MUS 1887 52000
Harbour Fish Market MKT NULL NULL
Pop-up Duck Gallery MUS NULL NULL
  • Columns are matched by name. new_places has no opened or bread_crumbs, so those are NULL for the two new rows. A column only the new source has is added, NULL for the existing rows.
  • Merged rows only go through the steps that come after the merge. Put the merge first if everything should be joined and filtered the same way.

snapshot

Names the chain's current state so later steps can join against it, or keep working on a copy of it (a branch) while the main chain carries on.

This example finds the places within 250 m of a crumb hotspot (a place handing out 50,000+ bread crumbs a year), where the ducks are bound to gather:

base: places
steps:
- {type: snapshot, id: crumb_hotspots}
- {type: filter, branch: crumb_hotspots, where: "bread_crumbs >= 50000"}
- {type: buffer, branch: crumb_hotspots, distance: 250}
- type: spatial_join
  source: crumb_hotspots
  predicate: within
  match: all
  fields: {near_hotspot: name}
- {type: filter, where: "near_hotspot IS NULL OR near_hotspot <> name"}

BeforeAfter

After: the two crumb hotspots' 250 m circles. A place inside another hotspot's circle gets near_hotspot; the red ones have no hotspot nearby.

name near_hotspot
Mallard Museum Quackington Library
Puddle Lane Café Mallard Museum
Puddle Lane Café Quackington Library
Quackington Library Mallard Museum
Soggy Crust Café Quackington Library

What happens, step by step:

  1. snapshot copies the places into a branch called crumb_hotspots. The main chain is unchanged.
  2. filter and buffer with branch: crumb_hotspots reduce the copy to the two hotspots and turn them into 250 m circles. The main chain still holds all eleven places as points.
  3. The spatial_join on the main chain uses source: crumb_hotspots, so each place is checked against the circles. match: all keeps one row per circle a place falls in.
  4. The last filter drops each hotspot's match with its own circle.

Output layers are always written from the main chain. A branch is only useful as the source of a later step.

Attributes: set columns between steps

attributes

Adds, overwrites or removes columns partway through the chain, for a later step to use.

base: places
steps:
- type: attributes
  columns:
  - {to: opened, from: opened, cast: INTEGER}
  - {to: era, expr: "CASE WHEN opened < 1950 THEN 'historic' ELSE 'modern' END"}
  drop:
  - inspected
- {type: filter, where: "era = 'historic'"}
name opened era
Old Lighthouse 1820 historic
Mallard Museum 1887 historic
Old Duck House 1901 historic
  • columns takes the same rules as the mapping. opened is text in the source, and the first column overwrites it with a number (cast: INTEGER).
  • The columns are set in order, so era can use the opened value set just above it.
  • The filter after it can then use era, a column that doesn't exist in the source. Without the attributes step, the filter would have to repeat the whole CASE expression.
  • You only list what you change. Every column you don't mention (name, category, bread_crumbs, …) is kept as it is and goes on to the next step. drop removes the ones you don't want, here inspected.