The Magic of Flexible Columns in Compact Form 2.0
“Perfection is achieved not when there is nothing more to add, but when there is nothing left to take away.” — Antoine de Saint-Exupéry
The Magic of Flexible Columns in Compact Form 2.0
“Perfection is achieved not when there is nothing more to add, but when there is nothing left to take away.” — Antoine de Saint-Exupéry
We’ve all built that one values table where the business asks for “just five or six important fields,” and three weeks later half the team wants ten more added to it. Instead of going back and forth on which fields make the cut, why not just let the user decide? One person wants to see 10 columns, another wants 20 — same table, same query, no SAQL required.
This is where a lot of us reach for .asProjection() and quietly convert the step to SAQL. It works, but it also means giving up the readability of compact form, and honestly, most of the time we don't need to. Compact Form 2.0 can handle a variable number of columns just fine — we just need to bind in the right place.
We’ll build this out using the Chicago Taxi Trips dataset we’ve used in a few of our other blogs, since it has more than enough fields to make a column picker actually useful.
The Setup
Here’s the scenario: our taxi dataset has a long list of fields — Trip ID, Trip Start Timestamp, Trip Seconds, Trip Miles, Pickup Community Area, Dropoff Community Area, Fare, Tips, Tolls, Extras, Trip Total, Payment Type, Company, and a handful of location fields. Nobody wants all of these in a table at once, and different users care about different slices of it. A dispatcher might want the location and timing fields. Finance wants Fare, Tips, Tolls, and Trip Total. We want one table that works for both, without building two dashboards.
The trick is separating two things that usually get bundled together: what the query fetches, and what the widget displays. The query pulls every field that could possibly be shown. The widget then decides, based on what the user picked, which of those fields to actually render — and that second part is where the binding lives.
Step 1: Build the Values Table
We start with the query itself. Create a values table step and load in the relevant fields — Trip ID, Fare, Tips, Company, and whatever else is a real candidate for this table. This is the part that trips people up, so it’s worth saying plainly: the query doesn’t change based on the selection later on. It fetches every field, every time, regardless of whether the user ends up showing 10 of them or 20.

Value Table
"query": {
"aggregateFilters": [],
"columnGroups": [],
"columnTotals": [],
"limit": 100,
"orders": [],
"rowTotals": [],
"sourceFilters": {
"Taxi_Trips": {
"filters": [
[
"Company",
[],
"isnotnull"
]
]
}
},
"sources": [
{
"columns": [
{
"field": "Company",
"name": "Company"
},
{
"field": "Pickup_Community_Area",
"name": "Pickup_Community_Area"
},
{
"field": "Tolls",
"name": "Tolls"
},
{
"field": "Extras",
"name": "Extras"
},
{
"field": "Fare",
"name": "Fare"
},
{
"field": "Tips",
"name": "Tips"
},
{
"field": "Trip_Miles",
"name": "Trip_Miles"
},
{
"field": "Trip_Total",
"name": "Trip_Total"
},
{
"field": "Trip_End_Timestamp",
"name": "Trip_End_Timestamp"
},
{
"field": "Trip_Start_Timestamp",
"name": "Trip_Start_Timestamp"
},
{
"field": "Dropoff_Community_Area",
"name": "Dropoff_Community_Area"
},
{
"field": "Payment_Type",
"name": "Payment_Type"
},
{
"field": "Trip_ID",
"name": "Trip_ID"
}
],
"filters": [],
"groups": [],
"joins": [],
"name": "Taxi_Trips"
}
]
},
No bindings here at all. It’s a plain values table, full width, every field we might want to show up somewhere in the picker. We’re deliberately over-fetching, because that’s what buys us the flexibility in the next step.
Step 2: Build the Column Picker to Match
Now that the query has our full field list locked in, we build the picker to mirror it exactly — same fields, same API names, nothing more and nothing less. In the Designer, create a static step with custom values and call it Select Columns. Set the display column and add a second column for the field's API name — this is the same pattern we used for the toggle steps in our Compare Table binding blog, multi-select this time.

Select Columns
Head into the JSON (Cmd/Ctrl + E) or the Advanced Editor, and it should look something like this:
"Select_Columns_1": {
"type": "staticflex",
"numbers": [],
"strings": [],
"groups": [],
"columns": {
"Display": {
"type": "string"
},
"value": {
"type": "grouping",
"dataset": {
"name": "Taxi_Trips"
}
}
},
"broadcastFacet": true,
"selectMode": "multi",
"values": [
{
"Display": "Company",
"value": "Company"
},
{
"Display": "Trip ID",
"value": "Trip_ID"
},
{
"Display": "Pickup Community Area",
"value": "Pickup_Community_Area"
},
{
"Display": "Trip Start Timestamp",
"value": "Trip_Start_Timestamp"
},
{
"Display": "Dropoff Community Area",
"value": "Dropoff_Community_Area"
},
{
"Display": "Trip End Timestamp",
"value": "Trip_End_Timestamp"
},
{
"Display": "Trip Miles",
"value": "Trip_Miles"
},
{
"Display": "Payment Type",
"value": "Payment_Type"
},
{
"Display": "Fare",
"value": "Fare"
},
{
"Display": "Extras",
"value": "Extras"
},
{
"Display": "Tolls",
"value": "Tolls"
},
{
"Display": "Trip Total",
"value": "Trip_Total"
},
{
"Display": "Tips",
"value": "Tips"
}
],
"label": "Select Columns"
},
start Just gives us a reasonable default selection so the table isn't blank when someone first opens the dashboard. Everything else is fair game for the user to add or remove — but only from within the set we already fetched in Step 1. If a field isn't in the query, it can't show up here no matter how it's bound.
Step 3: The Binding Lives in the Widget
This is the part we actually care about. The widget’s parameters.columns — the setting that controls what's visible and in what order — gets replaced with a binding that pulls straight from the picker's selection (both in the widget and query sections):
"parameters": {
"borderColor": "#e0e5ee",
"borderWidth": 1,
"cell": {
"backgroundColor": "#ffffff",
"fontColor": "#16325c",
"fontSize": 12,
"textWrap": true
},
"columnProperties": {},
"columns": "{{column(Select_Columns_1.selection, [\"value\"]).asObject()}}",
"customBulkActions": [],
"header": {
"alignment": "left",
"backgroundColor": "#f4f6f9",
"fontColor": "#16325c",
"fontSize": 12,
"italic": false,
"textWrap": true,
"underline": false
},
"headerProperties": {},
"innerMajorBorderColor": "#a8b7c7",
"innerMinorBorderColor": "#e0e5ee",
"interactions": [],
"maxColumnWidth": 300,
"minColumnWidth": 40,
"mode": "variable",
"numberOfLines": 1,
"showActionMenu": true,
"showRowIndexColumn": true,
"totals": true,
"verticalPadding": 8
},
column() pulls one field across every row of the selection — since this is a multi-select step, "every row" could be 3 rows or 13. .asObject() passes that straight through as an array, no reformatting needed. Whatever length that array comes out to, that's how many columns the table shows.
So when a dispatcher selects 10 fields, the array has 10 field names in it, and the table renders 10 columns. When someone on the finance side selects 20, same binding, same query, 20 columns. Nothing about the query, the step, or the widget type needs to change between the two.

Bindings in CompactForm 2.0
Why This Works Without SAQL
The reason this stays in compact form is that we never actually needed the query to be dynamic — only the display did. A lot of the SAQL detours we end up taking for “dynamic columns” are really solving a display problem with a query-level tool. If the use case is genuinely about which fields get fetched (say, we’re working with a wide dataset and want to avoid pulling fields nobody asked for), that’s a real reason to reach for SAQL and .asProjection(). But if the fields are already reasonably scoped and the real ask is "let people choose what they see," the widget-level columns binding gets us there with a fraction of the complexity.
One thing worth flagging: because the query always returns the full field set, this approach isn’t ideal if we’re working with a dataset where pulling unused fields has a real performance cost, or if we have dozens of candidate fields and only ever expect 4–5 to be shown at once. In that case, the SAQL projection route earns its complexity. For anything in the range of “10 to 20 out of maybe 15–20 available fields,” though, this is the simpler and more maintainable path.
Wrapping Up
Compact Form 2.0 gets dismissed sometimes as the “simple” option that can’t handle real dynamic requirements, but this is a good example of where it holds up just fine — we just have to know where the flexibility actually needs to live. Query fetches broad, widget renders narrow, and the binding sits at the boundary between the two.
Thank you, Roman Michalik, for chatting through how to get Compact Form 2.0 column selection to actually work!
메타데이터
- post_id
- 9c1957fe8a1c
- slug
- the-magic-of-flexible-columns-in-compact-form-2-0-9c1957fe8a1c
- url
- https://medium.com/crm-analytics/the-magic-of-flexible-columns-in-compact-form-2-0-9c1957fe8a1c
- canonical_url
- https://medium.com/crm-analytics/the-magic-of-flexible-columns-in-compact-form-2-0-9c1957fe8a1c
- author_url
- https://medium.com/@sayantanim
- status
- ok
- fetched_at
- 2026-07-18 21:53:05