0%

The Complete Guide to
Arcade in Experience Builder

From your first "Hello World" expression all the way to cross-layer FeatureSet queries, HTML-styled returns, and full dynamic styling dictionaries.

🌱 Beginner — 8 lessons 🔥 Intermediate — 7 lessons 🚀 Advanced — 6 lessons

What's inside

Your First Arcade Scripts

You know Experience Builder. Now let's teach your widgets to think. No prior coding experience needed — we'll explain every single line.

Lesson 0

What is Arcade and why should I care?

You've built EB apps. Text widgets show static text. Lists show rows of data. Maps show features. But everything just sits there.

What if a Text widget could count what someone selected and show their names? What if List items could change color automatically based on a field? What if a Button showed a different icon for each feature?

That's Advanced Formatting. And Arcade is the language you write it in.

💡 Think of it this way

Right now your widgets are a printed poster — same thing every time. Arcade turns them into a smart screen, like how your phone shows different info based on what's happening. Your widgets will react to selections, data values, and user actions.

Arcade is NOT JavaScript, NOT Python — it's Esri's own lightweight language designed specifically for GIS. It's simple, secure, and purpose-built. If you've ever used an Excel formula, you can learn Arcade.

📝

Advanced Dynamic Content

Arcade scripts that calculate values and display them as widget content — text, image URLs, button labels.

🎨

Dynamic Styling

Change colors, borders, icons, and backgrounds based on data. Some with Arcade, some with zero code.

📋 Requirements
  • ArcGIS Online (June 2025 update or later)
  • ArcGIS Enterprise 12.0+
  • Developer Edition 1.18+
  • A free ArcGIS account is enough to follow along
Lesson 1

Setting up the practice template

We'll use Esri's pre-made template so we can focus on learning Arcade, not building layouts. It has a map of US statistical areas, a List widget, and blank Text widgets waiting for us.

1
Go to experience.arcgis.com and sign in (or create a free trial)
2
Click Create new
3
Switch to the ArcGIS Online tab in the template gallery
4
Search for Try advanced formatting
5
Click Create — you now have a map showing Core Based Statistical Areas (CBSAs) with population data, plus empty widgets to wire up
🗺️ What's in the template
  • Map widget — showing CBSAs (metro/micro regions) with population data
  • List widget — connected to the same data, with Image and Button widgets inside
  • 3 Text widgets — labeled "Text Title", "Population Growth", "Population Density" — currently showing placeholder text that we'll replace with Arcade
Lesson 2

Variables, return, and your first script

Before we touch any widgets, let's understand the three most important concepts in Arcade. Everything else builds on these.

📦

Variables

Named containers that hold data. Like labeling a box: var name = "Jeff"

📤

return

Sends a value back to the widget. Whatever you return is what the widget shows.

🔌

$dataSources

How you connect to your data. Each data source has a unique ID you can reference.

Let's write the simplest possible Arcade expression. Click any Text widget, click the Arcade button {} in the toolbar, and type:

hello-world.arcade
return "Hello, Experience Builder! 🎉"

Click Insert. The widget now shows your text. That's it — you just wrote an Arcade expression! Now let's add a variable:

variable-example.arcade
// Create a variable — a named container for data
var greeting = "Welcome to"
var appName = "My Dashboard"

// Glue them together with + and return the result
return greeting + " " + appName
🔑 Key rules
  • var creates a variable. You pick the name, you assign the value with =
  • return ends the script and sends the result to the widget. You can only return once.
  • // starts a comment — notes for humans that Arcade ignores completely
  • + joins text together (called concatenation)

Quick check: What does return do?

Lesson 3

If / else — making decisions

This is where Arcade starts getting useful. if lets your script make decisions: "If this is true, do this. Otherwise, do that."

if-else.arcade
var population = 75000

if (population > 50000) {
  return "Metropolitan area"
} else {
  return "Micropolitan area"
}
💡 Think of it this way

It's exactly like a fork in the road. The condition inside the parentheses is the question: "Is population greater than 50,000?" If yes, take the left path. If no, take the right path. The curly braces { } are the walls of each path.

You can chain multiple conditions with else if:

else-if-chain.arcade
var pop = 250000

if (pop > 1000000) {
  return "Major Metro"
} else if (pop > 250000) {
  return "Large City"
} else if (pop > 50000) {
  return "Small City"
} else {
  return "Town"
}

There's also a shortcut called IIf (Inline If) for simple decisions:

iif-shortcut.arcade
// IIf(condition, valueIfTrue, valueIfFalse)
var label = IIf(population > 50000, "Metro", "Micro")
return label
OperatorMeaningExample
==Equalsstatus == "Open"
!=Not equalstype != "Closed"
>Greater thanpop > 50000
<Less thanage < 35
>=Greater or equalscore >= 90
&&AND (both true)pop > 100 && status == "Open"
||OR (either true)type == "A" || type == "B"
Lesson 4

Selection-aware Text widgets

Now we put it all together — real data, real widget. We'll make the "Text Title" widget respond to what the user selects on the map.

1
Click the "Text Title" widget on the canvas
2
Click Arcade {} in the widget toolbar
3
Paste the code below and click Insert
selection-title.arcade
// Ask EB: "what features did the user select?"
var selected = $dataSources["dataSource_2-197c022ade6-layer-2"].selectedFeatures;

// Nothing selected? Show a friendly default
if (Count(selected) == 0) {
  return "🌎 All Regions"
}

// Something selected — grab up to 3 names
var names = [];
var i = 0;

for (var f in selected) {
  if (i < 3) { Push(names, f.NAME); }
  i++;
}

var label = "📍 Selected: " + Concatenate(names, ", ");
if (i > 3) {
  label += " + " + (i - 3) + " more";
}

return label;
🔍 Line by line
  • Line 2: $dataSources["..."].selectedFeatures — this is how you ask EB "what did the user click?" The long string is the data source ID (unique to each layer)
  • Line 5: Count() returns how many items are in a set. If zero, nothing's selected.
  • Line 10: [] creates an empty array (list). We'll fill it with names.
  • Line 13-15: A for loop goes through each selected feature. f.NAME grabs the NAME field. Push() adds it to our list.
  • Line 18: Concatenate() joins the array into one string with commas between.
🧪 Try it

Turn on Live view (play button, top right). Use the map's rectangle-select tool to select some areas. Watch the text change in real time! Select different numbers of areas — notice how it caps at 3 names and adds "+ X more".

Lesson 5

Loops — going through features one by one

You just used a loop in Lesson 4 without us making a big deal about it. Let's zoom in on how loops work, because they're everywhere in Arcade.

loop-anatomy.arcade
// The "for...in" loop visits every feature in a set
var total = 0

for (var feature in someFeatureSet) {
  // This code runs once for EACH feature
  total += feature.POPULATION   // += means "add to existing value"
}

return "Total population: " + total
💡 Think of it this way

Imagine a stack of flash cards, each one representing a feature (row in your data). A loop picks up each card, lets you read it and do something with it, then moves to the next card. When the stack is empty, the loop is done.

🔑 Key patterns
  • for (var f in featureSet)f is the current feature on each iteration
  • f.FIELDNAME — access any field on that feature
  • += — shortcut for "add to the existing value" (same as total = total + f.POP)
  • You can use if inside loops to filter, count, or categorize
Lesson 6

Conditional List styling (zero code)

Good news — this one needs no Arcade at all. EB has a built-in conditional styling feature for List widgets. You point it at a field and say "if the value is X, paint it purple."

1
Click the List widget
2
In Content settings → expand States
3
Turn on Dynamic style → click the ⚙️ gear icon
4
Choose field CBSA_TYPE as the indicator
5
Add conditions:

Metropolitan

Background: #ccbaf0 (light purple)
Big metro areas, 50K+ people

Micropolitan

Background: #c0dfed (light blue)
Smaller areas, 10K-50K people

📋 Supported condition operators
  • String fields: is, is not, contains, is blank, is not blank
  • Number fields: is, is not, is greater than, is less than, is between
  • If multiple conditions match, the one higher in the list wins
Lesson 7

Dynamic Images and Button text

Inside a List, you get a special variable: $feature. This represents the single feature for each list item. Outside a List, you work with the whole dataset via $dataSources. This is the most important distinction in EB Arcade.

$dataSources["..."]

The entire dataset. Used in standalone widgets for aggregations (Sum, Count).

$feature

One single feature/row. Used inside List items where each card = one feature.

Dynamic Image: Click the Image widget inside the List → Settings → Connect to data → Dynamic → URL → Arcade → Arcade Editor:

dynamic-image.arcade
var type = $feature.CBSA_TYPE;

if (type == "Metropolitan") {
  return "https://arcgis.com/sharing/rest/content/items/d5ca28bf46fa44ea8e0aa0b9a8e513fb/data";
}
return "https://arcgis.com/sharing/rest/content/items/b385127f92de4460afba630cbe106b39/data"

Dynamic Button text: Click the Button widget → Settings → Text dropdown → Arcade:

button-text.arcade
var age = $feature.MED_AGE;
return IIf(age > 55, "Elder", IIf(age < 35, "Young Area", "Mid-age Area"));
🎓 Beginner section complete!

You now know: variables, return, if/else, IIf, loops, $dataSources vs $feature, and no-code conditional styling. That's a real foundation. The Intermediate section builds on every one of these concepts.

Data-Driven Power

Styled returns, filter-aware patterns, aggregation functions, date math, and reusable functions. Your scripts will start doing real analytical work.

Lesson 8

Styled returns — color, bold, and size

In Beginner, we returned plain text: return "hello". Now we'll return a dictionary — a package containing the text AND styling instructions.

styled-return.arcade
// Instead of returning a string, return a dictionary
return {
  value: "Growing Population",
  text: {
    color: "rgb(0, 128, 0)",     // green
    bold: true,
    size: 18,                  // pixels
    italic: false,
  },
};
💡 Dictionaries explained

A dictionary is like a labeled filing cabinet. Each drawer has a name (the key) and contents (the value). The text drawer itself contains more drawers: color, bold, size. EB opens each drawer and applies what it finds.

PropertyTypeWhat it does
valueStringThe text content to display
text.colorStringCSS color — hex, rgb(), or named colors
text.boldBooleantrue or false
text.italicBooleantrue or false
text.underlineBooleantrue or false
text.strikeBooleanStrikethrough
text.sizeNumberFont size in pixels

Now let's use this with real data — the "Population Growth" widget:

pop-growth-styled.arcade
var selected = $dataSources["dataSource_2-197c022ade6-layer-2"].selectedFeatures;
var all = $dataSources["dataSource_2-197c022ade6-layer-2"].layer;

var useSelection = Count(selected) > 0;
var targetSet = IIf(useSelection, selected, all);

var pop2018 = Sum(targetSet, 'POPULATION');
var pop2010 = Sum(targetSet, 'POP2010');

var str = ""; var color = ""; var bold = true;

if (IsEmpty(pop2018) || IsEmpty(pop2010)) {
  str = "No data"; color = "gray"; bold = false;
} else if (pop2018 >= pop2010) {
  str = "📈 Growing Population"; color = "rgb(0,128,0)";
} else {
  str = "📉 Shrinking Population"; color = "rgb(220,20,60)";
}

return { value: str, text: { color: color, bold: bold } };
User selects areas
Sum population fields
Compare 2018 vs 2010
Green or Red text
Lesson 9

The filter-aware pattern

Here's a problem: if your app has Filter widgets or spatial filters, the simple .layer approach ignores them. You'll show stats for ALL features even when the user filtered down to a subset. The fix is a helper function:

filter-helper.arcade — memorize this pattern!
function getFilteredFeatureSet(ds) {
  var result = ds.layer;
  var queryParams = ds.queryParams;

  if (!IsEmpty(queryParams.where)) {
    result = Filter(result, queryParams.where);
  }
  if (!IsEmpty(queryParams.geometry)) {
    result = Intersects(result, queryParams.geometry);
  }
  return result;
}

// Now use it:
var ds = $dataSources["dataSource_2-197c022ade6-layer-2"];
var filtered = getFilteredFeatureSet(ds);
var selected = ds.selectedFeatures;

var targetSet = IIf(Count(selected) > 0, selected, filtered);
🔑 Why this matters
  • Without this: Filter widget set to "Population > 100K" → your Arcade ignores it and sums ALL features
  • With this: Your Arcade respects the filter and only processes matching features
  • queryParams.where = attribute filters (e.g., "STATUS = 'Open'")
  • queryParams.geometry = spatial filters (e.g., features within a drawn polygon)
⚠️ Official best practice

From Esri's docs: "Filters and spatial filters applied to data sources or data views cannot be automatically added to profile data sources. Use the getFilteredFeatureSet function to manually synchronize the filter." Always use this pattern in production apps.

Lesson 10

Aggregation functions — Sum, Count, Average, Max

Instead of writing loops to add things up, Arcade has built-in functions that do it in one line. These work on FeatureSets (collections of features).

FunctionWhat it doesExample
Sum(fs, 'field')Add up all valuesSum(features, 'POPULATION')
Count(fs)Count featuresCount(selected)
Average(fs, 'field')Calculate meanAverage(features, 'MED_AGE')
Max(fs, 'field')Highest valueMax(features, 'POPULATION')
Min(fs, 'field')Lowest valueMin(features, 'POP2010')
Distinct(fs, 'field')Unique valuesCount(Distinct(fs, 'STATE'))
First(fs)First featureFirst(selected).NAME
aggregation-dashboard.arcade
var ds = $dataSources["YOUR_DS_ID"];
var features = ds.layer;

var total = Count(features);
var totalPop = Sum(features, 'POPULATION');
var avgAge = Round(Average(features, 'MED_AGE'), 1);
var biggest = Max(features, 'POPULATION');

return total + " regions | Pop: " + Text(totalPop, '#,###')
  + " | Avg age: " + avgAge
  + " | Largest: " + Text(biggest, '#,###');
💰 Text() formatting

Text(1234567, '#,###')1,234,567. The Text() function formats numbers with commas, decimal places, percentages, currencies, and more. Like Excel's number formatting.

Lesson 11

Math and classification (density buckets)

Division + if/else chains = classification. This is the "Population Density" widget from the template. The new idea here is protecting against division by zero.

density-classification.arcade
// (Using filter helper and selection check from Lesson 9)
var totalPop = Sum(targetSet, 'POPULATION');
var landArea = Sum(targetSet, 'SQMI');

// ⚡ CRITICAL: protect against divide-by-zero
var density = IIf(
  IsEmpty(landArea) || landArea == 0,
  null,
  totalPop / landArea
);

var str = ""; var color = "";

if (IsEmpty(density))       { str = "No data";           color = "gray"; }
else if (density > 500)    { str = "🏬 High Density";   color = "rgb(138,43,226)"; }
else if (density >= 100)   { str = "🏠 Moderate";       color = "rgb(255,165,0)"; }
else                       { str = "🚜 Low Density";    color = "rgb(105,105,105)"; }

return { value: str, text: { color: color, bold: true } };
⚠️ Always protect division

Dividing by zero or null crashes your expression. The pattern IIf(IsEmpty(x) || x == 0, null, a / x) is your seatbelt. Use it every single time you divide.

Lesson 12

Dynamic Button icons

Buttons can change their icon per feature. This requires two steps: define named icons in the UI, then reference them by name in Arcade.

1
Select Button widget inside List → turn on Advanced
2
Turn on Dynamic style → ⚙️ → Script tab → Arcade Editor
3
Before writing code: Click IconsAdd icons named Elder, Middle, Young
dynamic-icons.arcade
var age = $feature.MED_AGE;
var icon = IIf(age > 55, 'Elder', IIf(age < 35, 'Young', 'Middle'));

return {
  icon: {
    name: icon,      // must match your named icons exactly!
    size: 28,
    position: 'LEFT', // or 'RIGHT'
  }
};
⚠️ Icon names must match exactly

The returned string must be identical to the name you gave the icon in the UI. Case matters. If you named it "Elder" and return "elder", nothing shows.

Lesson 13

Date math — "X days ago" patterns

Arcade has date functions for time-based displays. This is great for "last updated" labels and time-sensitive dashboards.

date-math.arcade
// "X days ago" pattern from Esri's docs
var lastUpdated = $feature.EditDate;
var daysAgo = Floor(DateDiff(Now(), lastUpdated, "days"));

if (daysAgo == 0) { return "Updated today"; }
if (daysAgo == 1) { return "Updated yesterday"; }
if (daysAgo < 7) { return daysAgo + " days ago"; }
if (daysAgo < 30) { return Floor(daysAgo/7) + " weeks ago"; }
return Floor(daysAgo/30) + " months ago";
FunctionWhat it does
Now()Current date/time
DateDiff(date1, date2, "unit")Difference between dates. Units: "days", "hours", "minutes", "years"
Floor(n)Round down to nearest integer
Year(date) / Month(date)Extract parts of a date
Text(date, 'MMM D, YYYY')Format: "Jan 5, 2026"
Lesson 14

Functions — write once, use everywhere

You've already used the getFilteredFeatureSet function. Let's understand how to write your own.

custom-function.arcade
// Define a function — a reusable block of logic
function classifyPop(pop) {
  if (pop > 1000000) { return "Major Metro"; }
  if (pop > 250000)  { return "Large City"; }
  if (pop > 50000)   { return "Small City"; }
  return "Town";
}

// Now use it as many times as you want
var label = classifyPop($feature.POPULATION);
return label;
🔑 Function anatomy
  • function name(parameters) — declare the function with inputs
  • The code inside { } runs when you call the function
  • return inside a function sends a value back to where it was called
  • Functions must be defined before you call them (put them at the top)
🎓 Intermediate section complete!

You now have: styled returns, filter-aware patterns, aggregation functions, math + classification, dynamic icons, date functions, and custom functions. These are the building blocks of every production EB app.

Production Patterns

HTML-formatted returns, full style dictionaries with borders and backgrounds, hover states, the 10-expression limit, performance optimization, and real-world Red Cross operational patterns.

Lesson 15

HTML returns — full formatted content

Beyond styled dictionaries, Arcade in EB can return actual HTML. This opens up rich formatting: links, images, lists, multi-color text, all in one Text widget.

html-return.arcade
var name = $feature.NAME;
var pop = Text($feature.POPULATION, '#,###');
var type = $feature.CBSA_TYPE;
var color = IIf(type == "Metropolitan", "#7c6cf0", "#4ecdc4");

return '<h3 style="margin:0;color:' + color + '">' + name + '</h3>' +
  '<p style="margin:4px 0;font-size:14px;color:#666;">' +
  'Population: <strong>' + pop + '</strong> · ' +
  '<em>' + type + '</em></p>';
📋 Supported HTML tags
  • <h1> through <h6> — headings (style attribute OK)
  • <p>, <span>, <strong>, <em>, <u>, <s> — text formatting
  • <ul>, <ol>, <li> — lists
  • <a href="..." target="..."> — links
  • <img src="..." alt="..." width="..." height="..."> — inline images
  • <br> — line break
  • All tags support the style attribute for inline CSS
⚠️ Gotchas

Only the tags listed above are allowed — no <div>, no <table>, no <script>. EB strips anything not on the whitelist. Also, use inline styles only — no <style> blocks.

Lesson 16

Complete style dictionaries

Dynamic styling scripts can control much more than text color. Here's the full power available for each widget type:

WidgetProperties you can control
Texttext (size, color, bold, italic, underline, strike), background (color, image, fillType)
ButtonAll Text props + icon (name, position, size, color), border (type, color, width per side), borderRadius
List itembackground (color, image, fillType), border (type, color, width per side), borderRadius
full-style-dictionary.arcade
// A List item with full styling based on priority
var priority = $feature.PRIORITY;
var bgColor = "";
var borderColor = "";

if (priority == "High") {
  bgColor = "rgba(255,107,107,0.15)";
  borderColor = "#ff6b6b";
} else if (priority == "Medium") {
  bgColor = "rgba(254,202,87,0.15)";
  borderColor = "#feca57";
} else {
  bgColor = "rgba(0,214,143,0.15)";
  borderColor = "#00d68f";
}

return {
  background: { color: bgColor },
  border: {
    borderLeft: {
      type: "solid",
      color: borderColor,
      width: 4
    }
  },
  borderRadius: {
    unit: "px",
    number: [8, 8, 8, 8]  // [topLeft, topRight, bottomRight, bottomLeft]
  }
};
Lesson 17

Hover and Selected state styling

Buttons and List items have multiple states: Default, Hover, and Selected. Each state can have its own dynamic style script.

📋 Where to find state settings
  • Button widget: Dynamic style appears under Advanced. Default and Hover states have separate settings.
  • List widget: Dynamic style appears under each state (Default, Hover, Selected) in the States section.
  • You write a separate Arcade script for each state — they don't share code
hover-state.arcade — for Button Hover state
// Brighter version of the default colors on hover
var type = $feature.CBSA_TYPE;

return {
  background: {
    color: IIf(type == "Metropolitan",
      "rgba(124,108,240,0.3)",
      "rgba(78,205,196,0.3)")
  },
  text: { bold: true },
  border: {
    border: { type: "solid", color: "#fff", width: 1 }
  }
};
Lesson 18

The 10-expression limit and how to beat it

Each EB page supports a maximum of 10 Arcade expressions using the widget formatting profile. This includes both dynamic content AND dynamic style scripts. If you need more, here are your strategies:

🔗

Combine expressions

If two Text widgets use the same aggregation, merge them into one widget that returns HTML with both values.

📊

Use the Data profile

Arcade in the Add data window creates a FeatureSet data source that doesn't count toward the 10-expression limit. Calculate values once, reference everywhere.

🔑 What counts toward the limit
  • Each widget Arcade expression (dynamic content) = 1
  • Each widget dynamic style Script = 1
  • Does NOT count: Conditional style (no-code) — use this instead when possible!
  • Does NOT count: Data profile Arcade (Add data window)
  • List item widget formatting profile (scripts inside List items using $feature) may have separate limits
💡 Pro tip: Add data with Arcade

If you use the same aggregation value in multiple places, use the Arcade option in the Add data window to calculate it once as a data source. Then reference that calculated data source in multiple widgets without using additional expressions.

Lesson 19

Performance — writing fast expressions

Arcade expressions query your feature layers. Every query takes time. Here's how to keep your app fast.

✅ Do this

Use Sum(), Count(), Average() — they run as single server queries. Chain FeatureSet operations. Limit fields when possible.

❌ Not this

Don't loop through large FeatureSets manually. Don't write multiple expressions that query the same data. Don't fetch all fields if you only need one.

🔑 Performance rules
  • Aggregation functions are fast — they translate to a single SQL query on the server. A for loop downloads every feature to the client first.
  • Combine expressions — 3 expressions querying the same layer = 3 network requests. 1 expression doing all 3 calculations = 1 request.
  • FeatureSets are lazy — they don't query until used. Defining FeatureSetByName() costs nothing; calling Count() on it triggers the query.
  • Chain operationsCount(Filter(features, "POP > 50000")) sends one query. Storing the filtered set, then counting = also one query (thanks to lazy evaluation).
  • Console() for debugging — use Console(variable) to inspect values in browser dev tools (F12 → Console tab). Remove in production.
performance-combined.arcade — one expression, multiple stats
// ONE expression returning HTML with multiple stats
// Instead of 3 separate Text widgets with 3 Arcade expressions
var ds = $dataSources["YOUR_DS_ID"];
var features = ds.layer;

var total = Count(features);
var totalPop = Sum(features, 'POPULATION');
var avgAge = Round(Average(features, 'MED_AGE'), 1);

return '<p style="font-size:24px;font-weight:bold;margin:0;">'
  + Text(totalPop, '#,###') + '</p>'
  + '<p style="font-size:13px;color:#888;margin:4px 0 0;">'
  + total + ' regions · Avg age ' + avgAge + '</p>';
Lesson 20

Real-world Red Cross patterns

Everything you've learned translates directly to disaster response and humanitarian operations. Here are production-ready patterns for common Red Cross EB apps.

shelter-status.arcade — List conditional styling
// Shelter status — use no-code Condition tab with STATUS field
// Open → background #d4edda (green)
// At Capacity → background #fff3cd (yellow)
// Closed → background #f8d7da (red)

// Or with Arcade for more control:
var status = $feature.STATUS;
var occupancy = $feature.CURRENT_POP / $feature.CAPACITY;

var bg = "rgba(212,237,218,0.3)";  // green default

if (status == "Closed") {
  bg = "rgba(248,215,218,0.3)";
} else if (occupancy > 0.9) {
  bg = "rgba(255,243,205,0.4)";  // near capacity = yellow
} else if (occupancy > 0.75) {
  bg = "rgba(255,243,205,0.2)";  // filling up = light yellow
}

return {
  background: { color: bg },
  border: {
    borderLeft: {
      type: "solid", width: 4,
      color: IIf(status == "Closed", "#dc3545",
        IIf(occupancy > 0.9, "#ffc107", "#28a745"))
    }
  }
};
disaster-summary.arcade — Text widget with selection
function getFilteredFeatureSet(ds) {
  var result = ds.layer;
  var qp = ds.queryParams;
  if (!IsEmpty(qp.where)) { result = Filter(result, qp.where); }
  if (!IsEmpty(qp.geometry)) { result = Intersects(result, qp.geometry); }
  return result;
}

var ds = $dataSources["YOUR_SHELTER_DS"];
var filtered = getFilteredFeatureSet(ds);
var selected = ds.selectedFeatures;
var target = IIf(Count(selected) > 0, selected, filtered);

var totalShelters = Count(target);
var openShelters = Count(Filter(target, "STATUS = 'Open'"));
var totalPop = Sum(target, 'CURRENT_POP');
var totalCap = Sum(target, 'CAPACITY');
var pctFull = IIf(totalCap > 0,
  Round((totalPop / totalCap) * 100, 0), 0);

var pctColor = IIf(pctFull > 90, "#dc3545",
  IIf(pctFull > 75, "#ffc107", "#28a745"));

return '<h3 style="margin:0 0 8px;">Shelter Operations</h3>'
  + '<p style="margin:0;font-size:14px;">'
  + '<strong>' + openShelters + '</strong> of '
  + totalShelters + ' shelters open · '
  + '<strong>' + Text(totalPop, '#,###') + '</strong> residents'
  + '</p>'
  + '<p style="margin:4px 0 0;font-size:13px;color:'
  + pctColor + ';font-weight:bold;">'
  + pctFull + '% capacity</p>';
🏥 More Red Cross patterns to try
  • Damage assessment: Classify damage level (Destroyed/Major/Minor/Affected) with color-coded List items and aggregated counts in a summary widget
  • Resource tracking: Dynamic button icons showing supply levels (green checkmark/yellow warning/red alert)
  • Volunteer deployment: "X days ago" pattern on EditDate to show stale assignments
  • Feeding operations: Meals served / capacity ratio with color-coded density classification
  • Multi-shelter summary: One HTML Text widget combining total open/closed/at-capacity counts to conserve your 10-expression limit
🚀 You made it to the end!

You've gone from "what is Arcade?" to HTML returns, full style dictionaries, performance optimization, and production shelter dashboards. These 21 lessons cover the entire scope of what's possible with Arcade in Experience Builder as of 2026. The patterns are modular — mix and match them for any operational tool you need to build.