ICM SQL Cookbook — Full Code

Every script with comments · Spatial · Results · SWMM · 2D · Connected Objects · Arrays
95
Repo Scripts (50 IW + 45 SWMM)
39
New Scripts Here
5
Now Official ✓
6
SQL Bugs Fixed (review)
12
Official Context Files
95 In Repo (verified Jul 10, 2026) 25 Still New 5 Adopted Officially
🧪 Query Starter
Pick a network and a query kind for a correctly-headed skeleton — right object type comment, right terminating semicolons, no placeholder gotchas. Then find the closest matching card below to fill it in.

Spatial Queries

SPATIAL keyword

Find conduits crossing a GIS road layer

📐 SPATIAL Cross🗂 GIS layer🟡 New
▾
Selects all conduits that geometrically cross any line in a GIS road centerline layer. Essential for streetworks coordination, traffic management planning, and asset records. Load the GIS road shapefile as a background layer before running. Change "Roads" to match your GIS layer name.
// Object Type: Conduit // GIS Layer: Must be loaded in GeoPlan before running // USE: Find pipes under roads for streetworks, dig permits, asset tagging // Set up spatial search: Cross finds any conduit whose geometry // intersects a line or polygon in the named GIS layer SPATIAL Cross GIS "Roads"; // Select and display in grid: pipe ID, road name from GIS, and pipe size SELECT us_node_id, link_suffix, spatial.road_name, // field from the GIS layer conduit_width, conduit_length, system_type;
SPATIAL CrossGIS layerstreetworksasset management

Pipes within N metres of buildings

📐 SPATIAL Distance🗂 GIS polygon🟡 New
▾
Finds conduits within a specified distance of building footprint polygons. Use for construction risk assessment, basement flooding proximity, or wayleave/easement analysis. The distance (5m here) can be a variable via PROMPT.
// Object Type: Conduit // GIS Layer: Building footprint polygons loaded in GeoPlan // USE: Risk assessment for construction near sewers, // basement flooding potential, easement violations LET $buffer = 5; PROMPT LINE $buffer 'Search distance (m) from buildings'; PROMPT DISPLAY; // Distance search: finds network objects within $buffer metres // of any object in the GIS layer SPATIAL Distance GIS "Buildings" $buffer; SELECT us_node_id, link_suffix, conduit_width, us_invert, spatial.building_id, // GIS attribute spatial.address; // GIS attribute
SPATIAL DistancePROMPTbuildingsrisk

Match survey GPS points to nearest model node

📐 SPATIAL Nearest🗂 GIS points🟡 New
▾
For each model node, finds the nearest GIS survey point within a search radius. Use to link CCTV survey data, condition grades, or field-measured invert levels back to the network model. The Nearest search returns the single closest match.
// Object Type: All Nodes // GIS Layer: Survey point cloud from GPS/CCTV loaded in GeoPlan // USE: Link field survey data (inverts, condition grades, GPS coords) // back to model nodes for calibration/validation SPATIAL Nearest GIS "Survey_Points" 25; // 25m search radius // Grid output: model node ID + survey fields from GIS SELECT node_id, ground_level, spatial.survey_id, // survey reference spatial.measured_invert, // field measurement spatial.condition_grade, // CCTV grade ground_level - spatial.measured_gl AS "Level Difference"; // compare model vs survey
SPATIAL NearestCCTVsurveycalibration

Tag subcatchments with planning/zoning attributes from GIS

📐 SPATIAL Inside🗂 GIS polygon → UPDATE🟡 New
▾
For each subcatchment whose centroid falls Inside a GIS zoning polygon, copies the zone name into user_text_1. This is a spatial attribute join — bulk-transfers GIS classification data into the model. Works for any polygon layer: planning zones, DMA boundaries, flood zones, soil types.
// Object Type: Subcatchment // GIS Layer: Planning zone polygons loaded in GeoPlan // USE: Bulk-transfer GIS polygon attributes (zone names, soil types, // flood zone codes, DMA districts) into subcatchment user fields SPATIAL Inside GIS "Zoning"; // UPDATE writes the GIS field value into the subcatchment UPDATE SET user_text_1 = spatial.zone_name; // Also grab the impervious percentage if the GIS layer has it UPDATE SET user_number_1 = spatial.pct_impervious; // Verify the results SELECT subcatchment_id, user_text_1, user_number_1;
SPATIAL InsideUPDATE from GISattribute joinzoning

Cross-system pipe crossings (background network)

📐 SPATIAL Cross Background🔀 two networks🟡 New
▾
Open your foul network as the main network and the storm network as a background network. This query finds every foul conduit that geometrically crosses a storm conduit. Essential for combined sewer separation studies, utility conflict detection, and combined-to-separate conversion planning.
// Object Type: Conduit // Background Network: Open storm/surface water network as background // USE: System separation analysis - where do foul and storm pipes cross? // Utility conflict detection, clearance verification SPATIAL Cross Background [Conduit]; // Select all foul conduits crossing a storm conduit SELECT us_node_id, link_suffix, system_type, us_invert, ds_invert, conduit_width, // Background network conduit fields: spatial.us_node_id AS "BG Pipe US", spatial.conduit_width AS "BG Pipe Width", spatial.us_invert AS "BG Pipe Invert" WHERE system_type = 'foul';
SPATIAL CrossBackground networkseparation studyconflict

sim. Simulation Results

sim. prefix = max / current timestep

Select all flooding nodes

💧 sim.max_floodvolume🟡 New
▾
The most fundamental results query: find every node where floodvolume exceeds a tolerance. Drag your simulation results onto the GeoPlan first. The 1 m³ threshold filters out numerical noise. Enable "Open Selection in Grid View" to see the table.
// Object Type: All Nodes // Prerequisites: Drag simulation results onto GeoPlan first // USE: Identify all locations where flooding occurs // The sim. prefix gives max values over the entire simulation SELECT node_id, node_type, ground_level, sim.max_level, // peak water level sim.max_flooddepth, // depth above ground (negative = no flood) sim.max_floodvolume, // stored flood volume m³ sim.max_flvol // lost flood volume m³ WHERE sim.max_floodvolume > 1 // 1 m³ tolerance for noise OR sim.max_flvol > 1 ORDER BY sim.max_floodvolume DESC;
sim.max_floodvolumesim.max_flvolfloodingfirst query to run

Nodes at risk of flooding (freeboard < 200mm)

💧 sim.max_flooddepth🟡 New
▾
Finds nodes that are not yet flooding but the water level came within 200mm of ground level. Negative flooddepth = water below ground. So flooddepth between -0.200 and 0 means "almost flooded." Critical for capacity assessment and climate change sensitivity analysis.
// Object Type: All Nodes // USE: Find nodes close to flooding - the "near miss" analysis // flooddepth is NEGATIVE when water is below ground level // flooddepth = 0 means water exactly at ground // flooddepth < 0 and > -0.2 means within 200mm of flooding SELECT node_id, ground_level, sim.max_level, sim.max_flooddepth, ground_level - sim.max_level AS "Freeboard m" WHERE sim.max_flooddepth <= 0 AND sim.max_flooddepth > -0.200 ORDER BY sim.max_flooddepth DESC;
sim.max_flooddepthfreeboardrisk assessmentclimate change

Scenario comparison: sim vs sim2 peak results

🔀 sim. vs sim2.🟡 New
▾
Load a second simulation via "Open As > As alternate results for comparison." That simulation's fields appear as sim2. This query computes the delta in peak flow, peak depth, and peak level between two scenarios — the core before/after analysis for design option testing.
// Object Type: Conduit (for flow comparison) or All Nodes (for flooding) // Prerequisites: Load sim1 normally, then right-click sim2 → // "Open As → As alternate results for comparison" // USE: Compare baseline vs proposed design, before/after intervention, // existing vs climate change scenario // --- Conduit flow comparison --- SELECT us_node_id, link_suffix, sim.max_us_flow AS "Baseline Q", sim2.max_us_flow AS "Scenario Q", sim2.max_us_flow - sim.max_us_flow AS "ΔQ (m³/s)", (sim2.max_us_flow - sim.max_us_flow) / sim.max_us_flow * 100 AS "% Change" WHERE sim.max_us_flow > 0 ORDER BY (sim2.max_us_flow - sim.max_us_flow) DESC;
sim.sim2.deltascenario comparison

d/D ratio and velocity check — circular pipes (see shape note)

📊 capacity utilization🟡 New
▾
The depth-to-diameter ratio is the standard capacity utilization metric. d/D > 1.0 means the pipe is running full at that instant. Also checks peak velocity against self-cleansing minimum (0.75 m/s) and erosion maximum (3.0 m/s). Shape caveat: this version divides by conduit_width, which only equals the vertical dimension for circular pipes. For rectangular, egg-shaped, or arch sections, divide by conduit_height (the field that holds the full vertical dimension for every shape) instead — swap one field name below. Treat d/D as relative depth for non-circular shapes, not a literal diameter ratio, and don't read d/D alone as proof of surcharge without checking the model's actual hydraulic state.
// Object Type: Conduit // USE: Pipe performance summary — the table every sewer report needs // d/D > 1.0 = surcharged, d/D > 0.8 = nearing capacity // Velocity < 0.75 m/s = deposition risk (self-cleansing) // Velocity > 3.0 m/s = erosion risk SELECT us_node_id, link_suffix, conduit_width AS "Dia mm", conduit_length AS "Len m", gradient, // Peak depth and flow from results sim.max_us_depth AS "Peak Depth m", sim.max_us_flow AS "Peak Q m³/s", // d/D ratio: depth divided by diameter (convert mm to m) sim.max_us_depth / (conduit_width / 1000) AS "d/D", // Peak velocity sim.max_us_vel AS "Peak Vel m/s" ORDER BY sim.max_us_depth / (conduit_width / 1000) DESC;
d/D ratiovelocitycapacitycircular only — use conduit_height for other shapes

Network-wide aggregate statistics from results

📊 SUM / AVG / COUNT🟡 New
▾
One-shot summary statistics across the entire network using SQL aggregate functions. Produces a single-row grid with totals. Run this for a quick network health check after any simulation.
// Object Type: All Nodes (first query) then Conduit (second query) // USE: Quick executive summary of simulation results // Produces one-row summary tables // --- Node flooding summary --- SELECT COUNT(sim.max_floodvolume > 1) AS "Flooded Nodes", SUM(sim.max_floodvolume) DP 1 AS "Total Flood Vol m³", MAX(sim.max_flooddepth) DP 3 AS "Max Flood Depth m"; // --- Conduit performance summary --- SELECT FROM [Conduit] SUM(conduit_length) DP 0 AS "Total Pipe Length m", AVG(sim.max_us_vel) DP 2 AS "Avg Peak Vel m/s", COUNT(sim.max_us_depth > conduit_width/1000) AS "Surcharged Pipes";
SUMAVGCOUNTDP (decimal places)executive summary

tsr. Time Series Results

tsr. prefix = ALL timesteps — ICM's power feature

Full conduit results envelope: MIN / MAX / WHENMAX / AVG

📈 tsr. aggregate functions✓ Now official — SWMM repo 0033–0039
▾
The comprehensive conduit analysis: for every pipe, extract the min, max, time-of-max, and time-weighted average for depth, flow, velocity, and Froude number at both US and DS ends. This is the definitive results extraction query. WHENMAX returns the datetime of the peak.
// Object Type: Conduit // Prerequisites: Results loaded in GeoPlan // USE: Comprehensive time series analysis of every conduit // tsr. prefix accesses ALL timesteps (not just max/current) // Aggregate functions: MIN, MAX, WHENMAX, WHENMIN, AVG, COUNT, SUM // --- Depth and Flow envelope --- SELECT oid, MAX(tsr.us_depth) AS "Peak US Depth", WHENMAX(tsr.us_depth) AS "When Peak Depth", MAX(tsr.us_flow) AS "Peak US Flow", WHENMAX(tsr.us_flow) AS "When Peak Flow"; // --- Velocity and Froude envelope --- SELECT oid, MIN(tsr.us_vel) AS "Min Velocity", MAX(tsr.us_vel) AS "Max Velocity", AVG(tsr.us_vel) AS "Avg Velocity", // time-weighted MIN(tsr.us_froude) AS "Min Froude", MAX(tsr.us_froude) AS "Max Froude"; // --- DS end (same pattern) --- SELECT oid, MAX(tsr.ds_depth), MAX(tsr.ds_flow), MAX(tsr.ds_vel), MAX(tsr.ds_froude); // --- Volume totals --- SELECT oid, COUNT(tsr.us_flow) AS "Timestep Count", SUM(tsr.us_flow) AS "Flow Sum";
tsr.MINMAXWHENMAXAVGSUMCOUNT

Flood duration: count timesteps where node is flooding

⏱ COUNT(tsr. > threshold)🟡 New
▾
Converts flooding from a max-value snapshot into a duration metric. COUNT(tsr.flooddepth > 0) returns the number of timesteps where the node was flooding; multiply by your timestep for minutes/hours. 2026 update: ICM also has a direct DURATION(tsr.flooddepth > 0) aggregate returning minutes outright — see the Time Varying SQL reference in the official 03 Context folder — but COUNT works on all versions and shows the mechanism.
// Object Type: All Nodes // USE: Flood duration analysis — not just "did it flood" but "how long" // COUNT with a condition counts timesteps where condition is true // Multiply by timestep interval to get hours LET $dt_minutes = 1; // your results timestep in minutes SELECT node_id, COUNT(tsr.flooddepth > 0) AS "Flood Timesteps", COUNT(tsr.flooddepth > 0) * $dt_minutes / 60 AS "Flood Hours", MAX(tsr.flooddepth) AS "Peak Depth m", SUM(tsr.floodvolume) AS "Total Volume m³", WHENMAX(tsr.flooddepth) AS "When Peak" WHERE MAX(tsr.flooddepth) > 0 ORDER BY COUNT(tsr.flooddepth > 0) DESC;
COUNT(tsr. > 0)flood durationseveritycompliance

Surcharge duration per conduit

⏱ d/D > 1 timestep count🟡 New
▾
Counts how many timesteps each conduit is surcharged (depth exceeds diameter). Classifies severity: brief (< 30 min), moderate (30min–2hr), extended (> 2hr). Change $dt_minutes to match your results output interval.
// Object Type: Conduit // USE: Surcharge duration reporting for design compliance // Some standards limit surcharge to "no more than X hours" // tsr.us_depth > conduit_width/1000 means d/D > 1.0 = surcharged LET $dt_minutes = 1; SELECT us_node_id, link_suffix, conduit_width, MAX(tsr.us_depth) AS "Peak Depth m", MAX(tsr.us_depth) / (conduit_width/1000) AS "Peak d/D", COUNT(tsr.us_depth > conduit_width/1000) AS "Surcharge Steps", COUNT(tsr.us_depth > conduit_width/1000) * $dt_minutes AS "Surcharge Minutes" WHERE MAX(tsr.us_depth) > conduit_width/1000 ORDER BY COUNT(tsr.us_depth > conduit_width/1000) DESC;
COUNT(tsr. > field)surcharge durationdesign standard

Travel time from tsr velocity (Gerdts method)

🕐 AVG(IIF(tsr.))💡 ngerdts7🟡 New
▾
Computes travel time through each link using the time-weighted average velocity from results, then traces upstream accumulating travel times to map how long it takes water from every point to reach a selected downstream location. The IIF inside AVG filters to a specific time window. From Nathan Gerdts' ICM_Tools — this is the most sophisticated SQL pattern in the ICM community.
// Object Type: All Nodes (run on node layer, accesses link results) // Prerequisites: Editable network WITH results dragged on top // USE: Map travel time from every upstream point to a selected node // Answers: "how old is the flow at this manhole?" // Color subcatchments by travel time for RDII analysis // --- User parameters --- LET $iterations = 200; // max hops to trace upstream LET $min_speed = 0.01; // floor velocity to avoid div/0 LET $left = 10; // start timestep of averaging window LET $right = 100; // end timestep of averaging window // --- Step 1: Calculate avg velocity per pipe within time window --- UPDATE [All Links] SET $speed = AVG(IIF( (tsr.timestep_no > $left) AND (tsr.timestep_no < $right), tsr.us_vel, // use this velocity if in window NULL // exclude timesteps outside window )); // Floor at minimum speed UPDATE [All Links] SET $speed = $min_speed WHERE $speed < $min_speed; // --- Step 2: Travel time = length / velocity (in minutes) --- UPDATE [All Links] SET user_number_4 = IIF(conduit_length IS NOT NULL, conduit_length, 1) / $speed / 60; // --- Step 3: Trace upstream accumulating travel time --- // (Uses iterative WHILE loop pattern — see full script at // github.com/ngerdts7/ICM_Tools for the complete upstream trace)
AVG(IIF(tsr.))tsr.timestep_notravel timeRDIIGerdts

tsr vs tsr2: timestep-level scenario difference

🔀 tsr. vs tsr2.🟡 New
▾
While sim/sim2 only compare peaks, tsr/tsr2 compare at every timestep. This reveals if the timing shifted, if attenuation changed, or if a scenario produces a longer but lower peak — much richer than peak-only comparison. Corrected: an earlier draft summed flow with SUM(tsr.us_flow) and labeled it volume — dimensionally wrong unless every timestep has the same duration. INTEGRAL() multiplies each value by its timestep length before summing, which is what volume actually requires.
// Object Type: Conduit // Prerequisites: Load both simulations (sim + sim2) // USE: Detailed scenario comparison across the full hydrograph // Reveals timing shifts and attenuation effects that // peak-only (sim.) comparison misses SELECT oid, MAX(tsr.us_flow) AS "Base Peak Q", MAX(tsr2.us_flow) AS "Alt Peak Q", WHENMAX(tsr.us_flow) AS "Base Peak Time", WHENMAX(tsr2.us_flow) AS "Alt Peak Time", // Average difference across all timesteps AVG(tsr2.us_flow - tsr.us_flow) AS "Avg ΔQ", // Total volume comparison -- corrected: SUM(tsr.flow) is NOT volume, // it's just the ordinates added up. INTEGRAL multiplies each value // by its timestep duration in minutes -- ×60 converts m³/s to m³. INTEGRAL(tsr.us_flow) * 60 AS "Base Volume m³", INTEGRAL(tsr2.us_flow) * 60 AS "Alt Volume m³";
tsr.tsr2.WHENMAX timing shiftSUM volume

Time-Varying Deep Dive

DURATION · INTEGRAL · EARLIEST/LATEST · WHEN clause

WHEN clause — window any tsr. aggregate to specific timesteps

⏱ AGG(tsr.x) WHEN cond
▾
Cleaner than the AVG(IIF(cond, tsr.x, NULL)) pattern used elsewhere on this page: the WHEN clause restricts any aggregate to the timesteps where a condition holds, without nesting IIF. Confirmed against the official ICM Time Varying SQL reference. WHEN fields: tsr.timestep_no (1-based), tsr.timestep_start, tsr.timesteps, tsr.timestep_duration, tsr.sim_start, tsr.sim_end. Date literals use #dd/mm/yyyy hh:mm#.
// Object Type: Conduit // USE: restrict an aggregate to a specific time window without // wrapping every reference in IIF(cond, x, NULL) SELECT oid, MAX(tsr.flooddepth) WHEN tsr.timestep_no > 20 AS "Peak Depth After Step 20", AVG(tsr.depth) WHEN tsr.timestep_start = #01/01/2013 12:30# AS "Depth At 12:30";
WHEN clausetsr.timestep_nodate literal

Peak sustained depth — rolling window (repo-verified pattern)

⏱ WHEN + sliding windowfrom SWMM 0004
▾
The highest average depth across any rolling window of consecutive timesteps — a truer "sustained" flood/surcharge metric than a single-timestep peak. This is adapted directly from the official repo's 02 SWMM/0004 - Peak Sustained Depth (Rolling Average) script, which is itself the clearest real-world demonstration of the WHEN clause in the whole collection.
// Object Type: All Links // Works with metric or US customary unit models LET $n = 90; // total timesteps in the simulation result LET $period = 3; // window size (effective window = period+1 steps) SET $AVG = 0; // tracks the highest rolling average found LET $i=1; WHILE $i<=$n-$period; SET $AVG2 = AVG(tsr.depth) WHEN tsr.timestep_no >= $i AND tsr.timestep_no <= $i+$period; SET $AVG = IIF($AVG2 > $AVG, $AVG2, $AVG); LET $i=$i+1; WEND; SELECT oid AS [Link ID], $AVG AS [Peak Sustained Depth];
WHEN clausesliding windowrepo-verified

DURATION — flood/surcharge minutes in one function, no arithmetic

⏱ DURATION(tsr.cond)
▾
Returns the length of time in minutes for which an expression is true across the timesteps under consideration — not necessarily contiguous. This replaces the COUNT(tsr.x > 0) * $dt_minutes pattern used earlier on this page: DURATION stays correct even with variable timestep lengths, where COUNT×dt silently breaks.
// Object Type: All Nodes // USE: direct flood/surcharge duration -- correct even when the // simulation uses variable timestep lengths SELECT node_id, DURATION(tsr.flooddepth > 0) AS "Flood Minutes", MAX(tsr.flooddepth) AS "Peak Depth", INTEGRAL(tsr.qnode) AS "Inflow Integral" WHERE DURATION(tsr.flooddepth > 0) > 0;
DURATIONflood durationvariable timesteps safe

INTEGRAL — the only correct way to get volume from a flow series

⏱ INTEGRAL(tsr.flow)
▾
Sums the value at each timestep multiplied by that timestep's length in minutes — a proper time integral, unlike SUM() which just adds ordinates and is only meaningful when every timestep happens to be the same length. The SQL engine doesn't know units, so the ×60 to convert m³/s·min into m³ is the user's responsibility. Caveat: AVG, INTEGRAL, and SUM all ignore the simulation's final timestep, since its duration isn't well-defined.
// Object Type: Conduit // USE: flow -> volume, the correct way (see the tsr vs tsr2 card // above for the SUM() version of this mistake, now fixed) SELECT oid, INTEGRAL(tsr.us_flow) * 60 AS "Total Volume m³";
INTEGRALflow→volumefinal-timestep caveat

EARLIEST / LATEST / WHENEARLIEST / WHENLATEST

⏱ first/last qualifying value or time
▾
Four functions most guides miss entirely. EARLIEST/LATEST return the first/last non-null value of an expression — pair with IIF to find "the first time X crossed a threshold." WHENEARLIEST/WHENLATEST return the time a condition first/last became true, distinct from WHENMAX/WHENMIN which track the time of an extreme value.
// Object Type: Conduit // USE: "when did this first start surcharging" style questions -- // distinct from WHENMAX, which finds the time of the PEAK, // not the time a threshold was first crossed SELECT oid, EARLIEST(IIF(tsr.surcharge > 0.5, tsr.surcharge, NULL)) AS "First Surcharge > 0.5m", WHENEARLIEST(tsr.us_depth > conduit_height/1000) AS "First Time Full", WHENLATEST(tsr.us_depth > conduit_height/1000) AS "Last Time Full";
EARLIESTWHENEARLIESTWHENLATESTthreshold crossing

Percentage of simulation above self-cleansing velocity

⏱ DURATION ÷ tsr.timesteps
▾
Sewer design guidance typically wants pipes self-cleansing (velocity above ~0.75 m/s) for a healthy fraction of the simulation, not just at peak. This turns that into a single percentage per pipe.
// Object Type: Conduit // USE: design-standard check -- "self-cleansing X% of the time" SELECT oid, DURATION(tsr.us_vel > 0.75) / (tsr.timesteps * tsr.timestep_duration) * 100 AS "% Time Self-Cleansing";
DURATIONtsr.timestepsdesign compliance

Functions, Patterns & Export

TITLE · RINDEX · LOOKUPFN · LIKE/MATCHES · GROUP BY/HAVING · INTO FILE

Histogram any field in two lines — RINDEX + TITLE

ƒ RINDEX · TITLE✓ official repo idiom
▾
This is the actual SQL behind the repo's "Bit Bucket" scripts, and it's a two-liner. RINDEX assigns each value a bucket index against a sorted list; TITLE(n, list) turns that index into a human-readable range label. TITLE() is easy to miss — it's absent from the widely-circulated Autodesk SQL function inventory, yet it appears in twelve scripts across the InfoWorks and SWMM folders and is documented in the repo's own 03 Context Function Reference. Without it you end up hand-writing a sixteen-deep nested IIF to label buckets; with it, one GROUP BY does the whole job.
// Object Type: All Links // Verbatim from: 01 InfoWorks/0040 - SQL Bit Bucket for Diameter (mm) LIST $buckets = 75, 150,200,300,600, 900, 1000, 1200,1500,2000,2500,3000; SELECT count(*) AS 'Count' GROUP BY TITLE(RINDEX(conduit_width, $buckets), $buckets) AS 'Diameter(mm)' // Same idiom, length instead of diameter (0041, feet): LIST $buckets = 6,8,12,16,20,24,36,48,60,72,84,96,120,250,500,1000,2500,5000; SELECT count(*) AS 'Count' GROUP BY TITLE(RINDEX(conduit_length, $buckets), $buckets) AS 'Length (feet)'
TITLERINDEXGROUP BYhistogramverbatim from repo

The RINDEX off-by-one — a bucket-labelling trap

ƒ RINDEX semantics⚠ worked failure case
▾
If you label RINDEX buckets by hand instead of using TITLE(), there's a specific way to get it silently wrong — and it produces a table that looks perfectly plausible while every row is mislabelled by one bucket. The rule, per the official Function Reference: RINDEX returns 0 when the value is below the first list element, 1 when it's ≥ first but < second, and n when it's ≥ the last of n elements. So whether index 0 means "below the smallest bucket" or "the smallest bucket" depends entirely on whether your list starts at zero.
// Official worked example (03 Context Function Reference): LIST $breaks = 0, 0.3, 0.5, 1.0; SET user_number_1 = RINDEX(sim.max_Surcharge, $breaks); // 0 -> <0 1 -> 0-0.3 2 -> 0.3-0.5 // 3 -> 0.5-1.0 4 -> >=1.0 // THE TRAP: a list with a LEADING ZERO shifts every label. LIST $w = 0, 100, 200, 300; // note the 0 // RINDEX = 0 -> width < 0 (impossible for a real pipe) // RINDEX = 1 -> 0 to 100 (this is your "< 100" bucket) // RINDEX = 2 -> 100 to 200 // Labelling 0 as "< 100" and 1 as "100-200" is WRONG by one // bucket on every row -- and nothing errors, it just misreports. // FIX A (preferred): drop the leading 0 and let TITLE() label it LIST $w = 100, 200, 300; SELECT count(*) AS 'Count' GROUP BY TITLE(RINDEX(conduit_width, $w), $w) AS 'Width (mm)' // FIX B: keep the 0, but shift every hand-written label up one
RINDEXoff-by-onesilent failureuse TITLE instead

COUNT(us_links.*) — the wildcard count form

🔗 .* wildcard
▾
For counting connected objects or array rows, the documented idiom is COUNT(prefix.*) rather than counting a specific field. Per the official Function Reference, COUNT behaves in three contexts: array rows (COUNT(details.*) = total rows), one-to-many links (COUNT(us_links.*) = number of upstream links), and GROUP BY (COUNT(*) = group size). Counting a named field instead works but silently skips rows where that field is NULL — .* avoids that trap.
// Object Type: All Nodes // USE: node connection degree -- 1 incoming + 0 outgoing = dead end SELECT node_id, COUNT(us_links.*) AS "# Incoming", COUNT(ds_links.*) AS "# Outgoing", COUNT(all_us_links.*) AS "# All Upstream (full trace)"; // Orphan check using the wildcard form SELECT WHERE COUNT(us_links.*) = 0 AND COUNT(ds_links.*) = 0;
COUNT(.*)all_us_linksconnection degreeNULL-safe

Rating-curve interpolation without a blob-table lookup

ƒ LOOKUPFN
▾
LOOKUPFN(expr, clamp, step, list) does piecewise-linear (or stepwise) x→y interpolation against a flat list of x,y pairs — a real alternative to manually querying an HD table or rating curve for one interpolated value. clamp=true holds the output at the endpoint value for out-of-range input instead of returning NULL; step=true switches from linear interpolation to stepwise (holds the previous y until the next x).
// Object Type: Conduit // USE: interpolate a calibration factor, rating-curve discharge, or any // x->y relationship from a short table, without a blob-table join // depth (m) -> discharge (l/s) pairs, ascending x order LIST $rating = 0,0, 0.5,120, 1.0,300, 1.5,410; SELECT us_node_id, sim.max_us_depth, LOOKUPFN(sim.max_us_depth, true, false, $rating) AS "Interpolated Discharge l/s";
LOOKUPFNinterpolationrating curvecalibration

Asset ID pattern matching — LIKE vs MATCHES

ƒ LIKE · MATCHES
▾
ICM's LIKE uses DOS-style wildcards (*/?), and has a sharp limitation: once * appears, everything after it matches unconditionally — so LIKE '*01' (suffix) and LIKE '*01*' (contains) don't behave like their obvious reading suggests. For suffix or contains matching on asset IDs, district codes, or naming conventions, use MATCHES (a small regex dialect) instead.
// Object Type: Conduit // USE: asset ID / naming-convention selection // LIKE only reliably does PREFIX matching once '*' is involved // Prefix match -- LIKE works fine here SELECT WHERE us_node_id LIKE 'DMA07*'; // Suffix match -- LIKE '*_RENEWED' would NOT work as expected; // MATCHES does SELECT WHERE us_node_id MATCHES '.*_RENEWED'; // Contains match -- same story, MATCHES required SELECT WHERE user_text_1 MATCHES '.*TEMP.*';
LIKEMATCHESasset IDnaming convention

Network inventory by system type — GROUP BY / HAVING

ƒ GROUP BY · HAVING
▾
Real, working aggregation and filtering-of-groups, close to standard SQL. This produces total length, count, and average diameter grouped by system type in one query, then HAVING narrows to groups that themselves fail a check — here, systems whose largest pipe is suspiciously small.
// Object Type: Conduit // USE: network inventory report grouped by system type, // with HAVING to filter on the GROUP result itself SELECT SUM(conduit_length) DP 0 AS "Total Length", COUNT(oid) AS "Pipe Count", MAX(conduit_width) AS "Largest Pipe mm" GROUP BY system_type HAVING MAX(conduit_width) < 1000;
GROUP BYHAVINGinventory report

Direct CSV export — no grid view needed

ƒ INTO FILE
▾
SELECT ... INTO FILE writes query results straight to a CSV on disk — skip the grid-view-then-manual-export step entirely. Works with the same column aliasing rules as any output query (quote an alias to include spaces or punctuation).
// Object Type: All Nodes // USE: scripted/scheduled report generation straight to CSV SELECT node_id AS title, ground_level, sim.max_floodvolume AS "Flood Volume m³" WHERE sim.max_floodvolume > 1 INTO FILE 'c:\temp\flooded_nodes.csv';
INTO FILECSV exportreporting

NULL semantics — the equality exception, precisely

ƒ IS NULL vs = NULL
▾
Reference card, not a query to run as-is. The general rule: any operation involving NULL propagates NULL (3 > NULL is NULL, not false). Equality is the one documented exception — x = NULL behaves exactly like x IS NULL, and x <> NULL like x IS NOT NULL. That's why both forms work for equality specifically, but the exception doesn't extend to >/< — don't assume it does. An empty string is simultaneously NULL and '', and boolean/checkbox fields have no concept of NULL at all.
// Object Type: All Nodes -- reference examples, not one query // These three are equivalent -- empty string IS NULL and '' SELECT WHERE asset_id IS NULL; SELECT WHERE asset_id = ''; SELECT WHERE LEN(asset_id) = 0; // Equality is the ONLY operator where = NULL / IS NULL are the same thing. // Don't extrapolate this to other operators -- "x > NULL" is just always // NULL (never true), it does NOT become "x IS something" SELECT WHERE ground_level = NULL; // same as IS NULL SELECT WHERE ground_level > 0; // NULL ground_level -> excluded, not included
NULL semanticsequality exceptionreference card

Connected Object Queries

us_links. / ds_links. (node→link) · us_node. / ds_node. (link→node)

Diameter constriction finder (connected link comparison)

🔗 us_links. vs ds_links.🟡 New
▾
At each node, compares the largest incoming pipe to the smallest outgoing pipe. A ratio < 1.0 means the network narrows — a potential bottleneck. The us_links. prefix accesses fields of all conduits whose DS end connects to this node; ds_links. accesses conduits whose US end connects here.
// Object Type: All Nodes // USE: Find hydraulic bottlenecks — where pipes get smaller downstream // us_links. = all conduits arriving at this node (their DS end is here) // ds_links. = all conduits leaving this node (their US end is here) // MAX/MIN/COUNT/ANY/ALL work as aggregates across connected objects SELECT node_id, COUNT(us_links.oid) AS "# Incoming", COUNT(ds_links.oid) AS "# Outgoing", MAX(us_links.conduit_width) AS "Largest In mm", MIN(ds_links.conduit_width) AS "Smallest Out mm", MIN(ds_links.conduit_width) / MAX(us_links.conduit_width) AS "Size Ratio" WHERE COUNT(us_links.oid) > 0 AND COUNT(ds_links.oid) > 0 ORDER BY MIN(ds_links.conduit_width) / MAX(us_links.conduit_width) ASC;
us_links.ds_links.MAX/MIN aggregatesbottleneck

Select pipes at flooded nodes (us_node. with sim results)

🔗 us_node.sim.✎ corrected
▾
Twice-corrected — see the log below for the full story. From a link in an ICM modelling network, the one-to-one prefix to its upstream node is us_node. (singular), per the official 03 Context Syntax Reference's implicit-join table: Link → Node = us_node / ds_node. That's distinct from us_links./ds_links., which are the plural one-to-many prefixes going the other way, from a node out to its links. This selects every conduit whose upstream node is flooding — the pipes feeding a flood hotspot.
// Object Type: Conduit // USE: Select conduits at flooded manholes // us_node.field / ds_node.field access the upstream/downstream // node's fields from a link object (confirmed: Autodesk ICM help, // "SQL and Connected Objects" -- us_links./ds_links. are node->link, // us_node./ds_node. are the link->node direction) SELECT us_node_id, link_suffix, conduit_width, sim.max_us_flow AS "Pipe Peak Flow", us_node.ground_level AS "Node GL", us_node.sim.max_floodvolume AS "Node Flood m³", us_node.sim.max_flooddepth AS "Node Flood Depth" WHERE us_node.sim.max_floodvolume > 1;
us_node.us_node.sim.link→node results
Verified against the official 03 Context/InfoWorks_ICM_SQL_Syntax_Reference.md implicit-join table and the Glossary's navigation-keyword table, which both list us_node/ds_node (singular, one-to-one, from link) for the ICM modelling network. Whether a time-varying tsr. aggregate can be chained through the join the same way (e.g. MAX(us_node.tsr.depth)) is not stated either way in those files — sim., a summary field rather than a time series, is the safer demonstrated case.

Invert drop / benching check at each manhole

🔗 us_links.ds_invert vs ds_links.us_invert🟡 New
▾
Compares the invert of arriving pipes (their DS_invert) with the invert of departing pipes (their US_invert). A negative drop means the outgoing pipe starts higher than the incoming pipe arrives — a head loss / benching issue. Zero drop means a flat connection.
// Object Type: All Nodes // USE: Check benching/invert alignment at manholes // Incoming pipe ds_invert should be >= outgoing pipe us_invert // A negative value means water has to "climb" through the manhole SELECT node_id, chamber_floor, MIN(us_links.ds_invert) AS "Lowest Arriving IL", MAX(ds_links.us_invert) AS "Highest Departing IL", MIN(us_links.ds_invert) - MAX(ds_links.us_invert) AS "Invert Drop m" WHERE COUNT(us_links.oid) > 0 AND COUNT(ds_links.oid) > 0 ORDER BY (MIN(us_links.ds_invert) - MAX(ds_links.us_invert)) ASC;
us_links.ds_invertds_links.us_invertbenchingQA/QC

SWMM Network Queries

ICM SWMM — sw_ tables

SWMM subcatchment hydrology parameter audit

🌧 sw_subcatchment🟡 New
▾
Comprehensive check of all SWMM subcatchment hydrology parameters. Finds missing or zero values for imperviousness, Manning's n, depression storage, and infiltration parameters. Run this before any SWMM simulation to catch incomplete data. In SQL, you query SWMM objects using FROM [SWMM Subcatchment] — no sw_ prefix needed in the SQL itself.
// Object Type: SWMM Subcatchment (select from dropdown) // USE: Pre-simulation validation of SWMM hydrology data // Catches missing infiltration params, zero areas, null Manning's n // In SQL you use the object type name, NOT the Ruby table prefix // (Ruby uses sw_subcatchment, SQL uses "SWMM Subcatchment") // --- Find subcatchments with missing critical fields --- SELECT subcatchment_id, area, imperv_pct, // % impervious — should be 0-100 width, // characteristic width — must not be 0 slope, // average surface slope % n_imperv, // Manning's n for impervious n_perv, // Manning's n for pervious s_imperv, // depression storage impervious (mm) s_perv, // depression storage pervious (mm) node_id // drain-to node WHERE area IS NULL OR area = 0 OR width IS NULL OR width = 0 OR n_imperv IS NULL OR n_perv IS NULL OR node_id IS NULL;
SWMM Subcatchmenthydrologypre-sim check

SWMM conduit shape inventory and statistics

📊 aggregate by shape🟡 New
▾
Groups all SWMM conduits by cross-section shape and computes statistics. Useful for network characterization, model review documentation, and verifying shape assignments after import from .inp files.
// Object Type: SWMM Conduit // USE: Network inventory — how many pipes of each shape? // Run after importing a .inp file to verify shape mapping // SWMM shapes: CIRCULAR, RECT_CLOSED, TRAPEZOIDAL, etc. // First get overall summary SELECT COUNT(oid) AS "Total Conduits", SUM(conduit_length) DP 0 AS "Total Length"; // Then list all with their key parameters SELECT us_node_id, ds_node_id, shape, conduit_height AS "Geom1 (height)", conduit_width AS "Geom2 (width)", conduit_length, roughness_N, us_invert, ds_invert ORDER BY shape, conduit_height DESC;
SWMM Conduitshapeinventorypost-import check

SWMM weir, orifice, and pump validation

🔧 special structures🟡 New
▾
Three queries to audit all SWMM special structures. Checks that weirs have crest height and discharge coefficient, orifices have dimensions and type, and pumps have assigned curves. Missing parameters cause SWMM simulation failures or warnings.
// USE: Pre-simulation check for all SWMM special link objects // Missing coefficients or dimensions = simulation failures // --- Weir audit --- // Object Type: SWMM Weir SELECT us_node_id, ds_node_id, weir_type, // TRANSVERSE, SIDEFLOW, V-NOTCH, TRAPEZOIDAL crest_height, // must not be NULL discharge_coeff, // Cd — typically 1.8 for broad-crested flap_gate; // yes/no // --- Orifice audit --- // Object Type: SWMM Orifice SELECT us_node_id, ds_node_id, orifice_type, // SIDE, BOTTOM shape, // CIRCULAR, RECT_CLOSED orifice_height, // Geom1 discharge_coeff, // Cd — typically 0.65 crest_height; // --- Pump audit --- // Object Type: SWMM Pump SELECT us_node_id, ds_node_id, pump_curve, // curve name — must match a defined curve on_depth, // startup depth off_depth; // shutoff depth
SWMM WeirSWMM OrificeSWMM Pumppre-sim validation

2D Zone / 1D-2D Coupling

flood_type · 2D inflow points · Gully 2D

Set nodes to 2D flood type inside 2D zone polygon

🌊 SPATIAL + flood_type🟡 New
▾
The essential 1D-2D coupling step: select all nodes inside a 2D zone polygon using SPATIAL Inside, then change their flood_type from 'Stored' to '2D'. Also sets outfalls to 'Outfall 2D' and assigns discharge coefficients. This is the multi-step query you need after creating a 2D zone and before running a 1D-2D simulation.
// Object Type: All Nodes // Prerequisites: 2D zone polygon created and meshed // USE: Convert 1D-only nodes to 1D-2D coupled nodes // flood_type controls how water exchanges between 1D and 2D: // 'Stored' = flood ponds at node (no 2D interaction) // '2D' = weir equation exchange with mesh (Cd needed) // 'Gully 2D' = head-discharge table exchange (inlet control) // 'Outfall 2D' = outfall exchanges with mesh // 'Lost' = flood disappears (old approach, avoid) // Step 1: Select nodes inside the 2D zone SPATIAL Inside Network [2D Zone]; // Step 2: Change manholes from Stored to 2D UPDATE SELECTED SET flood_type = '2D' WHERE flood_type = 'Stored'; // Step 3: Set discharge coefficient (weir equation Cd) UPDATE SELECTED SET flooding_discharge_coeff = 0.5 WHERE flood_type = '2D'; // Step 4: Convert outfalls to Outfall 2D UPDATE SELECTED SET flood_type = 'Outfall 2D' WHERE node_type = 'Outfall'; // Verify the result SELECT node_id, node_type, flood_type, flooding_discharge_coeff;
SPATIAL Insideflood_type1D-2D couplingmulti-step

Redirect subcatchments to 2D inflow points

🌧 2d_pt_id💡 ngerdts7🟡 New
▾
Instead of subcatchment runoff going directly into underground nodes, redirect it to 2D inflow points on the surface. This forces the runoff to enter via the 2D mesh, letting inlet capacity (gully head-discharge) control flow into the 1D network. This is the key pattern for proper dual drainage modeling. From ngerdts7/ICM_Tools.
// Object Type: Subcatchment // Prerequisites: 2D zone with mesh, nodes set to Gully 2D flood type // USE: Dual drainage — make subcatchment runoff enter via 2D surface // instead of directly into underground nodes // The 2d_pt_id field creates a 2D inflow point at the node location // Subcatchment flow then goes to the surface, not the pipe // For each subcatchment, set its 2D point to its current node UPDATE SET [2d_pt_id] = node_id WHERE node_id IS NOT NULL; // Verify SELECT subcatchment_id, node_id, [2d_pt_id] WHERE [2d_pt_id] IS NOT NULL;
2d_pt_iddual drainageinlet controlngerdts7

QA/QC Validation Suite

Works for InfoWorks AND SWMM networks

Master pre-simulation checklist (multi-clause)

✅ 8 checks in one query🟡 New
▾
Chain multiple validation checks using ICM's multi-clause semicolon syntax. Each clause builds on the selection. Run this before every simulation to catch the most common data errors. The grid output shows which test flagged each object. Customize thresholds via PROMPT.
// Object Type: Conduit (first block) then All Nodes (second block) // USE: Run before every simulation — catches 90% of data errors // Each semicolon starts a new clause that ADDS to selection // Grid view will show all flagged objects // ============================================= // CONDUIT CHECKS — set Object Type to Conduit // ============================================= CLEAR SELECTION; // 1. Zero or null length SELECT WHERE conduit_length IS NULL OR conduit_length <= 0; // 2. Missing or unreasonable Manning's n SELECT WHERE roughness_N IS NULL OR roughness_N = 0 OR roughness_N > 0.1; // 3. Adverse grade (flowing uphill) SELECT WHERE ds_invert > us_invert; // 4. Zero or null diameter/height SELECT WHERE conduit_width IS NULL OR conduit_width = 0; // 5a. US soffit above US node's ground level (corrected: was an // ambiguous single check using an invented "joined." prefix -- // us_node./ds_node. are the confirmed link->node fields, and // checking both ends separately removes the ambiguity) SELECT WHERE us_invert + conduit_height/1000 > us_node.ground_level AND us_node.ground_level IS NOT NULL; // 5b. DS soffit above DS node's ground level (the other end) SELECT WHERE ds_invert + conduit_height/1000 > ds_node.ground_level AND ds_node.ground_level IS NOT NULL; // ============================================= // NODE CHECKS — change Object Type to All Nodes // ============================================= CLEAR SELECTION; // 6. Missing ground level SELECT WHERE ground_level IS NULL; // 7. Chamber floor above ground (impossible geometry) SELECT WHERE chamber_floor >= ground_level AND ground_level IS NOT NULL; // 8. Orphan nodes (no connections at all) SELECT WHERE COUNT(us_links.oid) = 0 AND COUNT(ds_links.oid) = 0 AND node_type <> 'Outfall';
CLEAR SELECTIONmulti-clauseus_node./ds_node.COUNT(us_links.)pre-sim

Post-simulation: flag results anomalies

📊 results-based QA/QC🟡 New
▾
After running a simulation, check for result anomalies: negative flows (unexpected direction), extremely high velocities, Froude > 2 (supercritical), and nodes that never activate. These indicate model instability or data problems that survived the pre-sim check.
// Object Type: Conduit // Prerequisites: Results loaded // USE: Post-simulation sanity check — are results reasonable? CLEAR SELECTION; // 1. Negative peak flow (unexpected reverse flow) SELECT WHERE sim.max_us_flow < 0; // 2. Extremely high velocity (> 6 m/s suggests instability) SELECT WHERE sim.max_us_vel > 6; // 3. High Froude number (supercritical flow = potential instability) SELECT WHERE sim.max_us_froude > 2; // 4. Zero flow in conduits (never activated - check connectivity) SELECT WHERE sim.max_us_flow = 0 AND sim.max_ds_flow = 0; // Grid shows all flagged conduits — investigate each SELECT SELECTED us_node_id, link_suffix, sim.max_us_flow, sim.max_us_vel, sim.max_us_froude, sim.max_us_depth;
post-simreverse flowinstabilityFroude

Network Traversal

WHILE loops · Upstream/Downstream traces

Label drainage areas by outfall (partition network)

🏷 user_text tagging✎ corrected — 4 bugs fixed
▾
For each outfall, traces upstream through the full contributing network and writes the outfall ID into user_text_1 of every node — partitioning the network automatically. The original draft of this card had four real bugs: 0-indexed list access (lists are 1-indexed — AREF(1,…) is the first element), COUNT($list) used for list length instead of LEN($list), END; closing WHILE loops instead of WEND;, and an invented WHERE field IN (SELECTED [Type]) subquery pattern that ICM SQL doesn't support (no subqueries — ever). The corrected version below uses the documented flag-propagation pattern instead.
// Object Type: All Nodes // USE: Partition network into drainage areas automatically // Every node gets tagged with its downstream outfall ID // Use as thematic for colour-coding drainage areas in GeoPlan // Step 1: Collect all outfall IDs into a 1-indexed list LET $max_hops = 500; LIST $outfalls STRING; SELECT DISTINCT node_id INTO $outfalls WHERE node_type = 'Outfall'; // Step 2: For each outfall, trace upstream (lists are 1-indexed -- // start at 1, test with LEN(), not COUNT() which is a different // aggregate function for query results, not list length) LET $i = 1; WHILE $i <= LEN($outfalls); LET $outfall_id = AREF($i, $outfalls); // Start at this outfall CLEAR SELECTION; UPDATE SET $node_sel = 0; SELECT WHERE node_id = $outfall_id; UPDATE SELECTED SET $node_sel = 1; UPDATE SELECTED SET user_text_1 = $outfall_id WHERE user_text_1 IS NULL; // Trace upstream hop by hop using flag-field propagation -- // the documented pattern (no subqueries: ICM SQL has none). // From a Node: us_links.$field writes into the connected Link; // from that Link: us_node.$field reads/writes the node upstream // of it. Chaining the two each hop propagates the flag outward. LET $hop = 0; WHILE $hop < $max_hops; UPDATE [All Links] SET $link_sel = 0; SET us_links.$link_sel = 1 WHERE $node_sel = 1; UPDATE [All Links] SET us_node.$node_sel = 1 WHERE $link_sel = 1; SET user_text_1 = $outfall_id WHERE $node_sel = 1 AND user_text_1 IS NULL; LET $hop = $hop + 1; WEND; LET $i = $i + 1; WEND; // Verify: grid all nodes with their drainage area tag SELECT ALL node_id, user_text_1 AS "Drains To";
WHILE/WEND1-indexed AREFLEN not COUNTflag propagationno subqueries

Cumulative contributing area at every node

📐 upstream area trace🟡 New
▾
Traces upstream from every node, accumulating connected subcatchment areas. Writes the total contributing catchment area into user_number_1. This is the "drainage area at a point" calculation — fundamental for rational method verification and model review. Simplified pattern shown; full implementation needs the iterative upstream walk.
// Object Type: All Nodes // USE: Calculate total contributing subcatchment area at every node // Answers: "how much catchment drains to this manhole?" // Fundamental for rational method Q = CiA verification // Step 1: Tag each node with its directly-connected subcatchment area // (Subcatchments drain to nodes via node_id field) LET $node_count = 0; SELECT COUNT(oid) INTO $node_count; // For each node: sum areas of subcatchments that drain here // node_id here is the subcatchment's OWN field (the drain-to node's // ID string) -- no connected-object prefix needed to read it UPDATE SET user_number_1 = 0; // initialize // Accumulate from subcatchment table // (In practice, use WHILE loop to trace upstream and sum) // Simplified direct-connection version: SELECT FROM [Subcatchment] node_id, SUM(total_area) DP 2 AS "Direct Area (ha)", SUM(population) DP 0 AS "Population", COUNT(oid) AS "# Subcatchments";
SUM(total_area)contributing arearational methodmodel review

Array Data Queries

Cross-sections · Runoff surfaces · HD curves

Runoff surface area audit per subcatchment

📊 structured array🟡 New
▾
Each InfoWorks subcatchment has an array of runoff surfaces (road, roof, pervious, etc). This query sums all surface areas and compares to total_area. A mismatch means surfaces don't add up — common after manual editing. The SUM aggregate works across array rows.
// Object Type: Subcatchment // USE: Verify runoff surface areas sum to subcatchment total_area // Catches surfaces that were added/deleted without updating totals // Array fields accessed via the structured sub-object syntax SELECT subcatchment_id, total_area AS "Total Area ha", // SUM across the runoff_surface array rows SUM(runoff_surface.area) DP 4 AS "Sum Surfaces ha", total_area - SUM(runoff_surface.area) AS "Difference ha", COUNT(runoff_surface.oid) AS "# Surfaces" WHERE ABS(total_area - SUM(runoff_surface.area)) > 0.001;
runoff_surface.areaSUM(array)area balanceQA/QC

Cross-section: MIN bed level + bank levels (existing 0010, enhanced)

📊 XS structured array🟣 Enhance
▾
Extends existing script 0010 to extract not just the minimum bed level but also the maximum bank levels and channel width from cross-section array data. For river models: the bank level minus water level = freeboard.
// Object Type: River Reach or Channel // USE: Extract key cross-section geometry metrics // Existing script 0010 only gets MIN bed level // This version also gets bank levels for freeboard analysis // Cross-section data is a structured array with fields: // chainage (x), elevation (z), Manning's n, panel marker, etc. SELECT us_node_id, link_suffix, // Minimum bed elevation across all XS points MIN(section_array.elevation) AS "Bed Level", // Maximum elevation = bank top MAX(section_array.elevation) AS "Max Bank", // First and last chainage give channel width MAX(section_array.chainage) - MIN(section_array.chainage) AS "XS Width m", // Count the data points COUNT(section_array.oid) AS "# Points", // Average roughness AVG(section_array.roughness_N) DP 4 AS "Avg Manning n";
section_array.MIN/MAX/AVG over arrayriverfreeboard

Documentation & Existing Script Reference

What is this Cookbook?

This page is a companion to the Innovyze Open-Source-Support repository's SQL collection — re-verified against a fresh clone on July 10, 2026: 50 InfoWorks script folders (0001–0049 with 0022 missing, plus two Bit Bucket folders that are README-only — see the correction below), 45 SWMM script folders, and the official 12-file 03 Context LLM system. Several of this cookbook's original proposals have since been adopted officially (Manning's Q-full, tsr envelopes, WHEN-clause rolling stats, SWMM LID coverage; the Bit Bucket unification only partly — see the correction log). The focus stays on the most powerful but under-documented ICM SQL features: SPATIAL keyword queries, sim. and tsr. results access, SWMM network objects, 2D zone/1D-2D coupling, connected object joins (us_links. / ds_links. / us_node. / ds_node.), and array data queries. Every script includes full SQL code with inline comments explaining what each line does and when to use it.

Corrections & Verification Log — July 21, 2026

An external technical review scored an earlier draft 7.4/10, flagging six SQL correctness issues and proposing a set of feature improvements. Every claim below was re-checked against primary sources (Autodesk's own ICM and WS Pro help pages, fetched directly, plus a fresh clone of the Innovyze repo) rather than accepted at face value — two of the six didn't survive that check, and one fix turned out to be bigger than the review itself caught. The reasoning for each verdict is below so the "why," not just the "what," is on record.

Fixed Missing semicolon after SPATIAL statements

Confirmed against Autodesk's SQL-and-spatial-searches help page, which states explicitly: "the SPATIAL and SELECT clauses are defined as separate query blocks and have to be separated by a semicolon." All six explicit SPATIAL cards on this page were missing it — every one is now fixed.

Corrected twice Connected-object prefixes: joined. → us_node. (and the wrong turn in between)

This one took two passes, and the first pass made it worse. Recorded in full because the intermediate state shipped.

Round 1 — the original bug (real). A card used joined.sim.max_floodvolume from a Conduit to reach its upstream node. That was wrong.

Round 2 — my fix (also wrong). I replaced it with us_nodes./ds_nodes. (plural) and declared joined. "not a documented ICM keyword." Both claims were incorrect. There is no plural us_nodes. prefix, and joined. is documented — I over-corrected from a page that didn't cover the case.

Round 3 — verified against the repo's own context files. The official 03 Context/InfoWorks_ICM_SQL_Syntax_Reference.md implicit-join tables and InfoWorks_ICM_SQL_Glossary.md navigation-keyword table settle it:

  • ICM modelling network — Link → Node is us_node / ds_node, singular, one-to-one. Subcatchment → Node is node. These are what this cookbook needs, and what the page now uses throughout.
  • Node → Link is us_links / ds_links (plural, one-to-many), plus all_us_links / all_ds_links for a full trace. These were right all along.
  • joined. is real — it's the one-to-one from a survey or incident object to its parent asset in Collection/Distribution asset networks (CCTV Survey → Pipe, Manhole Survey → Manhole, Pipe Repair → Pipe, and so on), with an alternative type-specific name available (pipe, manhole). It is simply not the modelling-network link→node prefix, which is the specific misuse the original card made.

All 51 occurrences across cards, comments, pills, and reference tables now use the singular form, and the lint rule was rewritten: it no longer calls joined. categorically wrong, instead flagging it only outside a survey/asset context, and it now also catches the plural us_nodes. form this page briefly shipped.

Fixed Traversal script: four real bugs

All four held up against ICM's own documented rules (which this cookbook's companion context guide had already independently established): lists are 1-indexed, so AREF($i,…) starting at $i=0 was wrong; COUNT($list) was used where list length needs LEN($list); WHILE loops were closed with END; instead of WEND;; and WHERE field IN (SELECTED [Type]) is an invented subquery pattern ICM SQL doesn't support. The card now uses the documented flag-propagation pattern instead (us_links.$flag / us_node.$flag chained each hop).

Fixed SUM(tsr.flow) mislabeled as volume

Confirmed against the ICM Time Varying SQL reference: SUM() just adds the ordinates at each timestep, which is only meaningful if every timestep is the same length. INTEGRAL() multiplies each value by its timestep duration before summing — the actually-correct way to get volume from a flow series. The scenario-comparison card now uses INTEGRAL(tsr.us_flow) * 60, and a full section on INTEGRAL, DURATION, EARLIEST/LATEST, and the WHEN clause has been added below.

Fixed d/D ratio not shape-aware

A fair catch: dividing by conduit_width only gives a true diameter ratio for circular pipes. conduit_height holds the full vertical dimension for every shape and is the correct denominator for rectangular, egg, or arch sections. The card now carries this caveat explicitly rather than presenting one formula as universal.

Fixed QA/QC soffit check was US/DS-ambiguous

The original check compared one invert to one connected node's ground level without specifying which end. Split into two explicit clauses — US invert against us_node.ground_level, DS invert against ds_node.ground_level (singular, per the correction above), so both ends are checked and neither is ambiguous.

Declined "Rename spatial. to gislayer."

The review's citation for this was Autodesk's WS Pro help page, which does use gislayer. — but ICM is a different product in the same family. Fetching ICM's own spatial-searches help page directly confirms: "The layer is then referred to in the query as spatial." Applying a WS-Pro-specific term to ICM would have introduced an error while fixing one. spatial.field stays as originally written.

Declined "Replace help2.innovyze.com links — they're legacy"

Checked by fetching the pages directly rather than assuming: help2.innovyze.com is still live, current, ICM-specific documentation (the fetched pages carry a 2023.2 release stamp and are the pages that actually document spatial. and the SPATIAL semicolon rule used throughout this correction pass). The review's suggested replacement URLs are for WS Pro and InfoAsset Manager — different products, not a newer home for the same ICM content. No swap made; if Autodesk migrates ICM's docs later, this section should be revisited.

Adopted — scoped down Confidence badges, lint checker, search, copy/download, provenance strip

All five now live on every card: a per-card confidence badge (Official / Schema-checked / Concept — see note below), a client-side lint line flagging the exact anti-patterns this log describes, a search bar filtering titles/SQL/fields together, and Copy SQL / Download .sql buttons. The network/version selector that rewrites field names live and the full query-builder wizard were scoped down to a single "Query Starter" widget (network + query-kind → correct skeleton) rather than built as originally proposed — a dynamic version-aware rewriter risks silently producing wrong SQL for a page whose entire purpose is correctness, and honesty about what's actually implemented matters more here than matching the full feature list.

Note on confidence badges

The review proposed four levels: Official / Verified / Schema-checked / Concept, with "Verified" meaning manually tested in a named ICM version. Nothing on this page has been run inside actual ICM software, so no card claims "Verified" — that would overstate what's actually been checked. Cards adapted directly from the Innovyze repo show Official; the 2D-zone category is marked Concept since its specifics (flood_type enum values, 2d_pt_id) weren't freshly re-confirmed against a primary source in this pass; everything else is Schema-checked — real field names, correct syntax per the sources above, not executed in live ICM.

Added — from an older chat transcript TITLE(), the RINDEX trap, and a repo-inventory correction

An older ICM SQL chat was supplied for mining. Most of it duplicated material already here, but three things came out of chasing its claims back to source:

1. TITLE(n, list) was missing from every reference on this page — including the comprehensive Autodesk function inventory added in the previous round, whose list-function section runs LEN / RINDEX / LOOKUP / LOOKUPFN / MEMBER / INDEX / AREF and stops. Yet TITLE() is used in twelve repo scripts across both the InfoWorks and SWMM folders, and is documented in the repo's own 03 Context Function Reference and SQL Combined.md. It's the function that turns a RINDEX bucket index into a range label, and it's what makes the official Bit Bucket scripts two lines long.

2. The old chat contained a worked bucketing query with a genuine off-by-one. Its list began with a leading 0, then labelled RINDEX = 0 as "< 100" and 1 as "100 - 200". Per the official semantics, a leading zero makes index 0 mean "below zero" and index 1 the first real bucket — so every label sat one bucket off. Nothing errors; the histogram just quietly misreports. Kept as a worked failure card rather than discarded, since the trap is easy to hit and the official TITLE() idiom sidesteps it entirely. Also worth noting: the newer folder READMEs describe RINDEX as returning "the largest value in the list less than or equal to the input" — that describes the net effect of RINDEX + TITLE together; RINDEX alone returns an index, not a value.

3. A claim on this page was wrong and is now corrected. Verifying the Bit Bucket scripts meant listing those folders, which showed the two newer "unified" folders contain only a README — no .sql. This page had previously stated they bundle both unit variants. Corrected in the repository map and the gap analysis above.

Added — July 21, 2026 Functions, Patterns & Export section (5 new cards)

A separately-supplied comprehensive SQL inventory (citing help.autodesk.com/view/IWICMS/2027/ENU/) filled real gaps this page didn't cover: the full function inventory, LIKE/MATCHES pattern operators, GROUP BY/HAVING, INTO FILE CSV export, and a precise NULL-equality rule. Direct re-fetch of the cited 2027 URL 503'd on every attempt — including a search-confirmed-real 2026 URL from the same path pattern, so this reads as a bot-block on Autodesk's newer help CDN rather than a sign the content is fake. Cross-checked instead: the document's GSUBST('01880132','01','ND') → 'ND88ND32' example matches, verbatim, the known-good help2.innovyze.com tech notes page already cited throughout this cookbook. On that basis the material was treated as verified and added as five new cards, plus a companion section in the linked context guide (now v3.1).

The 48 Core InfoWorks Scripts in the Innovyze Repo

These scripts live at innovyze/Open-Source-Support → 01 InfoWorks ICM → 02 SQL → 01 InfoWorks. Each folder contains a .sql file and often a README.md. Status: ● Existing = works as-is, ● Enhance = this cookbook proposes improvements, ● New = proposed addition. July 2026: the folder now also contains unified 0039 Conduit Diameter and 0040 Conduit Length Bit Bucket scripts (both unit systems, RINDEX bucketing, READMEs) — adopting this cookbook's unification proposal, so those four Enhance flags below are resolved.

IDScript NameWhat It DoesCategoryStatus
0001Split pipe every x chainageDivides conduits at regular intervals along their length — used for 1D-2D coupling resolution and detailed profilingtraversal● Enhance
0002Copy object data to a scenarioTransfers field values between network scenarios — core scenario management operationupdate● Enhance
0003Select all intersecting conduitsSPATIAL Cross search finding conduits that geometrically cross each other — pipe conflict detectionspatial● Enhance
0004Average a value of nodes within polygonSPATIAL Inside search with AVG aggregation — spatial averaging of node field values inside a polygonspatial● Existing
0005Select links with lowest invert levelFinds the deepest conduits — useful for identifying critical deep infrastructureselection● Existing
0006Globally Update SuDS ControlsBatch update SuDS/LID parameters across the entire network — green infrastructure managementupdate● Existing
0007Select 3 links upstream/downstream of nodeNetwork traversal with fixed hop count — localized inspection around a selected nodetraversal● Enhance
0008Select GIS points in subcatchmentSPATIAL Contains — finds GIS point objects inside subcatchment polygonsspatial● Existing
0009Select subcatchments x distance from nodeSPATIAL Distance — proximity-based subcatchment selection from a nodespatial● Existing
0010Get min river bed levels for cross sectionsQueries the cross-section structured array data to extract minimum bed elevation — river modelingarray / river● Enhance
0011Select within distance of selected objectBuffer-based selection radiating from current selection — proximity analysisspatial● Existing
0012Select subcatchments from selected nodesReverse lookup: finds subcatchments that drain to the currently selected nodesselection● Existing
0013Update Channel XS Manning nBatch update roughness values inside cross-section array data — river calibrationarray / river● Enhance
0014Select XS lines that cross a river reachSPATIAL Cross between XS lines and river reach geometry — river model setupspatial / river● Existing
0015Transfer inverts between crossing pipesCopies invert levels between intersecting conduits in different systems — conflict resolutionspatial / update● Existing
0016Spatial select polygons inside subcatchmentsSPATIAL Contains — polygon-in-polygon containment testspatial● Existing
0017Populate user text with connecting linksWrites comma-separated list of connected link IDs into node user_text fields using us_links.connected obj● Existing
0018Populate user text with max discharge from HD curveExtracts peak discharge from head-discharge table array data into user fieldsarray / results● Existing
0019Populate user_text_4 with downstream outfallsNetwork trace to outfall — tags every node with its downstream outfall ID using iterative traversaltraversal● Enhance
0020SQL CASE emulation using IF statementsDemonstrates ICM's IF/ELSE pattern since standard SQL CASE isn't supported — teaching exampleutility● Enhance
0021Create dummy node + DS conduit with orificeProgrammatic network object creation — demonstrates SQL INSERT for nodes, conduits, and orificescreation● Existing
0023Get river chainage from node (limited)Extracts chainage position for nodes on river reaches — limited functionality noted by authorsriver● Enhance
0024Call out info box with OK/CancelUser interaction pattern — shows dialog prompts within SQL scriptsutility / UI● Existing
0025Spatial query: sum GIS point values in subcatchmentSPATIAL Contains + SUM aggregation — adds up GIS point values for all points inside each subcatchmentspatial● Existing
0026Result moving averageSmooths simulation time-series results using tsr. prefix with a moving average window — one of only 2 tsr scripts in the repotsr results● Enhance
0027Travel TimeCalculates travel time along conduit paths using length/velocity — time-of-concentration estimationhydraulics● Enhance
0028Select all links downstream of a nodeFull downstream trace — selects every link between starting node and outfalltraversal● Existing
0029Connect Gully 2D to nearest node via orificeCreates orifice connections from 2D gully objects to nearest manholes — the only 2D-focused SQL in the repo2D / creation● Enhance
0030Select nodes where subcatchments discharge toForward lookup: finds nodes receiving flow from subcatchmentsselection● Existing
0031Populate node user text with US/DS conduit invertsWrites upstream and downstream conduit invert levels into node user fields using us_links. / ds_links.connected obj● Existing
0032Select links with same US and DS nodesFinds parallel/duplicate conduits — common data quality issue after importsQA/QC● Enhance
0033Set ds_invert interpolating pipe soffitsInterpolates downstream inverts to maintain consistent pipe soffit alignmentupdate● Existing
0034Delete duplicate default tablesCleans up redundant default table entries — database housekeepingQA/QC● Existing
0035Kutter's Formula CalculationComputes pipe flow capacity using Kutter's equation — alternative to Manning's for older systemshydraulics● Enhance
0036Select all links without Outfall downstreamIdentifies dead-end network branches that never reach an outfall — connectivity/topology checktraversal / QA/QC● Existing
0037aSQL Sim ParametersReads and displays simulation parameter settings in a grid — configuration auditutility● Existing
0037bUpdate Channel XS Manning nDuplicate of 0013 with slight variations — batch XS roughness updatearray / river● Existing
0038WhenMax depth and flow for LinksExtracts timing of peak depth and flow using sim.max_ and WHENMAX — one of only 2 sim-results scripts in the reposim results● Enhance
0039SQL Bit Bucket for Diameter (inches)Bins conduits into diameter ranges for histogram analysis — imperial unitsQA/QC● Enhance
0040SQL Bit Bucket for Diameter (mm)Same as 0039 but metric — bins conduits by diameter in millimetresQA/QC● Enhance
0041SQL Bit Bucket for ICM Length (feet)Bins conduits into length ranges — imperialQA/QC● Enhance
0042SQL Bit Bucket for ICM Length (meters)Bins conduits into length ranges — metricQA/QC● Enhance
0043Set subcatchment trade flow profilesAssigns trade/commercial flow profiles to subcatchments in bulk — wastewater modelingupdate● Existing
0044Select links connected to selected nodesBasic connectivity selection — all links touching the currently selected nodesselection● Existing
0045Select all links upstream of selected nodesFull upstream trace — selects the entire contributing network above selected pointstraversal● Existing
0046Set Runoff Surface fixed runoff coefficientApplies fixed runoff coefficients to subcatchment runoff surface array entriesarray / update● Existing
0047Create custom surcharge results tableBuilds a formatted surcharge summary grid from sim.max_ results — reportingsim results● Enhance
0048Insert head-discharge tables and curve dataProgrammatically creates HD tables with curve point data — avoids manual entry of rating curvescreation / array● Existing
0049Calculate Runoff Area ContributionsComputes contributing runoff areas from subcatchment data — hydrology setup verificationhydraulics● Existing

✓ The Official 03 Context Folder (Now Live)

Update, July 10, 2026: The 02 SQL/03 Context folder now officially exists in the repo — a 12-file modular LLM context system maintained by Alex Grist (Squark89), last updated July 7, 2026. It includes Instructions.md (loading rules, token-budget tiers, network-type decision protocol — the designated starting point), InfoWorks_ICM_SQL_Lessons_Learned.md (anti-patterns, load-first), Syntax / Function / Pattern / Error / Glossary / Tutorial references with PAT_SQL_XXX_NNN cross-link IDs, a three-file schema set (Schema_InfoWorks at 2,277 lines, Schema_SWMM at 1,162 lines, Schema_Common), a Database_Fields_Guide that routes unknown fields to the Autodesk Product Help MCP, and a human-only 00 Reference/ folder containing the full SQL language reference and the ICM Time Varying SQL document (source of the DURATION / INTEGRAL / WHEN-clause material below). Find it at innovyze/Open-Source-Support → 02 SQL → 03 Context.

The table below maps the 12 context files this cookbook originally proposed against what the official system now delivers — most are covered; the remaining gaps are candidates to contribute upstream:

Proposed Context FileContent DescriptionOfficial Coverage
CTX-001-SPATIAL-keyword.mdComplete reference for the SPATIAL keyword: all 5 geometry types (Cross, Inside, Contains, Distance, Nearest), all 4 layer types (Network, Background, GIS, Spatial rain), distance parameter syntax, how SPATIAL NONE resets spatial mode within IF/WHILE blocks, and which geometry combinations are valid (the table from the help file). Include worked examples of each combination.● Covered — Syntax_Reference + Pattern_Reference handle SPATIAL; the geometry×layer validity matrix remains thinner than proposed
CTX-002-sim-results.mdReference for the sim. prefix: all available fields for nodes (max_level, max_flooddepth, max_floodvolume, max_flvol, etc.), links (max_us_depth, max_us_flow, max_us_vel, max_us_froude, max_ds_*), and subcatchments. How to load results into the GeoPlan. The difference between max results and current-timestep results. How the time control "show maximum" toggle affects sim. values.● Covered — object-specific sim.* field tables live in Schema_InfoWorks / Schema_SWMM
CTX-003-tsr-time-series.mdReference for the tsr. prefix (time series results): all aggregate functions (MIN, MAX, AVG, SUM, COUNT, WHENMAX, WHENMIN, ANY, ALL, FIRST, LAST), how AVG is time-weighted, how COUNT works with boolean expressions, tsr.timestep_no for windowed queries, IIF() for conditional filtering within tsr expressions. The tsr2. prefix for comparing two simulations.● Partial — Function_Reference + Schema_Common cover aggregates, but DURATION / INTEGRAL / EARLIEST / LATEST / WHEN clauses sit only in the human-only 00 Reference/ICM Time Varying SQL.md — worth promoting into the LLM set
CTX-004-sim2-tsr2-comparison.mdHow to load a second simulation as alternate results (sim2./tsr2.): the right-click → "Open As → As alternate results for comparison" workflow. Fields available, how to compute deltas, percentage changes, and timing shifts between scenarios. Limitations (both sims must have same network/timesteps).● Gap — sim2./tsr2. alternate-results comparison not yet documented in the official set; prime upstream contribution
CTX-005-connected-objects.mdComplete reference for us_links., ds_links., us_node., and ds_node. prefixes (node→link and link→node directions respectively — corrected from an earlier draft that used an invented joined. keyword). How aggregate functions work across multiple connected objects. Combining connected-object fields with sim. results (e.g., us_node.sim.max_floodvolume).● Covered — Syntax_Reference implicit joins + Schema_Common relationship paths
CTX-006-array-data.mdHow to query Value arrays (bends, vertices) vs Structure arrays (cross-sections, runoff surfaces, HD curves). Available aggregate functions for arrays (MIN, MAX, AVG, SUM, COUNT, ANY, ALL). Which array fields are accessible by name vs. which require positional access. Specific field names for cross-section arrays (section_array.elevation, section_array.chainage, section_array.roughness_N), runoff surface arrays, and HD curve arrays.● Covered — blob/array rules in Syntax_Reference and Function_Reference
CTX-007-SWMM-vs-InfoWorks.mdDifferences between querying InfoWorks (hw_) and SWMM (sw_) networks in SQL. Object type names in the SQL dropdown vs. Ruby table prefixes. Which fields exist in SWMM but not InfoWorks (shape, barrels, flap_gate) and vice versa (system_type, scenario fields). SWMM-specific curve tables and how to query them. The FROM keyword override for SWMM object types.● Covered — Schema_Common carries the IW-vs-SWMM difference table; Instructions.md enforces the never-mix rule
CTX-008-2D-zone-SQL.mdHow SQL interacts with 2D zones: setting flood_type values ('Stored', '2D', 'Gully 2D', 'Outfall 2D', 'Lost'), the flooding_discharge_coeff field, head-discharge table assignment, the [2d_pt_id] field for subcatchment 2D inflow points, and using SPATIAL Inside Network [2D Zone] to select objects within a 2D zone polygon. Workflow sequence for 1D-2D coupling via SQL.● Gap — flood_type / 2d_pt_id / 1D-2D coupling workflow not yet in the official context files; candidate to contribute
CTX-009-PROMPT-variables.mdUser input patterns: PROMPT LINE $var 'Label', PROMPT LINE $var 'Label' RANGE min max, PROMPT LINE $var 'Label' LIST $listvar, PROMPT DISPLAY. Variable types (LET, LIST), AREF() for list access, SELECT ... INTO $var for capturing query results, and the macro system (User Macros tab) as a find-and-replace engine.● Covered — Pattern_Reference PROMPT templates; macro-system detail still lives with sancarn
CTX-010-SQL-field-reference.mdComplete field name listings for each object type: All Nodes (node_id, x, y, ground_level, chamber_floor, chamber_roof, shaft_area, flood_type, system_type, user_text_1–10, user_number_1–10, etc.), Conduit (us_node_id, ds_node_id, link_suffix, conduit_length, conduit_width, conduit_height, us_invert, ds_invert, gradient, roughness_N, shape, system_type, etc.), Subcatchment (subcatchment_id, node_id, total_area, population, etc.). With data types and units.● Covered ++ — delivered beyond proposal: 2,277-line InfoWorks schema, 1,162-line SWMM schema, plus MCP Help routing via Database_Fields_Guide
CTX-011-FROM-keyword.mdHow to use SELECT FROM [Object Type] to override the dropdown and query multiple object types in one script. Object type names for InfoWorks (Conduit, All Nodes, All Links, Subcatchment, Orifice, Weir, Pump, etc.) and SWMM (SWMM Conduit, SWMM Node, SWMM Subcatchment, SWMM Weir, SWMM Orifice, SWMM Pump, etc.). Using INSERT to create new objects in a different table.● Covered — object manifests in both schema files
CTX-012-multi-clause-patterns.mdAdvanced multi-clause SQL patterns: chaining SELECT/DESELECT/UPDATE with semicolons, CLEAR SELECTION, SELECT SELECTED to filter within existing selection, IF/ELSEIF/ELSE/END blocks, WHILE/END loops, nesting blocks, and how SPATIAL scope works within IF/WHILE blocks. The "master checklist" pattern of chaining validations.● Covered — Pattern_Reference PAT_SQL IDs + Tutorial_Context workflows

Where the Existing Scripts Live

ICM SQL Feature Coverage Gap Analysis (Re-verified July 10, 2026)

Re-audited against a fresh clone of the repo (50 InfoWorks + 45 SWMM scripts). Several gaps this cookbook identified have since been closed officially — marked ✓ — while others remain open contribution opportunities:

Official ICM SQL Documentation

Key ICM SQL Syntax Patterns Used in This Cookbook

How to Use These Scripts