This is the documentation of version 0.64.0. Go to the latest version, 1.0.0-rc.5.

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

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 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-N entry 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 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 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:

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:

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 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