A default FEWS configuration for a system with hundreds of gauges can drift into
DisplayGroups or IdMap files thousands of lines long — one hand-copied block
per station, with the only difference between blocks being an id. That’s not a
consequence of the schema; it’s what happens when you reach for the explicit
list form (display, location, moduleInstance) everywhere, instead of the
generating forms sitting next to it in the same schema.
This page collects those generating forms in one place — the answer to “is there
a tool for this?” the next time you’re about to paste a block for the fiftieth
time. Every mechanism here is sourced from the shipped .xsd schemas in this
repo; none of it is a workaround, it’s built-in.
FEWS config doesn’t have one DRY mechanism — it has four different kinds, and
picking the wrong kind for the problem is why people give up and paste. Keep
these categories straight; every tool below is one of them.
Kind
What it does
Ask yourself
Reference
Define a group once (a location set, a module instance set), then point many config blocks at its id instead of relisting members.
“Am I listing the same ids in more than one file?”
Generate
Author one block; FEWS expands it into many at config-load time (or run time) — one per member of a set.
“Is every block here structurally identical except for one id?”
Externalize
Move the repeating data out of XML entirely — a CSV, shapefile, or database table supplies it.
“Is this data (not structure) — hundreds of rows of similar-shaped facts?”
Split
Physically break one file into several smaller ones by topic; FEWS merges them back into one logical config.
“Is this file just long, not repetitive?”
The rest of this page is organized by config area, but every entry is tagged
with which kind it is.
locationLoop is a “loop” in the sense of config-time expansion, not a
runtime for loop with iteration logic. It’s a template macro: FEWS reads the
display/displayGroup element(s) nested under it once, then generates one
copy of that subtree per location in a location set when it loads/builds the
Explorer tree. There’s no index variable, no conditional branching, no
per-iteration logic — it’s closer to “stamp out N copies” than to a
programming loop. (Compare that with loopLocationSetId
further down this page, which is a genuine runtime loop.)
It nests as an optional child of displayGroup, sitting alongside the
display/displayGroup/displayGroupId choice block in
displayGroups.xsd
— so a displayGroup can either be hand-authored per location, or driven by
locationLoop, using the same child elements either way.
Why it condenses DisplayGroups files. Without locationLoop, if you have
a plot (e.g. DischargePlot) you want shown for 50 gauges, you’d hand-write
50 <display> blocks, each with its own <name> and <locationId>:
<displayGroupname="Discharge gauges">
<displayname="River Gauge 01">
<locationId>Gauge01</locationId>
<plotId>DischargePlot</plotId>
</display>
<displayname="River Gauge 02">
<locationId>Gauge02</locationId>
<plotId>DischargePlot</plotId>
</display>
<!-- ...48 more, one per gauge -->
</displayGroup>
With locationLoop, you replace all of that with one loop over a location set:
<displayGroupname="Discharge gauges">
<locationLoop>
<locationSetId>AllDischargeGauges</locationSetId>
</locationLoop>
<displayname="%LOCATION%">
<plotId>DischargePlot</plotId>
</display>
</displayGroup>
FEWS then generates one display node (named after each location) for every
member of AllDischargeGauges at config-load time — the file stays fixed-size
no matter how many gauges are in the location set.
The LocationLoopComplexType fields:
Element
Meaning
locationSetId (required)
The location set to iterate over.
includeChildren (optional, default false)
Also loop over child locations of each looped location.
locationRelationId (optional, repeatable)
Also pull in locations related to the loop location via one of the named location relations.
Available since build 2015.01 — don’t rely on it against older FEWS builds.
singleLocationDisplays / singleParentLocationDisplays — one flat display per location (Generate)
These sit in the same choice as display itself (they’re the same
DisplayComplexType), so they accept every field a normal display does —
plotId, locationSetId, styling, the lot. The difference is purely in how
many display nodes come out the other end:
One display node. Every gauge in AllDischargeGauges is available inside
it (pick a station from the map/location panel to see its series) — a single
multi-location chart.
<displayGroupname="Discharge gauges">
<singleLocationDisplaysname="%LOCATION%">
<locationSetId>AllDischargeGauges</locationSetId>
<plotId>DischargePlot</plotId>
</singleLocationDisplays>
</displayGroup>
Same two fields, but FEWS emits one separate display per location in the
set — each fixed to a single station. Use this when you want each gauge to be
its own addressable node (e.g. for explorerLocationId click-through, or a
flat picklist of stations) rather than one shared multi-station chart.
singleParentLocationDisplays is the same idea one level up the location
hierarchy: for each parent location, it generates a display showing that
parent plus its children (a reservoir and its inflow gauges, a basin and its
sub-catchments) — provided the parent/child relationship is declared on the
location set. Reach for it instead of locationLoop when
you want flat, individually-addressable displays rather than a nested
displayGroup tree.
displayGroupId — reference a display group defined elsewhere (Reference)
Since 2023.01. Include a displayGroup that’s already defined — in this file
or another DisplayGroups*.xml — by id, instead of copy-pasting it into a
second location in the tree:
<displayGroupname="By basin">
<displayGroupname="Basin A">
<displayGroupId>DischargeGauges</displayGroupId>
</displayGroup>
</displayGroup>
Root-level display groups referenced this way are placed only where
referenced, not also kept as a separate top-level tree.
The most basic reuse: define subplot/line/axis styling once under <plot id="…">, then every display that wants that look points at it with
plotId and supplies only its own location constraint. This is what makes
locationLoop and singleLocationDisplays worth using at all — the plot
definition itself never repeats.
groupBy — auto-split a filter node by attribute (Generate)
moduleInstanceIdPattern accepts * and ? wildcards (since 2015.01) — so one
line can match every module instance following a naming convention, no list at
all. Members can also be listed explicitly with moduleInstanceId if there’s
no convention to exploit.
Once declared, any timeSeriesSet-shaped block — in a filter, a display, an
import, an export, a threshold value set — can reference it instead of listing
module instances inline:
Field (in TimeSeriesSet/TimeSeriesFilter)
Meaning
moduleInstanceId (repeatable)
Explicit list — the thing you’re trying to stop writing.
moduleInstanceIdPattern (repeatable, */?)
Same wildcard matching, inline, without a named set.
moduleInstanceSetId
Query one module instance available from the named set.
filterModuleInstanceSetId
Query all module instances in the named set that are available for this series.
See sharedTypes.xsd
(ModuleInstanceIdPatternSequence, ModuleInstanceIdsChoice,
TimeSeriesSetComplexType) for the exact choice structure — it differs
slightly between a plain timeSeriesSet and a criteria-based timeSeries
filter.
Full detail in Map external IDs.
Two Generate-kind tools stop IdMap files from becoming one <location> line
per gauge:
<locationIdPattern
internalLocationSet="AllDischargeGauges"
internalLocationPattern="RiverGauge_*"
externalLocationPattern="GAUGE-*"/>
* maps the whole set by transformation (RiverGauge_01 ⇆ GAUGE-01, …), and
parameterIdFunction / locationIdFunction / qualifierIdFunction go further
— they derive the external id from an attribute on the internal object
(e.g. @EXTERNAL_ID@, pulled from that location’s own attribute value) instead
of a shared naming pattern.
Full detail in Thresholds & warnings.
ThresholdValueSets supports a *Function sibling for nearly every field that
would otherwise need to differ per location:
Instead of
Use
Effect
value
valueFunction
e.g. @FLOOD_WATCH@ — the number comes from that gauge’s own location attribute.
description
descriptionFunction
Per-location wording, same substitution.
levelThresholdId
levelThresholdIdFunction
Which threshold definition applies, per location.
One thresholdValueSet scoped to a locationSetId then produces a different
number at every location — sourced from the location’s own attribute row, not
from a per-gauge XML block. If the attribute is missing for a location, that
field is silently skipped for that location (not an error) — validate your
attribute coverage before relying on this.
This is the highest-leverage tool on this page and the least documented one:
multiple module instances can share a single module config file.
From workflow.xsd’s
own annotation on moduleConfigFileName:
By default the config file name equals the module instance id. Multiple
module instances can share the same config file by using properties. The
config file is used as template and the $key$ are replaced by the property
values.
In practice: instead of fifty near-identical RainfallRunoffModel_<gauge>.xmlGeneral Adapter files, write one template with
$-delimited placeholders, and give each activity its own tiny property
block:
One module config file, no matter how many gauges you add — only the
workflow’s property blocks grow, and each is two lines. This composes with
everything above: the timeSeriesSet inside the template can use
moduleInstanceSetId/filterModuleInstanceSetId too.
Workflow-level loops — the closest thing to an actual for loop
locationLoop and singleLocationDisplays above are config-time
expansion: FEWS reads one block and multiplies it while loading the config.
A workflow activity’s loopLocationSetId is different — it’s evaluated
at run time, once per forecast run:
Inside that activity’s (templated) module config, refer to the current
iteration’s location with $LOOP_LOCATION_ID$. Pair it with a
loopLocationAttribute workflow property to pull an attribute of the
current loop location into another $key$ — e.g. exposing that location’s
MODEL_NAME attribute as $MODEL_NAME$ for the templated config to use.
Since 2015.01; not supported combined with an ensemble loop on the same
activity.
The ensemble block on an activity is the same run-time-loop idea for
ensemble members: runInLoop (default true) runs each member as a separate
pass, and ensembleMemberIdRegularExpression (e.g. ^Q.*) selects which
members to loop over by pattern instead of listing them. See Workflows &
scheduling.
Externalize: CSV and other data files instead of hand-written XML
The same shape repeats across several RegionConfigFiles: a csvFile (or
sibling esriShapeFile/geoJsonFile/table) element that supplies rows of
data instead of one XML element per row. Reach for these once a hand-written
list is really data, not structure.
This is an Externalize-kind tool, not a Generate one: the CSV rows still
have to exist somewhere, but a spreadsheet of 500 rows is a much smaller
maintenance burden than 500 near-identical XML blocks, and it’s editable by
someone who doesn’t know the schema.
Not every long file is repetitive — sometimes it’s just organized wrong.
FEWS’s file discovery convention
(<TypeName> <version> <description>.xml) exists specifically so you can give
several files the same type and version but a different description, and
FEWS loads and merges all of them into one logical config — it does not force
one physical file per config type:
Config/RegionConfigFiles/
├── Filters 1.00 discharge.xml
├── Filters 1.00 levels.xml
└── Filters 1.00 rainfall.xml
All three load and merge into one filters tree (their <filter> nodes are
siblings). This is the direct answer to “how do I stop this one file from
being 10,000 lines”: split by topic (river basin, module, display purpose),
not by inventing new XML structure. It composes with everything else on this
page — a Filters 1.00 discharge.xml full of groupBy and locationSetId
references is both shorter and fewer lines than a hand-listed one.
Config-time expansion vs. run-time loop, mixed up.locationLoop and
singleLocationDisplays happen once, when FEWS loads config — adding a
location to the set and reloading regenerates the tree, but nothing re-runs
per location during a forecast. loopLocationSetId is the only mechanism
here that re-executes at run time.
Wildcards and patterns fail silently. A moduleInstanceIdPattern,
locationIdPattern, or *Function that matches nothing behaves exactly
like the explicit-list mismatches covered in Filters &
display groups and ID
mapping — an empty result,
no error. Test bulk tools against one known-good id before trusting them
across a whole set.
$KEY$ vs %KEY% confusion. A property referenced with the wrong
delimiter for its context isn’t substituted — it’s left in the config as a
literal string (a location id that’s literally $LOC_ID$, say), which then
fails to resolve downstream and looks like an unrelated bug.
loopLocationSetId and ensemble loops don’t combine on the same
activity — split them into separate activities (or a nested workflow) if
you need both.
A missing attribute silently drops that location.valueFunction,
*IdFunction, and loopLocationAttribute all depend on the referenced
attribute existing on every location they touch; when it’s absent for one
location, that location is skipped for that field, not flagged.
Splitting files doesn’t fix a version conflict. Same type, same
description, different version → highest wins, others ignored. Same type,
different description → all load and merge. Know which situation you’re in.