Bring your own data

BYOD (bring-your-own-data) layers let the agent read and render customer GeoJSON next to the built-in data. Any GeoJSON FeatureCollection works, such as sales territories, delivery zones, customer pins or polygons your backend computes.

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

Your databyod-N entry in stateRendered on the mapanalyseData / processData inputrecallState({ kind: 'byod' })setByodLayersURL-hosted .geojsonInline FeatureCollection addByodSource({ url })addByodSource({ data })setByodLayers (decide layers from profile)updateByodDisplaybyodEntryIDs: [id]{ id }

Two ways to ingest

1. URL-hosted layer (via the addByodSource tool)

The model calls addByodSource with a URL the user gives it. The body must parse as a GeoJSON FeatureCollection.

addByodSource({
    label: 'Sales territories',
    url: 'https://example.com/territories.geojson',
});

Every fetch has these limits:

Restricting which URLs may be fetched

By default, addByodSource loads only public https:// URLs. It refuses these before any request leaves the browser:

A browser can't resolve a hostname before it fetches. A public hostname that points at a private address passes the check, so a policy of your own is the stronger control.

To set your own policy, set byod.validateSourceUrl. It replaces the default policy, receives the parsed URL before the fetch, and may be async, for an allowlist lookup. Return { valid: true } to allow or { valid: false, reason } to block, and the model gets reason as the tool error. The limits listed above still apply.

const agent = createMapAgent(map, {
    model: openai('gpt-4o'),
    byod: {
        validateSourceUrl: (url) => {
            const allowed = new Set(['data.example.com', 'cdn.example.com']);
            return url.protocol === 'https:' && allowed.has(url.hostname)
                ? { valid: true }
                : { valid: false, reason: `Host "${url.hostname}" is not on the BYOD allowlist.` };
        },
    },
});

To load data from your own origin or a local development server, allow exactly those URLs in your validator. The check applies only to URLs, since 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 without the model, with await agent.state.byod.addEntry(featureCollection, 'Depots') and optional layers, which await agent.state.byod.showEntry(id) then renders.

Data you own upfront belongs in your own tools, not here. BYOD state is for runtime data, which you don't know ahead of time and the user introduces mid-conversation. If you already own the data and its schema, routing it through BYOD only gives you a generic byod-N entry with no domain meaning, and leaves the agent guessing layers from an inferred profile. Model it as your own state and tools instead. They receive the same state object 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 when the entry is added. A source's geometry types and properties can be anything a GeoJSON FeatureCollection holds. The profile captures them, so the model can reason about the data without the raw features:

The profile is bounded, so it stays cheap even for wide datasets. It caps the property and example counts (propertiesOmitted says how many were cut) and truncates long strings. The slice infers it locally, with no service call, and stores it 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 provides them 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 …"). So every BYOD result the model sees gets a safe profile:

The full profile, including string examples, 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, and 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. With an id, it returns the safe data profile. Neither returns the raw FeatureCollection, which would flood the context and whose 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 the entry.

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

To turn BYOD off, for example in an embed that trusts only TomTom data, disable the kind 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 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. When no property stands out, it still picks legible layers.

Related