<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>Custom agent with slottable UI | Sample | ArcGIS Maps SDK for JavaScript</title>
<script type="module" src="https://js.arcgis.com/5.2/"></script>
.route-directions arcgis-directions {
<arcgis-map id="main-map" item-id="c3196ef421134b3f97cadcf97a6bf735">
<arcgis-zoom slot="top-left"></arcgis-zoom>
<arcgis-expand slot="top-left"><arcgis-legend></arcgis-legend></arcgis-expand>
<calcite-shell-panel slot="panel-end" width="l" height-scale="l">
reference-element="#main-map"
heading="Routes and directions"
entry-message="Navigate to an origin and a destination, then ask for a route between them."
<arcgis-assistant-navigation-agent></arcgis-assistant-navigation-agent>
<arcgis-assistant-data-exploration-agent></arcgis-assistant-data-exploration-agent>
<arcgis-assistant-help-agent></arcgis-assistant-help-agent>
<template id="directions-template">
<div class="route-directions">
reference-element="#main-map"
] = await $arcgis.import([
"@arcgis/core/layers/RouteLayer.js",
"@arcgis/core/rest/support/RouteParameters.js",
"@arcgis/core/rest/support/Stop.js",
"@arcgis/core/portal/Portal.js",
"@arcgis/core/rest/networkService.js",
"@arcgis/ai-components/agent-utils/SkillAgent.js",
"@arcgis/ai-components/agent-utils/tools/FunctionTool.js",
"@arcgis/ai-components/agent-utils/createSchema.js",
"@arcgis/ai-components/orchestrator/signals.js",
const mapElement = document.querySelector("arcgis-map");
const assistant = document.querySelector("arcgis-assistant");
const template = document.querySelector("#directions-template");
const directionsSlots = new Map();
// Adjust panel layout based on screen size
const panel = document.querySelector("calcite-shell-panel");
const narrowScreen = window.matchMedia("(max-width: 760px)");
function updatePanelLayout() {
panel.slot = narrowScreen.matches ? "panel-bottom" : "panel-end";
panel.layout = narrowScreen.matches ? "horizontal" : "vertical";
narrowScreen.addEventListener("change", updatePanelLayout);
// Listen for slottable requests and render directions UI accordingly
assistant.addEventListener("arcgisSlottableRequest", () => {
// find only slottable requests for directions UI
const requests = assistant.slottableRequests.filter(
(request) => request.name === "block" && request.data?.block?.type === "directions",
// Remove expired directions slots
for (const [slotName, element] of directionsSlots) {
if (!requests.some((request) => request.slotName === slotName)) {
directionsSlots.delete(slotName);
// Render new directions slots
for (const request of requests) {
const layerId = request.data.block.data?.layerId;
if (typeof layerId !== "string" || !layerId) continue;
let element = directionsSlots.get(request.slotName);
element = template.content.firstElementChild.cloneNode(true);
// the slot name is generated by the assistant for this request
element.slot = request.slotName;
element.querySelector("arcgis-directions").mapLayerId = layerId;
assistant.append(element);
directionsSlots.set(request.slotName, element);
// Function tool for solving routes
const solveRoute = new FunctionTool({
"Solve a route through ordered shared points, display its RouteLayer, emit interactive directions UI, and publish the route geometry as a shared polyline. Do not repeat turn-by-turn steps in chat.",
inputSchema: await createSchema({
items: { type: "string" },
"At least two exact point resource IDs from listSharedResources, in origin, waypoint, destination order.",
required: ["sharedResourceIds", "travelModeName"],
responseFormat: "content-and-artifact",
execute: async ({ sharedResourceIds, travelModeName }, config) => {
if (sharedResourceIds.length < 2) throw new Error("A route requires at least two shared points.");
await mapElement.viewOnReady();
// Get shared resources required for calculating the route
const resources = config?.state?.agentExecutionContext?.sharedResources;
const waypoints = sharedResourceIds.map((id) => {
const point = resources?.find((resource) => resource.id === id);
if (point?.kind !== "point") {
"Select valid points from listSharedResources. Navigate to missing locations before solving.",
// Get network service for calculating a route
const portal = Portal.getDefault();
const url = portal.helperServices?.route?.url;
if (!url) throw new Error("The portal does not provide a route helper service.");
const description = await networkService.fetchServiceDescription(url);
const travelMode = description.supportedTravelModes?.find((mode) => mode.name === travelModeName);
if (!travelMode) throw new Error(`The service does not support ${travelModeName}.`);
const title = `Route: ${waypoints.map((point) => point.description).join(" to ")}`;
const routeLayer = new RouteLayer({
geometry: { ...point.payload, spatialReference: point.payload.spatialReference ?? { wkid: 4326 } },
// Solve the route with the specified parameters
const result = await routeLayer.solve(
ignoreInvalidLocations: false,
outSpatialReference: mapElement.spatialReference,
{ signal: config?.signal },
routeLayer.update(result);
mapElement.map.add(routeLayer);
const geometry = routeLayer.routeInfo?.geometry;
if (!geometry) throw new Error("The route service returned no route geometry.");
await mapElement.goTo(geometry);
// to display interactive UI within the chat interface, the agent must emit a UX suggestion
// including data the application can use for rendering the interactive directions
await sendUXSuggestion({ type: "directions", data: { layerId: routeLayer.id } }, config);
// return the agent message to the chat and add the resulting route information as a shared resource
// for other agents to use, such as the Data Exploration Agent
"Route solved and added to the map. Interactive directions are displayed in chat; do not repeat the steps.",
totalDistanceMeters: routeLayer.routeInfo.totalDistance,
totalDurationMinutes: routeLayer.routeInfo.totalDuration,
value: { layerId: routeLayer.id },
sharedResourceAdditions: [
{ kind: "polyline", description: `${travelModeName} ${title}`, payload: geometry.toJSON() },
// definition for the custom network analysis agent
const networkAnalysisAgent = new SkillAgent({
name: "Network Analysis",
"Solve routes between shared locations, display interactive directions, and publish reusable route geometry.",
systemPrompt: "You are an ArcGIS network analysis agent.",
"Use shared resources from earlier agents for spatial inputs and publish reusable geometry through tool artifacts.",
description: Solve routes between shared locations and display interactive directions.
allowed-tools: listSharedResources solveRoute
- Call "listSharedResources" with kinds: ["point"] before solving a route.
- Select only points whose descriptions clearly match the user's locations. Never invent coordinates or IDs.
- If a required point is missing or ambiguous, ask the user to navigate to it or clarify the location.
- Pass at least two exact "sharedResourceIds" in origin, intermediate stops, destination order.
- Preserve the user's requested stop order. Ask for clarification if it is ambiguous.
- Use Walking, Driving, or Trucking modes as requested. Prefer Time unless distance is explicitly requested.
- "solveRoute" solves and adds the route layer in one call. Do not ask about output format.
- Give a brief confirmation and the returned duration in minutes and distance in meters when available.
- Never invent metrics or repeat turn-by-turn directions in chat; the component displays them. Base claims about map changes only on successful tool results.`,
toolLoaders: { solveRoute: async () => solveRoute },
// register the custom agent with the assistant as a child component
const agentElement = document.createElement("arcgis-assistant-agent");
agentElement.agent = networkAnalysisAgent;
assistant.append(agentElement);
assistant.suggestedPrompts = [
"Go to Messe Frankfurt.",
"Find a walking route from Messe Frankfurt to Frankfurt Central train Station.",