This is the documentation of version 0.48.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-authored GeoJSON alongside the built-in stack. A sales territories overlay, a delivery zones layer, customer pin sets, isochrone polygons computed by your backend — anything you can express as a GeoJSON FeatureCollection.

Each BYOD source becomes a byod entry in agent state. The model can then style it with MapLibre layers chosen from the data's detected schema (setByodLayers), reference it by id from any unified data tool (analyseData, processData), toggle its visibility on the map, or recall its contents.

Lifecycle

Your databyod-N entry in stateRendered on the mapanalyseData / processData inputrecallByodsetByodLayersURL-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 most conversational path: the model is told the URL by the user and calls addByodSource. URL fetching is policy-gated for safety — only http:// and https:// are allowed, responses are capped at 25 MB, the request times out at 15 seconds, and the body is parsed as GeoJSON before being committed to state.

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

The show object (omit it to store the source hidden) mirrors discoverPlaces minus the marker style: zoomMode ("auto" fits the camera to the rendered entries, "none" keeps the map still) and hidePreviousEntries ("all" or a list of ids to clear earlier BYOD layers before this one renders).

The tool returns the new entry's id (byod-0, byod-1, …), a one-line summary the model relays back to the user, and an auto-detected data profile describing the FeatureCollection's shape.

2. Inline FeatureCollection (also via addByodSource)

When the data is already in scope — pasted into the chat, generated by a previous tool, or held by your application — pass data instead of url:

addByodSource({
    label: 'Customer pins 2026-Q1',
    data: { type: 'FeatureCollection', features: [/* … */] },
});

No fetch policy applies — the data was never out on the network. As with every ingest path, the new entry has no layers and renders nothing until the agent sets them from the detected schema with setByodLayers (see Styling).

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

Whichever ingest path you use — URL or inline — the slice derives a compact data profile in a single pass over the FeatureCollection the moment the entry is added. A BYOD source's only guaranteed shape is "GeoJSON FeatureCollection"; everything past that — which geometry types it carries, which feature properties exist — is whatever the customer's data happens to hold. The profile captures exactly that, so the model can reason about the data without ever receiving the raw features:

addByodSource returns the full profile in its result; recallByod surfaces it too (propertyNames in the list view, the full profile when you pass an id). The profile is bounded — capped property and example counts, truncated long strings — so it stays cheap to ship to the model even for wide or large datasets.

Why this matters: the model needs the exact property keys to write correct analyseData / processData code (e.g. territory.properties.name below). The profile hands it those keys — types, coverage, and sample values included — straight from ingest, so it never has to pull raw GeoJSON just to discover the schema.

The profile is inferred locally and dependency-free — one linear scan, no extra service call and no schema upload. It is stored on the entry as a BYODDataProfile and re-used by every recall path.

Inspecting what's loaded

// List every BYOD entry (compact summary)
recallByod();

// Full FeatureCollection of one entry
recallByod({ id: 'byod-0' });

Like every recall tool, recallByod is opt-in: it returns the full data only when an id is provided, so the LLM doesn't accidentally pull a megabyte of features into context. The list view stays light — id, label, featureCount, geometryTypes, propertyNames, source, shown — while passing an id adds the full data profile (per-property types / coverage / examples) alongside the raw FeatureCollection.

Toggling visibility

// Show one entry
updateByodDisplay({ entryIds: ['byod-0'], action: 'show' });

// Hide everything BYOD-related
updateByodDisplay({ action: 'hide', clearAll: true });

Visibility is independent of presence in state — hiding an entry doesn't remove it from byodEntryIDs inputs to analyseData / processData. That's deliberate: the model can compute against the data without rendering 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 = {};
        for (const territory of byod.features) {
            const inside = places.features.filter((p) =>
                turf.booleanPointInPolygon(p, territory),
            );
            counts[territory.properties.name] = inside.length;
        }
        return counts;
    `,
});

The injected byod identifier is a merged FeatureCollection over every id passed in byodEntryIDs. Mixed-geometry collections are normal — Point, LineString, and Polygon features can all live in one BYOD entry. The model knows properties.name exists on each territory because the entry's data profile surfaced that key (with its type and coverage) at ingest — it doesn't need to peek at raw features first.

For the cross-kind operations the sandbox supports (turf.booleanPointInPolygon, turf.buffer, h3.polygonToCells, etc.), see Code generation.

Disabling BYOD entirely

For deployments where you don't want any BYOD surface at all — for example, an embed where you trust only TomTom-sourced data — switch the kind off via dataEntries:

const agent = createMapAgent(map, {
    model: openai('gpt-4o'),
    dataEntries: {
        byod: { enabled: false },
    },
});

This drops addByodSource, setByodLayers, recallByod, and updateByodDisplay from the registry AND removes byod from analyseData / processData scope. The classifier won't pick BYOD tools and the model won't see them in its prompt. For data you own upfront, model it as your own state and tools rather than the BYOD slice.

Styling: the agent decides the layers

A new BYOD entry has no layers — addByodSource renders nothing on its own, and there are no automatic geometry-typed defaults. Choosing the layers that fit the data is the agent's job on every ingest, via 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 supplied layers replace the entry's current set wholesale. On a shown entry they apply live in place (no flicker) via the module's runtime layer diff; pass show to render a hidden entry or re-fit the camera. Invalid specs are rejected with a semantic error and leave the previous styling intact, so a bad attempt never breaks the map.

Because the agent reads the entry's data profile — property names, JSON types, coverage, examples — before choosing, it can pick the encoding that suits the schema: graduate a circle-radius or colour ramp by a well-covered numeric field, colour a fill / line by a small categorical set, or switch dense points to a symbol. Even when no single property dominates, it still picks legible layers suited to the geometry rather than leaving the entry blank. (setByodLayers also steers toward basemap-legible styling — translucent fills with outlines, text halos, high-contrast palettes.)

Related