← All writing
March 31, 2026
GIS
ArcGISDeck.glqueryFeaturesJavaScriptGPUFeature LayerMapLibre
gis

The AGOL Bridge: queryFeatures() to Deck.gl

The ArcGIS JavaScript SDK and Deck.gl seem like they live in different worlds. One is ESRI's flagship web mapping toolkit. The other is an open-source visualization framework built by Uber. They have different APIs, different documentation ecosystems, and different communities of developers.

But they connect through a single operation: queryFeatures().

Once you understand the bridge, the two worlds stop being separate. Your AGOL data — the Feature Layers your organization has spent years maintaining — becomes the input to one of the most capable visualization tools available on the web.


What the Bridge Looks Like

The pattern has three steps: query the Feature Layer, reshape the response, hand it to Deck.gl.

import FeatureLayer from '@arcgis/core/layers/FeatureLayer';

// 1. Point at your AGOL layer
const shelterLayer = new FeatureLayer({
  url: 'https://services.arcgis.com/{org}/arcgis/rest/services/Shelters/FeatureServer/0'
});

// 2. Query it
const result = await shelterLayer.queryFeatures({
  where: "STATUS = 'OPEN'",
  outFields: ['NAME', 'CAPACITY', 'OCCUPANCY'],
  returnGeometry: true
});

// 3. Reshape the response into the shape Deck.gl expects
const shelters = result.features.map(f => ({
  name: f.attributes.NAME,
  capacity: f.attributes.CAPACITY,
  load: f.attributes.OCCUPANCY / f.attributes.CAPACITY,
  lon: f.geometry.x,
  lat: f.geometry.y
}));

// 4. Hand to Deck.gl — nothing else changes
new deck.ScatterplotLayer({
  data: shelters,
  getPosition: d => [d.lon, d.lat],
  getRadius: d => Math.sqrt(d.capacity * d.load) * 120,
  getFillColor: d => d.load > 0.85 ? [237,27,46,200] : d.load > 0.65 ? [184,134,11,200] : [45,90,39,200],
  pickable: true
})

That's the entire bridge. The Deck.gl layer doesn't know or care that the data came from AGOL. It just sees a JavaScript array of objects with position and attribute fields.


The Part That Actually Does the Work

Step 3 is where the connection happens:

const shelters = result.features.map(f => ({
  name: f.attributes.NAME,
  capacity: f.attributes.CAPACITY,
  load: f.attributes.OCCUPANCY / f.attributes.CAPACITY,
  lon: f.geometry.x,
  lat: f.geometry.y
}));

The .map() call transforms each feature from AGOL's format into the shape Deck.gl expects. AGOL returns features with an attributes object and a geometry object. Deck.gl wants flat objects with whatever field names you choose.

The .map() in the middle does the translation. Field by field, you pull values out of AGOL's structure and put them into the flat object shape. You can also compute new values at this step — dividing OCCUPANCY by CAPACITY to get a load percentage, for example — before the data ever reaches the renderer.

After .map() runs, the data is plain JavaScript. No ESRI types, no SDK objects, no connection back to AGOL. It's an array of plain objects that any tool can consume.


The async/await Requirement

queryFeatures() makes a network request. It sends a query to your AGOL Feature Layer's REST endpoint and waits for the response to come back. That round trip takes time — typically under a second on a good connection, but it's not instantaneous.

JavaScript handles this with async/await. The await keyword pauses execution at that line until the response arrives, then continues with the result:

// This doesn't work — the query hasn't finished before you use the result
const result = featureLayer.queryFeatures({...});
const shelters = result.features.map(...);  // result is undefined here

// This works — await pauses until the data arrives
const result = await featureLayer.queryFeatures({...});
const shelters = result.features.map(...);  // result has your data

In practice, you wrap the entire sequence in an async function that runs when the page loads:

async function initMap() {
  const result = await shelterLayer.queryFeatures({...});
  const shelters = result.features.map(f => ({...}));
  
  // Initialize Deck.gl with your data here
  const overlay = new deck.MapboxOverlay({
    layers: [new deck.ScatterplotLayer({ data: shelters, ... })]
  });
  map.addControl(overlay);
}

initMap();

async/await is the only JavaScript concept the bridge requires that might be unfamiliar. The rest is standard object manipulation.


How the Layers Cooperate

MapLibre handles the base map. Deck.gl renders your data on top. The MapboxOverlay adapter is the single line that connects them:

map.on('load', () => {
  const overlay = new deck.MapboxOverlay({
    interleaved: false,
    layers: buildLayers()
  });
  map.addControl(overlay);
});

MapLibre draws the tiles — the dark background, the roads, the coastlines. Deck.gl draws your features — the shelter circles, the evacuation arcs, the density hexbins — on a transparent layer above the map. They share the same camera so they move together as you pan and zoom. From the user's perspective it's one map. Underneath, they're two completely separate rendering engines cooperating through a thin adapter.


What ESRI Is and Isn't Doing

In this architecture, ESRI is doing exactly what it's best at: hosting authoritative geospatial data and serving it reliably at scale. The Feature Layer REST API is production-grade infrastructure that your organization is already paying for and already trusting for critical response data.

What ESRI is not doing: rendering the visualization, determining the visual appearance of features, or deciding how the data gets displayed. That work moves to Deck.gl.

This is not a workaround or a hack. It's what the Feature Layer REST API was designed for — to be queryable by any compliant client. Deck.gl is a compliant client. The AGOL data and the Deck.gl renderer don't know they're ESRI and open-source. They just know that one is providing JSON and the other is consuming it.


The One Build That Makes This Click

Reading about the pattern is one thing. Building it from scratch — starting with a real Feature Layer URL, writing the queryFeatures() call, reshaping the response, and watching your AGOL data appear as Deck.gl geometry — is something else.

The hurricane evacuation flow demo at jbf.com/gis-sdk-live.html shows the rendered result. The next step is replacing the synthetic data arrays in that demo with a live queryFeatures() call against your own Feature Layer. Once you've done it once, the bridge becomes second nature — because there's nothing conceptually difficult about it once you've seen it work with real data.


Related: GPU vs CPU — What It Actually Means for Your Map