The notebook / engineering in practice
Cascading Dashboard Parameters, Resolved on the Server.
Inside Datapipelines: how a server-side parameter engine resolves cascading filters, resets stale selections, and keeps dashboard client code reusable.
A dashboard starts with three innocent controls: country, state, and city.
Then someone changes the country. The state list must refresh. The selected state may no longer exist. The city list must wait for the replacement state. Its selection may need replacing too. Meanwhile, the charts still show results from the previous selection.
The controls occupy a small part of the screen. Getting their behavior right can occupy a surprisingly large part of the implementation.
In Datapipelines, we made this a reusable platform capability. Authors declare the parameters, their sources, and their dependencies. A server-side engine computes the resulting form state. A shared browser runtime renders it and coordinates dashboard actions.
Here is how that works, including the distinction that matters most: refreshing dependent choices and refreshing dashboard data are two separate operations.
Implementation snapshot: based on the source reviewed on 8 November 2026. Examples use illustrative geography data and artifact names. This describes the implementation, not a benchmark or a claim about the revision running in every deployment.
What is this pattern called? #
Cascading parameters is the clearest name for it. Microsoft uses that term for report parameters whose available values depend on other parameter selections. Its examples include country, state, city, and postal code. Microsoft: cascading parameters.
You will also encounter cascading filters, dependent dropdowns, and chained variables. Grafana calls dependent query variables chained variables, with linked variables and nested variables as alternative names. Grafana: chained variables.
I use parameters here because the controls do more than filter rows. They can supply pipeline inputs, compute initial values, and govern whether another control is hidden or disabled. A dropdown is one presentation of that behavior.
The hard part is keeping the whole form consistent #
Consider a current selection of USA → NJ → NWK. The user chooses Canada.
Simply reloading the state dropdown leaves several decisions unresolved:
- Can the old state remain selected if it still belongs to the new option list?
- If it does not, should the engine use a default, the first option, or no value?
- Does the city query receive the stale state or the newly resolved state?
- What happens when a selector fails or returns no options?
- Can a refresh button run while the cascade is still resolving?
- Which charts become stale when a dependent value changes automatically?
These are shared rules. Reimplementing them in each dashboard produces slightly different behavior every time someone adds a control.
Our approach is to represent the form as a parameter set: a named, versioned definition whose parameters have types, sources, presentation hints, and explicit dependencies.
Declare the relationship once #
A parameter is either an INPUT, for a value someone types, or a SELECT, for a value chosen from options. Selects support single or multiple selections. Options can be constants or come from a pinned SQL template and datasource.
For example, this is an illustrative state selector inside a parameter set’s parameters array. The country parameter, datasource, and template version must exist separately:
{
"name": "state",
"label": "State / province",
"type": "STRING",
"kind": "SELECT",
"cardinality": "SINGLE",
"required": true,
"depends_on": ["country"],
"source": {
"template": {
"id": "sales/filters/states.sql",
"version": 1
},
"datasource": "warehouse"
}
}
Its SQL template could contain a query like this, assuming the illustrated table and a dialect supporting these expressions:
SELECT
state_code AS value,
state_name AS display_value,
FALSE AS is_default
FROM geography_states
WHERE country_code = :country
ORDER BY state_name, state_code
The selector returns three columns: value, display_value, and is_default. The value is what downstream consumers receive; the display text is what the user sees. The ordering also matters because the first option can become the fallback selection.
The parent value travels as a bound SQL parameter. Authors do not interpolate a selected country into SQL text. Save-time validation checks that a parameter referenced by a selector bind is declared in depends_on; hidden and disabled expressions must declare their parameter references there too.
That last detail is deliberate: the engine builds its graph from explicit depends_on edges. It checks references against that graph rather than silently discovering extra dependencies from arbitrary SQL.
Save-time validation rejects self-dependencies, unknown parents, and cycles. A definition such as country depending on city while city depends on country cannot become an executable parameter graph.
For a city query that needs both country and state, declare both parents. A transitive relationship does not automatically make every ancestor available as a direct query bind.
One evaluation, ordered by dependencies #
The engine evaluates the whole parameter set for each evaluation request. It does not accept a changed field and run only that field’s descendants as an incremental cache update.
On the first evaluation, the selection object is empty. On subsequent evaluations, the client sends the full current selection map, including values that may have become stale. That gives the server enough information to preserve valid choices and replace invalid ones consistently.
For our three-control example, the relevant part of the request after changing country is:
{
"selections": {
"country": "CAN",
"state": "NJ",
"city": "NWK"
}
}
This is the selection payload, not the complete dashboard request envelope. The dashboard runtime adds its configuration and instance identifiers.

The server first checks submitted values against their declarations. It then creates a coroutine for each parameter in topological order. Each coroutine awaits its direct parents and evaluates using their effective, resolved values.
In this example:
- Country resolves to
CAN. - State loads the options for
CAN. IfNJis absent, the engine chooses the configured fallback or first option, sayON. - City loads options using the resolved parents,
CANandON. It does not query using the staleNJselection. - If
NWKis absent, city resolves to its fallback, sayTOR. - The response contains the complete parameter set in its authored display order.
A parameter that is both a child and a parent is evaluated once within that evaluation. Independent branches can proceed concurrently, subject to the shared selector pool’s capacity. For example, an unrelated channel selector need not wait for the country–state–city chain.
The dependency order and the display order are separate concerns. The engine can resolve a graph correctly while the UI presents controls in the order the author chose.
Refreshing options also means resolving selections #
Replacing an option list is only half the operation. The engine also decides which value survives.
For a single-select parameter, the selection policy is:
| Situation | Result |
|---|---|
| The submitted value is in the refreshed options | Keep it, with origin: "client". |
| No submitted value survives, and the configured default is available | Use the default, with origin: "default". |
| No usable default exists, but options remain | Use the first option, with origin: "first". |
| There are no options | Resolve to null, with origin: "none"; a required parameter reports an error. |
The default candidate comes from default_value when declared, otherwise from the option marked is_default. It must still belong to the current options to be used. If an old submitted choice is dropped, reset: true makes that explicit.
For a multi-select, the engine keeps the submitted members that remain valid, in the client’s order. If some disappear, the survivors remain selected and reset is true. If none survive, it falls back to available configured defaults, then the first option as a one-item selection.
A stale but correctly typed option is different from malformed input. NJ disappearing from the Canadian option list causes a normal reset. A value with the wrong type or an input that violates a constraint produces a parameter error.
There is also an intentional convention around empty selections: an omitted value, null, and an empty multi-select array mean nothing chosen, so fallback rules apply. An explicit “All” or “None” choice must be modeled as an option with a sentinel value and handled by the downstream query.
For an INPUT, a valid submitted value wins. Otherwise the engine uses its sourced value, then a literal default if available. A sourced input is recomputed even when the user’s typed value remains selected; the separate computed_default tells the renderer what the current suggested value would be.
Return enough state for a generic renderer #
The server returns more than a value map. Each parameter includes its definition, transitive dependents, and evaluated state:
| State field | Why the client needs it |
|---|---|
value | The resolved value to display and submit. |
options | The available choices, including labels and default marks. |
origin | Whether the value came from the client, a default, the first option, a source, or nowhere. |
computed_default | The current fallback or sourced initial value. |
reset | Whether the engine dropped a submitted choice. |
hidden / disabled | The evaluated interaction flags. |
errors | Structured failures associated with this parameter. |
At the top level, values contains the canonical consumer payload. valid is false if any parameter carries an error.
Hidden and disabled parameters retain their values and remain in that payload. These flags govern interaction; they do not erase business inputs or act as authorization rules.
The dashboard response additionally identifies parent parameters, supplies a parameter revision, and applies configured dashboard visibility/enabled overrides. The shared client therefore has the metadata to coordinate interactions without implementing a country-specific or state-specific dependency algorithm.
What stays in the browser? #
The browser still has real work to do. The simplification is that this work belongs to one reusable runtime and adapter rather than bespoke code for each dashboard.
The adapter renders controls from the returned definitions, reads current selections, emits commit events, and displays errors. The runtime submits the full selection map, applies the accepted response, and coordinates refresh actions.
There is a useful optimization in the dashboard path: a committed change to a parent parameter triggers reevaluation. A leaf change can simply mark affected visualizations stale. A parameter-bound action, or recovery from an invalid parameter state, also triggers reevaluation. The separate Parameter Sets page reevaluates on every commit.
While a dashboard parameter evaluation is pending, the runtime refuses another parameter evaluation and blocks refresh actions. It keeps that gate closed until the response has been rendered and accepted, or the attempt expires. Deadline and ownership checks stop late responses from becoming the accepted runtime state.
That is different from the standalone Parameter Sets page, which supersedes an older pending evaluation when a new selection arrives and closes its stream. These two interaction policies share the same evaluator, but they are not identical browser behavior.
A dashboard author can add another dependent selector by changing the parameter definition and its source template. The shared client already understands the response contract. There is no new onCountryChange → fetchStates → chooseState → fetchCities chain to write for that dashboard.
When do the charts refresh? #
After a parent evaluation is accepted, the runtime marks visualizations affected by the original parameter change as stale. It also compares the previous and current resolved values: if a child changed automatically, that child’s affected visualizations become stale too.
Authors can configure parameter_scopes to map parameters to affected groups, including nested groups. Without a scope for a changed parameter, the client conservatively marks all visualizations stale.
Stale does not mean a query has already rerun. It means the displayed result belongs to an earlier selection.
Dashboard authors choose when to request new data:
- Bind a refresh action to a parameter control to refresh after its committed change has been evaluated.
- Provide an explicit action button, such as Apply, to let users make selections before requesting new results.
Refresh actions can target all visualizations or a specified list. This is separate from parameter_scopes: one describes what became stale; the other describes what the action should refresh.

On a visualization refresh request, the server evaluates the submitted selections again. If the resulting parameter state is invalid, it refuses the refresh before starting its source work. Otherwise, it plans the requested refresh using the resolved values.
The earlier form response is therefore not a permanent authorization to run with old options. Source data may change between interactions, and the refresh boundary resolves the selections afresh. This also means the form evaluation and the later refresh are separate evaluations, not one database snapshot.
Failures and limits are part of the design #
A selector can fail even when the dependency graph is correct. A datasource can become unavailable, a query can exceed its timeout, or an option list can grow beyond the allowed size.
For expected selector failures, the engine attaches an error to the affected parameter and still returns the form. A failed select has no options and resolves to null; its children evaluate against that null value. They are not automatically skipped, so selector authors must think about what null parents mean in their queries.
Whole-request failures have their own boundaries. An unknown selection key is rejected. An evaluation has an overall deadline. An oversized response is refused rather than silently truncating the options.
SQL selectors run through the read-only query path, with statement limits and a bounded shared pool. When the pool is full, a parameter receives a saturation error. A timed-out database driver may take longer to stop, so its worker retains its capacity slot until it actually exits; the HTTP deadline is not a promise that the database instantly canceled the query.
These choices make the cost visible. Whole-set evaluation is simpler to reason about, but it can rerun selectors whose inputs did not change. A long dependency chain still has sequential work. Concurrent independent branches help, but they do not remove query cost, indexing needs, or the importance of keeping option lists manageable.
The platform payoff #
The visible feature is a set of controls that stay in sync. Underneath it is a reusable contract for dependencies, typed values, source queries, defaults, errors, and refresh timing.
That is the part I enjoy building: establish the conventions once, express each dashboard’s behavior through configuration, and let a shared implementation do the repetitive work.
In Datapipelines, the server owns dependency resolution and value policy. The browser owns presentation and interaction coordination. A new dashboard can reuse both, while its author concentrates on what the parameters mean and where their values come from.