From connecting your first data source to building multi-widget dashboard architectures that handle real-world disaster operations at scale.
Every EB app starts with data. Get the data model wrong, and you'll fight the tool the entire build. Get it right, and everything clicks.
Experience Builder has a fundamentally different data model from Web AppBuilder or ArcGIS Dashboards. Understanding this model is the single most important thing you can do before building anything.
In Dashboards, you add a layer to a widget and the widget owns that connection. In EB, data flows through a centralized data source system. Widgets don't own data — they subscribe to it.
Dashboards is like giving each employee their own copy of a spreadsheet. EB is like putting one spreadsheet on a shared drive and having everyone read from it. When the spreadsheet updates, everyone sees the change simultaneously.
There are three core concepts you need to internalize:
The original connection to your data — a Web Map, Feature Layer, CSV, or GeoJSON. Added once in the Data panel, shared by all widgets.
A filtered or sorted "lens" on a data source. Same data, different perspective. Multiple widgets can share one data view, or each can have its own.
Data that a widget produces as output — like "the features the user selected on the map." Other widgets can subscribe to this output.
Data flows one direction: from Data Sources, through Widgets, out as Output Data Sources, and into other Widgets. Understanding this flow is how you build apps that don't break when users interact with them. If you're fighting EB, you're probably fighting this flow.
| Concept | What it is | Example |
|---|---|---|
| Data Source | Connection to external data | A Feature Layer of shelter locations |
| Data View | Filtered lens on a data source | Only open shelters (status = 'Open') |
| Output Data Source | Widget-generated data | Features the user clicked on the map |
| Use Data Source | A widget's connection config | Map widget connected to Web Map DS |
The Data panel (left sidebar, database icon) is where every data connection starts. You can add four types of data sources, and each has different trade-offs.
| Source Type | Best For | Limitations |
|---|---|---|
| Web Map | Most apps. Brings all layers, popups, symbology, and labels with it. | Can't exclude individual layers (they all come in). Popup config travels with the map. |
| Feature Layer | When you need a layer without a map, or a layer that isn't in your web map. | No symbology — widgets get raw data only. You'll configure display in each widget. |
| CSV | Quick prototyping, static reference data, lookup tables. | No editing. Data is embedded in the app — no live connection. Max ~10MB practical limit. |
| GeoJSON | External API data, developer-generated content. | Same as CSV — static, embedded, no editing. Must be valid GeoJSON. |
Adding the same Feature Layer both directly AND inside a Web Map creates two separate data sources. Widgets connected to one won't react to filters on the other. Pick one source and stick with it. If your layer is in the Web Map, use the Web Map version.
Create a new EB app. Add a Web Map that has at least two layers. Then add one standalone Feature Layer. Open the Data panel and notice how the Web Map shows its layers nested underneath it, while the standalone layer is at the top level.
The List widget is EB's workhorse. It turns any data source into a scrollable, clickable, searchable list of items. Think of it as a repeating template — you design one item, and the List stamps it out for every record in your data.
The List widget is like a mail merge. You design the letter template once, and the software fills in each recipient's name, address, and details. Each "letter" in the List is called a list item, and you design it using child widgets (Text, Image, Button).
{NAME} or {STATUS}.| Setting | What it controls | Recommendation |
|---|---|---|
| Items per page | How many items load at once | 10-20 for most apps. More causes slow initial load. |
| Scroll type | Pagination vs infinite scroll | Pagination for data exploration. Infinite scroll for browsing. |
| Item height | Fixed vs auto height | Fixed for uniform layouts. Auto if content varies significantly. |
| Selection mode | Single vs multi-select | Single for "click to see details." Multi for "select items to export." |
The List template is a container layout that repeats. Child widgets inside the template automatically get the context of the current record. When you put a Text widget inside a List template and bind it to {NAME}, each list item shows a different name. You don't need to set up 50 text widgets — one template handles all records.
If your list shows 1,000 features but you set "items per page" to 1,000, EB will try to render all 1,000 DOM elements at once. This destroys performance on mobile. Use 10-20 items per page and let users paginate or scroll. The List widget handles this efficiently with lazy loading.
The Table widget shows your data as rows and columns — exactly like a spreadsheet. It's the fastest way to give users a way to browse, sort, filter, and export data. Less design flexibility than List, but faster to set up and better for data-heavy workflows.
Users need to compare values across records, sort by multiple columns, export data, or see many fields per record.
You want visual design control, images, custom layouts, or when the data is best shown as cards rather than rows.
The Table's CSV export only exports currently visible/filtered rows, not the entire dataset. If the user has a filter active, the export reflects that filter. This is actually useful — but users will be confused if they don't realize it. Add a Text widget near the Table showing the current record count so users know what they're exporting.
The Filter widget is how you let users narrow down data without writing code. It builds SQL WHERE clauses behind the scenes and applies them to one or more connected widgets. There are two modes, and choosing the wrong one is the most common beginner mistake.
You define the exact SQL clause. User sees a clean dropdown or input. Best for controlled, predictable filtering.
Users build their own filter by choosing field, operator, and value. More flexible, but can confuse non-technical users.
STATUS = 'Open'. Check "Ask for value" to let the user pick from a dropdown.Because widgets subscribe to shared data sources, a single Filter widget can update a Map, a List, a Table, and a Chart all at once. You don't connect the Filter to each widget — the Filter modifies the data source, and every widget subscribed to that source automatically refreshes. This is fundamentally different from Dashboards, where you wire each connection manually.
-- Single value filter (dropdown) STATUS = '${value}' -- Multi-value filter (checkboxes) REGION IN ('${value}') -- Range filter (slider) POPULATION >= ${value1} AND POPULATION <= ${value2} -- Date range filter CREATED_DATE >= '${value1}' AND CREATED_DATE <= '${value2}' -- Combined AND clause STATUS = 'Open' AND CAPACITY > 50
If you add a Filter that targets a Web Map data source, it filters all layers in that Web Map that have the matching field name. If your shelter layer and your county layer both have a "NAME" field, filtering by NAME affects both. Solution: use more specific field names, or target individual layers instead of the Web Map root.
Add a Map, a List, and a Filter all connected to the same Feature Layer. Create a SQL filter for one field (like STATUS or TYPE). Apply the filter and watch both the Map and the List update simultaneously. This is the fundamental EB interaction pattern.
The Chart widget visualizes your data as bar, line, pie, or scatter charts. It connects to the same data sources as everything else, so filters applied elsewhere automatically update the chart. This is what makes EB dashboards genuinely interactive.
| Chart Type | Best For | Key Setting |
|---|---|---|
| Bar | Comparing categories (shelters by region, cases by type) | Category field + value field + aggregation (count, sum, avg) |
| Line | Trends over time (daily case count, monthly openings) | Date field on X axis + value field + aggregation |
| Pie | Proportions of a whole (% by status, % by category) | Category field + count or sum aggregation |
| Scatter | Correlation between two numeric fields | X field + Y field, both numeric. No aggregation. |
When a Filter widget changes the data source, the Chart re-aggregates from the filtered data. This means your "Shelters by Region" chart will show different numbers depending on what status filter is active. This is usually what you want — but if you need a chart that always shows all data regardless of filters, you must connect it to a separate data source or a data view that isn't affected by the filter. More on this in Lesson 7.
The Text widget isn't just for static labels. When connected to a data source, it becomes a dynamic display that shows field values, counts, and even simple Arcade calculations. This is how you build info panels, summary headers, and detail views.
{FIELD_NAME} for field values, {OBJECTID} for the record ID, and statistic values like record count.// Show a status badge with dynamic text var status = $datapoint.STATUS var name = $datapoint.SHELTER_NAME if (status == "Open") { return name + " — Currently OPEN" } else { return name + " — Closed" }
When a Text widget is inside a List template, $datapoint automatically refers to the current list item's record. Outside a List, $datapoint refers to the first record in the connected data source, or the selected record if a selection exists. This context-sensitivity is what makes Text widgets so versatile.
Add a Text widget connected to a Feature Layer. Insert a dynamic field value (like NAME). Then add a second Text widget and use the Statistics dynamic content to show the total record count. Put both above your Map — you now have a simple dashboard header that says "Showing 47 shelters" and updates when filters change.
You know the individual widgets. Now learn how to make them talk to each other — and how to avoid the circular dependency traps that break most first-time dashboard builds.
This is the concept that trips up the most people. Esri's docs use both terms loosely, but they are fundamentally different things, and confusing them is the root cause of 90% of "my widgets aren't connected" bugs.
A pre-defined filter or sort on a data source. You create it in the Data panel. It always exists, whether the user does anything or not. Think of it as a "saved view."
Data produced by a widget at runtime. It only exists when the user interacts — selecting features on a map, filtering rows in a table. It's dynamic and ephemeral.
A data view is like a saved SQL query that always returns the same filtered results. An output data source is like a "selected items" shopping cart — it's empty until the user puts things in it, and it changes every time they interact.
| Attribute | Data View | Output Data Source |
|---|---|---|
| Created by | You (in the Data panel) | Widgets (automatically, at runtime) |
| Exists when | Always — even before user interaction | Only after user interaction (selection, filter) |
| Changes at runtime | No (unless a Filter widget targets the same source) | Yes — every time the user interacts |
| Use case | "Only show open shelters in the list" | "Show details for whatever the user just clicked" |
| Found in | Data panel, under the parent data source | Widget settings, "Output data source" section |
This is the cascade architecture — the pattern behind every production EB dashboard. A single user action (choosing a filter value) triggers a cascade of updates through all connected widgets. Getting this right is the difference between a demo and a deployable app.
The cascade works because all widgets share the same data source. When the Filter modifies that data source's active query, every subscriber re-renders. But there are rules:
Be careful connecting a List to the Map's output data source AND applying a Filter. The Filter changes the data source, which changes what's on the Map, which changes what can be selected. If your List is connected to the output data source, it might show "no data" after a filter change because the previous selection is now invalid. Solution: clear the selection when filters change (using the Map's action settings), or connect the List to the data source directly.
Build this exact layout: Filter (top) → Map (left, 60% width) + List (right, 40% width, connected to same data source) + Chart (bottom, full width, same data source). Apply a filter — everything updates. Click a feature on the Map — the List highlights that row. This is the production pattern you'll use for 80% of EB dashboards.
The Edit widget turns your EB app from a read-only viewer into a data entry tool. Users can create new features, update existing ones, and delete records — directly from your app, with no AGOL login required (if the layer allows anonymous editing).
The Edit widget requires your Feature Layer to have editing enabled. In ArcGIS Online, go to the layer's Settings tab and check "Enable editing." Choose which operations to allow: Add, Update, Delete. If the layer is view-only, the Edit widget will show an error.
The typical edit flow: user clicks a feature on the Map → Edit widget shows the attribute form → user modifies fields → clicks Save → the feature is updated in the Feature Layer → all other widgets (List, Table, Chart) automatically refresh because they share the same data source. For new features: user clicks the "Add" button → clicks on the map to place the point → fills in the form → saves.
If your Feature Layer allows anonymous editing, anyone with the app URL can modify data. For production apps, either: (1) require AGOL login to edit, (2) use the layer's editing settings to restrict who can edit, or (3) use ArcGIS Workflow Manager for approval-based editing. Never expose unrestricted editing on sensitive operational data.
The Near Me widget lets users draw a point or area on the map and find features within a search radius. This is critical for disaster operations — "What shelters are within 10 miles of this address?" or "How many damage assessments are in this county?"
User enters their address → Near Me shows shelters within 10 miles → sorted by distance → user clicks one to see details and driving directions.
User draws a polygon around a neighborhood → Near Me counts damage reports within that area → grouped by severity level.
Near Me performs a spatial query against your Feature Layer. If the layer has 100,000+ features, large search radii (50+ miles) can be slow. Use the maxRecordCount setting on the layer and keep search distances reasonable. For very large datasets, consider creating a pre-computed "nearest facility" field in your data instead of relying on runtime spatial queries.
The Query widget is the Filter widget's more powerful sibling. While Filter modifies the data source globally (affecting all connected widgets), the Query widget lets you build complex, multi-step queries with AND/OR logic, spatial filters, and the ability to target specific widgets rather than the entire data source.
Simple. Modifies the shared data source directly. All connected widgets update. Best for global app-wide filtering.
Advanced. Creates its own output data source with query results. You control which widgets consume it. Best for targeted, multi-criteria searches.
-- User-facing query with three criteria -- "Ask for value" on STATUS and REGION -- Fixed value on CAPACITY STATUS = '${user_status}' -- Dropdown: Open/Closed AND REGION IN ('${user_regions}') -- Multi-select checkboxes AND CAPACITY >= 25 -- Fixed: only shelters with 25+ capacity
Conditional visibility lets you show or hide entire widgets based on data conditions. This is how you build apps that adapt — showing an "emergency alert" banner only when there's an active disaster, or hiding the edit panel when there's nothing selected.
true (show) or false (hide).// Connect to the Map's output data source (selected features) // Show the detail panel only when something is selected var count = Count($datapoints) return count > 0
When your Map has nothing selected, widgets connected to its output data source show "No data." This is ugly. Instead, create two versions of your detail panel: one that shows "Click a feature to see details" (visible when output DS is empty) and one that shows the actual data (visible when output DS has records). Toggle between them using conditional visibility. This pattern is called empty state handling and it's the mark of a polished app.
You've built dashboard prototypes. Now learn the patterns that survive contact with real users, real data volumes, and real operational pressure.
When you have 8+ widgets all sharing data sources, you need an architecture — not just a collection of widgets. The two patterns that work at scale are hub-and-spoke and parallel pipelines.
One central data source feeds all widgets. A Filter widget modifies the hub, and all spokes update. Simple, predictable, and works for 80% of dashboards.
Multiple data sources, each feeding their own widget chain. Use when you need widgets that DON'T sync — e.g., a "comparison mode" with two independent maps.
The hub is your primary data source (usually a Web Map with its layers). All widgets connect to this hub. Filter and Query widgets modify the hub. Every widget downstream reacts. This is the default pattern — use it unless you have a specific reason not to.
A circular dependency happens when Widget A's output feeds Widget B, and Widget B's output feeds Widget A. Example: a Map selects features that populate a List, and clicking a List item zooms the Map — which changes the selection — which updates the List — which triggers another zoom. The result: infinite loops, frozen UI, or unpredictable behavior.
ArcGIS Feature Layers have a maxRecordCount — the maximum number of features returned per query. The default is 1,000 (AGOL) or 2,000 (Enterprise). If your layer has 50,000 features and a widget queries without pagination, you only get the first 1,000. This is the single most common "where's my data?" problem in EB.
When you see a List showing exactly 1,000 items out of 50,000, or a Chart aggregation that seems wrong, or a "record count" text that tops out at 1,000 — you've hit the maxRecordCount limit. The widget isn't broken; it's only seeing partial data.
| Approach | How it works | When to use |
|---|---|---|
| Pagination (List/Table) | List and Table widgets paginate automatically — they request pages of records. Each page = one query up to maxRecordCount. | Default for List/Table. Works well up to ~50K records. |
| Server-side filtering | Apply filters before data reaches the client. The WHERE clause runs on the server, returning only matching records. | Always. Reduce the dataset before rendering. A filter that takes 50K → 500 records makes everything fast. |
| Increase maxRecordCount | In AGOL, go to the layer's Settings → change "Max Record Count" (up to 32,000 on AGOL, higher on Enterprise). | When widgets need all records for accurate aggregation (Charts, Statistics). |
| Feature Layer Views | Create a View Layer in AGOL with a permanent WHERE clause. The view only contains the subset you need. | When your app only needs a geographic or attribute subset of a large dataset. |
| Materialized views / summary tables | Pre-aggregate data in a separate table. Instead of querying 100K individual records, query 50 summary rows. | For charts and statistics on very large datasets (100K+). |
If your Chart widget does a "count by region" aggregation on a 50,000-record layer with maxRecordCount = 1,000, it only aggregates the first 1,000 records. The chart looks right but shows wrong numbers. This is insidious because there's no error message — just incorrect data. Solutions: increase maxRecordCount for that layer, use a pre-aggregated summary table, or use a server-side group-by query (available in EB's advanced data source settings).
Arcade isn't just for formatting text. In EB's advanced settings, you can use Arcade to create computed fields (virtual columns that don't exist in the actual data), dynamic labels, and conditional data transformations that happen at query time.
// Use in a data source's "expression" field or in a widget's Arcade var current = $datapoint.CURRENT_OCCUPANCY var capacity = $datapoint.MAX_CAPACITY if (capacity == 0 || capacity == null) { return "Unknown" } var rate = Round((current / capacity) * 100, 1) if (rate >= 90) { return "Critical" } if (rate >= 75) { return "High" } if (rate >= 50) { return "Moderate" } return "Low"
var lastUpdate = $datapoint.LAST_UPDATED var now = Now() if (IsEmpty(lastUpdate)) { return "Never updated" } var days = DateDiff(now, lastUpdate, "days") if (days < 1) { return "Today" } if (days < 2) { return "Yesterday" } return Round(days, 0) + " days ago"
Arcade computed fields run in the browser, not on the server. They can't be used in server-side queries or filters — only in display. If you need to filter by a computed value (like "show only Critical occupancy shelters"), you need to either: (1) add the computed field to the actual layer using a Python script, or (2) use the Arcade expression within the Filter widget's SQL builder. Option 1 is more reliable for production apps.
Pick a Feature Layer with a date field. Add a Text widget, connect it to that layer, and write an Arcade expression that returns "X days since last update" using DateDiff. Then add a second Text widget that shows the occupancy rate classification (Critical/High/Moderate/Low) based on two numeric fields. You now have dynamic computed data without modifying the original layer.
After building dozens of operational dashboards for Red Cross disaster response, these are the patterns that survive contact with real users under real pressure. Every pattern here has been tested during actual disaster operations.
Layout: Filter bar (top) + Map (left 60%) + Tabbed panel (right 40%) with List/Table/Chart tabs + Summary bar (bottom). Data: one Web Map with shelter, distribution, and volunteer layers. Filter targets the Web Map root — all layers filter simultaneously. The tabbed panel shows different views of the same filtered data. This pattern handles 3-5 operational layers and 10,000+ records comfortably.
Layout: Map (full width, top half) + Detail panel (bottom half, initially hidden). User clicks a feature on the Map → detail panel appears (conditional visibility on output DS). Detail panel contains: name/status header (Text widget with Arcade), attribute table (Table widget connected to output DS), related records (List widget), and an Edit button. This is the pattern for field teams who need to view and update individual records.
Layout: Two side-by-side Maps, each connected to a different data view of the same data source. Left map shows "before" (data view filtered to a date range), right map shows "after." A shared Filter for region/area applies to the underlying data source, so both maps filter together. Separate Charts below each map show stats for their respective time periods. This uses parallel pipelines architecture.
| Pattern | Architecture | Best For |
|---|---|---|
| Operations Hub | Hub-and-spoke, single Web Map | Daily ops monitoring, multi-layer dashboards |
| Detail Drilldown | Hub + output DS cascade | Field data collection, record inspection |
| Comparison | Parallel pipelines, data views | Before/after analysis, A/B comparison |
| Public Facing | Hub-and-spoke, read-only, mobile-first | Shelter finders, resource locators |
| Data Entry | Map + Edit widget, minimal UI | Damage assessment, survey collection |
When widgets show "No data" or display wrong information, the problem is almost always in the data connection — not the widget itself. Here's a systematic approach to debugging every data issue you'll encounter in EB.
| Symptom | Likely Cause | Fix |
|---|---|---|
| Widget says "No data" | Wrong data source, or filter returning zero records | Check connection in widget settings. Clear all filters. |
| List shows items but no field values | Dynamic text references wrong field name | Check field names in the Data panel. They're case-sensitive. |
| Chart shows wrong numbers | maxRecordCount limiting the aggregation | Increase maxRecordCount on the Feature Layer. |
| Filter doesn't affect Map | Filter targets a different data source than the Map | Ensure both use the exact same data source (not copies). |
| Widget works in builder but not published | Layer sharing permissions | Share the layer with "Everyone" or the appropriate group. |
| Clicking Map doesn't update List | List connected to data source, not Map's output DS | Connect List to Map's output data source for selection-driven updates. |
| Data appears stale | No auto-refresh configured | Set refresh interval in Data panel → layer settings. |
| Arcade expression returns blank | Null values in referenced fields | Add null checks: if (IsEmpty(field)) return "N/A" |
Experience Builder has TWO configuration files: the Data tab (published/live app) and Resources → config/config.json (draft/editor). If you manually edit the published config (via AGOL Assistant or the REST API), your changes will work in the live app — but opening the EB editor and saving will overwrite your changes with the draft config. Always update the draft config first, then publish. This has bitten every EB developer at least once.
where (the filter clause), outFields (requested fields), resultRecordCount (pagination).{"features":[]}, the WHERE clause is filtering out all records. Copy the WHERE clause and test it directly in the Feature Layer's REST endpoint.You've gone from "how does data work in EB?" to multi-widget dashboard architectures, large dataset strategies, and production debugging protocols. These 18 lessons cover the full data-to-widget pipeline in Experience Builder. The patterns are modular — hub-and-spoke for most dashboards, parallel pipelines for comparisons, output data sources for selection-driven details. Combine them for any operational tool you need to build.