Bring your own data
BYOD (bring-your-own-data) layers let the agent read and render customer GeoJSON next to the built-in data: sales
territories, delivery zones, customer pins, polygons computed by your backend — any GeoJSON FeatureCollection.
Each source becomes a byod entry in state. The model styles it with MapLibre layers chosen from the
data's detected schema (setByodLayers), computes over it with analyseData / processData, shows or hides it, and
recalls its shape.
Lifecycle
Two ways to ingest
1. URL-hosted layer (via the addByodSource tool)
The user gives the model a URL and it calls addByodSource. The fetch is bounded: http:// and https:// only, 25 MB at
most, a 15-second timeout, and the body must parse as GeoJSON.
addByodSource({
label: 'Sales territories',
url: 'https://example.com/territories.geojson',
});
Restricting which URLs may be fetched
The built-in policy bounds the cost of a fetch, not where it goes: the URL is whatever the model, steered by the
user, supplies — an internal address or a cloud-metadata endpoint included. Add your own policy with
byod.validateSourceUrl. It runs after the scheme check and before the fetch, receives the parsed URL, and
returns { valid: true } to allow or { valid: false, reason } to block, with reason surfaced to the model as the
tool error. It may be async, for an allowlist lookup.
const agent = createMapAgent(map, {
model: openai('gpt-4o'),
byod: {
validateSourceUrl: (url) => {
const allowed = new Set(['data.example.com', 'cdn.example.com']);
return allowed.has(url.hostname)
? { valid: true }
: { valid: false, reason: `Host "${url.hostname}" is not on the BYOD allowlist.` };
},
},
});
Only the URL path is gated — inline data never touches the network. You can also assign
agent.state.byod.sourceUrlValidator directly.
addByodSource returns the new entry's byodEntryId (byod-0, byod-1, …), its label and source, and the
data profile. The entry starts hidden, with no layers.
2. Inline FeatureCollection (also via addByodSource)
When the data is already at hand — pasted into the chat or produced by an earlier step — the model passes data
instead of url:
addByodSource({
label: 'Customer pins 2026-Q1',
data: { type: 'FeatureCollection', features: [/* … */] },
});
No fetch policy applies. As with every ingest path, the entry renders nothing until the agent sets its layers.
Your app can add an entry the same way, without the model: await agent.state.byod.addEntry(featureCollection, 'Depots'),
optionally with layers to render it straight away.
Data you own upfront belongs in your own tools, not here. BYOD state is for runtime data — GeoJSON you don't know ahead of time and the user introduces mid-conversation. When you already own the data and its schema, routing it through BYOD only gets you a generic
byod-Nentry with no domain meaning, leaving the agent to guess layers from an inferred profile. Model it as your own state and tools instead — they receive the samestateobject and compose with the built-in stack.
Auto-detected data profile
Whatever the ingest path, the slice derives a compact data profile in one pass the moment the entry is added. Past
"GeoJSON FeatureCollection", a source's geometry types and properties are whatever its data holds; the profile
captures them, so the model can reason about the data without receiving the raw features:
featureCount— number of features in the collection.geometryTypes— the geometry types present (Point,LineString,Polygon,MultiPolygon, …).properties— one entry per feature-property key, highest-coverage first. Each carries itsname, the JSONtypesseen across the data (["string"],["number", "null"], …),coverage(the fraction of features that carry the key —1means every feature), and a few short, distinctexamples.
The profile is bounded — capped property and example counts (propertiesOmitted says how many were cut),
truncated long strings — so it stays cheap even for wide datasets. It is inferred locally, with no service call, and
stored on the entry as a BYODDataProfile.
The model needs the exact property keys to write correct analyseData / processData code, such as
territory.properties.name below; the profile hands them over at ingest.
Untrusted data never flows back to the model
BYOD content comes from outside, so its string property values are untrusted — a feature could carry
prompt-injection text ("Ignore previous instructions and …"). Every model-facing BYOD result therefore passes through
toByodSafeProfile:
- String
examplesare withheld. Each property keeps itsname,typesandcoverage, plus numeric and boolean examples, which can't carry instructions. recallStatenever returns the raw GeoJSON. To compute over the values, the model hands the entry toanalyseData/processData, whose code returns a result, not the raw text.
The full profile, string examples included, stays on the entry in state.byod for your app to show the user.
Untrusted content may reach the user, but not the model as if it were instructions. This is one layer of defence;
the code-generation threat model covers the others.
Inspecting what's loaded
// List every BYOD entry (compact summary)
recallState({ kind: 'byod' });
// Full data profile of one entry (never the raw GeoJSON)
recallState({ kind: 'byod', id: 'byod-0' });
The list returns each entry's id, label, timestamp, featureCount, geometryTypes, propertyNames, source
and shown; an id returns the safe data profile. Never the raw FeatureCollection —
it would flood the context, and its strings are untrusted.
Toggling visibility
// Show one entry, hiding the other BYOD entries
updateByodDisplay({ entryIds: ['byod-0'], action: 'show', hideOthers: true });
// Hide every BYOD entry
updateByodDisplay({ action: 'hide', clearAll: true });
// Remove an entry from the session
updateByodDisplay({ entryIds: ['byod-1'], action: 'remove' });
A hidden entry stays in state and remains a valid byodEntryIDs input, so the model can compute over data without
rendering it; remove deletes it.
Using BYOD in unified data tools
Once an entry exists, the model can pass it into analyseData / processData like any other entry kind:
analyseData({
byodEntryIDs: ['byod-0'],
placesEntryIDs: ['places-2'],
name: 'territory-coverage',
code: `
const counts = {};
const territories = byodByEntry['byod-0'].features;
const placeFeatures = placesByEntry['places-2'].features;
for (const territory of territories) {
const inside = placeFeatures.filter((p) =>
turf.booleanPointInPolygon(p, territory),
);
counts[territory.properties.name] = inside.length;
}
return counts;
`,
});
byodByEntry maps each id in byodEntryIDs to its FeatureCollection; one entry may mix Points, LineStrings and
Polygons. The model knows properties.name exists because the data profile reported it.
Code generation lists the cross-kind operations available.
Disabling BYOD entirely
Where you want no BYOD at all — say, an embed that trusts only TomTom data — switch the kind off with dataEntries:
const agent = createMapAgent(map, {
model: openai('gpt-4o'),
dataEntries: {
byod: { enabled: false },
},
});
This drops addByodSource, setByodLayers and updateByodDisplay, and removes byod from the
analyseData / processData inputs.
Styling: the agent decides the layers
A new BYOD entry has no layers and no automatic defaults. On every ingest the agent picks the layers that fit the
data with setByodLayers, which is also how the entry first becomes visible:
// Colour polygons by a categorical field
setByodLayers({
byodEntryId: 'byod-0',
layers: [
{
type: 'fill',
paint: {
'fill-color': ['match', ['get', 'region'], 'North', '#1f77b4', 'South', '#ff7f0e', '#cccccc'],
'fill-opacity': 0.4,
'fill-outline-color': '#333',
},
},
],
});
// Graduate point size by a numeric field
setByodLayers({
byodEntryId: 'byod-1',
layers: [
{
type: 'circle',
paint: {
'circle-radius': ['interpolate', ['linear'], ['get', 'revenue'], 0, 3, 100000, 20],
'circle-color': '#d62728',
},
},
],
});
- The
layersreplace the entry's current set. Each takes a MapLibretypesuited to the geometry (circle,symbolorheatmapfor points,line,fillorfill-extrusion), withpaint,layout,filterand an optionalid. - On a shown entry they apply live, without flicker. To render a hidden entry, pass
show: { zoomMode: 'auto' | 'none', hidePreviousEntries? }—'auto'fits the camera,hidePreviousEntries('all'or a list of ids) clears earlier BYOD entries. - An invalid spec is rejected with a semantic error and the previous styling stays.
beforeIDdraws a layer below a given style layer. Prefer the SDK'smapStyleLayerIDsanchors, by raw name:'Borders - Treaty label'(lowestLabel) for fills, lines and heatmaps so labels stay on top. An id missing from the current style drops the layer.
The agent reads the entry's data profile first, so it can graduate a circle-radius or
colour ramp by a well-covered number, colour a fill or line by a small set of categories, or switch dense points to a
symbol — and still picks legible layers when no property stands out.
Related
- State — the
byodslice's shape andentryModesemantics. - Customizing tools — wrap BYOD ingest in your own domain-specific tools (e.g. "Load the Q1 sales territories").
- Scope-aware data tools — passing
byodintoanalyseData/processData. - Code generation — what the sandbox can do with BYOD features.
- Securing your agent — why BYOD string values are withheld from the model, and the other places untrusted text arrives.