Geo
The four typed geo operations on the Platform SDK — resolve a coordinate locally, discover authorized layers, and query features around an area without guessing precision
geo is a platform capability, like LLM or web search. Your organization picks
one geo provider (an installed extension such as an ArcGIS or a GeoServer
endpoint); your code calls four typed operations and never branches on which
provider answered.
import { createHttpPlatformTransport, createScrydonSDK } from '@scrydon/sdk/platform'
const platform = createScrydonSDK(
createHttpPlatformTransport({
baseUrl: 'https://scrydon.com',
headers: () => ({ authorization: `Bearer ${accessToken}` }),
})
)
const { location } = await platform.geo.locations.resolve({
location: { kind: 'mgrs', reference: '33UUP9005' },
})| Operation | HTTP | What it does |
|---|---|---|
geo.locations.resolve | POST /api/platform/v1/geo/locations/resolve | Converts a typed location into WGS84, in-process. No provider is contacted. |
geo.layers.list | POST /api/platform/v1/geo/layers/list | Lists the layers your selected provider serves. |
geo.layers.describe | POST /api/platform/v1/geo/layers/describe | Describes one layer: fields, geometry type, source CRS, extent, attribution. |
geo.features.query | POST /api/platform/v1/geo/features/query | Returns features inside an area, optionally filtered. |
All four are read-only and safe to retry.
Locations are typed — the platform never guesses
There is one location shape, a discriminated union on kind. You always name
the coordinate system; an unlabelled numeric pair is accepted nowhere.
kind | Fields | Example |
|---|---|---|
latLng | lat, lng (numbers, WGS84 degrees) | { kind: 'latLng', lat: 47.89, lng: 13.53 } |
dms | lat, lng (strings) | { kind: 'dms', lat: "47°53'36\"N", lng: "13°32'06\"E" } |
mgrs | reference (string) | { kind: 'mgrs', reference: '33UUP9005' } |
utm | zone, hemisphere, easting, northing | { kind: 'utm', zone: 33, hemisphere: 'north', easting: 390000, northing: 5305000 } |
ups | hemisphere, easting, northing | { kind: 'ups', hemisphere: 'north', easting: 2000000, northing: 2200000 } |
A malformed reference is a typed rejection, never a best guess. It comes back as
INVALID_REQUEST with a details.geoParseCode naming what was wrong:
empty_input, malformed, out_of_range, invalid_zone, or
odd_digit_count.
geo.locations.resolve — precision you can trust
A grid reference names a cell, not a point. 33UUP is a 100 km square;
33UUP9005 is a 1 km square inside it. The result therefore carries the cell
alongside the representative point, so nothing downstream can claim precision
the operator never supplied.
const { location, executionId } = await platform.geo.locations.resolve({
location: { kind: 'mgrs', reference: '33UUP' },
})
location.point // [lng, lat] — the cell CENTRE, not a corner
location.extent // [west, south, east, north] — present because the input named a cell
location.precisionMeters // 100000 — the cell size the reference actually declared
location.original // the input, echoed back
location.provenance // 'local' — converted in-process, no provider contactedextentandprecisionMetersare present exactly when the input named a cell (mgrs). AlatLng,dms,utmorupsinput names a point and carries neither.precisionMetersis one of1,10,100,1000,10000,100000.- Positions are GeoJSON order —
[lng, lat], never[lat, lng]— and are 2D. Altitude is dropped before it reaches this contract.
This operation runs inside the platform against the local conversion kernel. A grid reference an operator typed never leaves the platform to be converted, and no geo provider needs to be configured for it to work.
geo.layers.list and geo.layers.describe
const { layers, vendor } = await platform.geo.layers.list({})
const { layer } = await platform.geo.layers.describe({ layer: layers[0].id })
layer.id // opaque, provider-scoped — pass it back verbatim
layer.geometryType // 'point' | 'line' | 'polygon' | 'multi' | 'unknown'
layer.fields // [{ name, type, alias? }] — query and display names
layer.sourceCrs // what the provider publishes, e.g. 'EPSG:3857'
layer.extent // [west, south, east, north], when the provider declares one
layer.attribution // display this when you render the layer
layer.supportedOperations // ['listLayers', 'describeLayer', 'queryFeatures']Results are always WGS84 regardless of sourceCrs; axis order and projection
are normalized before anything reaches you.
supportedOperations is per layer. A provider can implement feature queries
while an individual layer still refuses them, so check the layer before offering
a query against it. Whether the provider implements an operation at all is a
separate question the platform answers for you — see
Errors.
geo.features.query
const page = await platform.geo.features.query({
layer: layer.id,
area: { kind: 'location', location: { kind: 'mgrs', reference: '33UUP9005' } },
filters: [{ field: 'status', op: 'eq', value: 'active' }],
fields: ['status', 'name'],
limit: 200,
})
page.features // [{ id?, geometry, properties }] — bounded GeoJSON geometry
page.truncated // true when there is more than this page holds
page.cursor // pass back as `cursor` for the next page
page.retrievedAt // when the PLATFORM retrieved this page (ISO 8601)
page.attribution // display this alongside the resultsThe area is a bbox, or a location that names a cell
area: { kind: 'bbox', bbox: [13.5, 47.8, 13.6, 47.9] } // [west, south, east, north]
area: { kind: 'location', location: { kind: 'mgrs', reference: '33UUP9005' } }A location area works by taking the resolved cell extent as the search
envelope. A location kind that names a point — latLng, dms, utm, ups —
has no extent, so it is refused with INVALID_REQUEST rather than turned into
an area by inventing a radius you never supplied. If you want a radius, say so
with an explicit bbox.
Filters are structured, never a query string
filters: [
{ field: 'status', op: 'eq', value: 'active' },
{ field: 'altitude', op: 'gte', value: 1000 },
{ field: 'callsign', op: 'in', value: ['ALPHA', 'BRAVO'] },
]op is one of eq, ne, gt, gte, lt, lte, like, in; value is a
string, number, boolean, or an array of those. There is no raw CQL, SQL or
where-clause input — the provider's query language is compiled inside the
extension, so nothing you send is interpolated into one.
Truncation is explicit
truncated is always present and always honest: a page the provider capped
reads as truncated, never as complete. When a cursor is present, pass it back
with the same query to continue. If a truncated result has no cursor, narrow the
area or filters; do not interpret the page as the complete dataset.
retrievedAt is when the platform fetched the page. It is deliberately not
the age of the underlying data — if a layer publishes observation or update
times, they arrive in each feature's properties, and that is the field to show
an operator who asks how fresh a feature is.
Choosing a provider
Every operation except locations.resolve accepts an optional provider. Omit
it — or send "auto" or "" — and your organization's configured geo provider
answers. Send a provider explicitly and that choice is pinned for the call:
credentials, the support check, and dispatch all resolve to the same one.
Limits
| Limit | Value |
|---|---|
| Positions per geometry | 4096 |
| Rings per polygon | 32 |
| Geometries per multi-part | 256 |
| Filters per query | 32 |
| Features per page | 500 |
These are contract bounds, not advice. A provider response that exceeds them is
reported as a PROVIDER_CONTRACT_VIOLATION rather than silently truncated.
Errors
Every failure is a PlatformApiError carrying a stable code. Geo adds no new
codes.
| Code | HTTP | When |
|---|---|---|
INVALID_REQUEST | 400 | A malformed location or reference (details.geoParseCode), a point-kind location used as an area, or a body that fails the contract. |
CAPABILITY_NOT_CONFIGURED | 409 | No geo provider is configured for this scope, no binding exists, or the selected provider does not serve the operation you called. |
POLICY_BLOCKED | 422 | A governance decision — read details.policyCode. |
PROVIDER_CONTRACT_VIOLATION | 502 | The provider returned geometry or a page outside the contract. |
PROVIDER_UNAVAILABLE | 503 | The provider could not be reached. |
DEADLINE_EXCEEDED | 504 | The call ran past its deadline. |
A destination your organization has not allowlisted is not a 503. It is
refused before the request leaves the platform, as POLICY_BLOCKED (422) with
details.policyCode: "EGRESS_BLOCKED_BY_POLICY" and retryable: false —
because retrying cannot change an allowlist. Add the endpoint's exact hostname
under Settings → Governance → Egress. This is distinct from
EGRESS_BLOCKED_BY_CLEARANCE below, which is a classification decision about
data rather than a decision about the destination.
An empty features array is a success, not an error: the area held nothing
matching. It is distinct from unsupported (409), unavailable (503), and denied
(422), and the distinction is pinned by contract tests.
import { PlatformApiError } from '@scrydon/sdk/platform'
try {
await platform.geo.features.query({ layer, area })
} catch (err) {
if (err instanceof PlatformApiError && err.code === 'CAPABILITY_NOT_CONFIGURED') {
// Ask an administrator to configure a geo provider, or pick another layer.
}
}CAPABILITY_NOT_CONFIGURED on a query when layers.list worked usually means
the selected provider implements layer listing and not feature query. The
platform refuses that before contacting the provider, so it reads as a
configuration answer rather than a provider outage. The error's details name
the operation you asked for and the methods the provider does serve.
Using Geo in Cortex
Cortex exposes the same platform operations through three tools:
| Tool | Use |
|---|---|
resolve_location | Convert an explicitly typed coordinate; retain a grid cell's precision and extent. |
list_geo_layers | Discover provider layers and their supported operations, or describe a selected layer. |
query_geo_features | Query a selected provider layer within an explicit bbox or resolved grid cell and display the results on a map. |
For example, ask Cortex to resolve MGRS 33UUP9005, list the available geo
layers, then query a chosen layer within that cell. A point coordinate needs an
explicit bounding box before a feature query; Cortex does not invent a radius.
These tools use your scoped platform connection. query_map searches your
organization's ontology objects; it does not query ArcGIS or GeoServer layers.
An explicit provider and layer selection stays attached to the request.
The map shows provider attribution and draws polygon boundaries as outlines. A resolved grid cell is shown as an enclosing envelope, not its exact footprint. Partial results are marked as truncated; use the returned continuation cursor to request another page. Retrieval time tells you when the platform fetched the page, while observation times remain properties of the source features.
An empty result means the query succeeded with no matching features. A missing provider, denied request or unavailable service is reported as a failure.
Governance
Coordinates are not redacted. A rewritten grid reference is a corrupted
position, not a protected one, so positions are never governed by content
redaction. Text is governed the usual way: attribute-filter values on the
way in, and feature properties, layer titles, descriptions, field aliases and
attribution on the way out. Filter field and op are structural and are left
intact — a redacted field name would address no column.
Where a position is governed depends on whether the call leaves the platform.
geo.locations.resolve converts in-process and contacts no provider, so there
is no egress boundary to govern. geo.layers.list, geo.layers.describe and
geo.features.query dispatch to your installed geo provider, and those are
clearance-gated: the classification your run asserts is compared against the
rating on the connection that resolved, before the provider is contacted.
A connection nobody has rated counts as unrated, which any classified run
outranks.
What happens on a violation is your organization's classification mode:
| Mode | On a violation |
|---|---|
enforce | The call is refused with POLICY_BLOCKED and details.policyCode: "EGRESS_BLOCKED_BY_CLEARANCE". |
audit, test_with_notifications | The call proceeds; the violation is recorded in the audit log. |
disabled | No comparison is made. |
A run that asserts no classification is not gated at all — there is nothing to write down.
disabled turns the comparison off, not the lookup: your classification
policy is read before the mode is known, so if that read fails the call is
refused with INTERNAL_ERROR in every mode. A failure to read the endpoint's
own rating is treated differently — it is fatal only under enforce, and in the
other modes the endpoint is treated as unrated.