Field Mapping
Map parsed records to entities and observations using CEL expressions
After parsing, each record is a flat or nested map of fields. Field mappings use CEL (Common Expression Language) expressions to transform records into Respondent entities and observations. Every expression receives the current record as the record variable.
Entity mapping
The entity section defines how each record becomes a domain entity. Entity mappings produce the identity and metadata for each data point.
entity:
external_id: >
record.id
name: >
record.properties.name
metadata:
category: >
has(record.category) ? record.category : "unknown"
Fields
external_id
CEL -> string
requiredexternal_id every time it is fetched. This is the key used for upsert and dedupe recording modes.name
CEL -> string
requiredmetadata
map[string]CEL -> stringdisplay.field_renderers.string() to convert numeric fields: string(record.magnitude). Returning a non-string type causes a runtime error.External ID patterns
The external_id must be unique and deterministic. Choose a pattern based on your data:
record.idrecord.id = "eq-2024-abc123""eq-2024-abc123"string(record.eventid)record.eventid = 12345"12345"string().record.lat + "_" + record.lon + "_" + record.daterecord.lat = "39.7", record.lon = "-104.9", record.date = "2024-01-15""39.7_-104.9_2024-01-15"Observation mapping
The observation section maps each record to a spatial observation – a position-and-time snapshot attached to an entity.
observation:
latitude: >
double(record.lat)
longitude: >
double(record.lon)
altitude: "0.0"
timestamp: >
unix_ms(record.time)
velocity:
speed: >
has(record.speed) ? double(record.speed) : 0.0
heading: >
has(record.heading) ? double(record.heading) : 0.0
metadata:
status: >
has(record.status) ? record.status : ""
content_hash: ""
Position fields
latitude
CEL -> double
requiredentity_type is global_indicator.longitude
CEL -> double
requiredentity_type is global_indicator.altitude
CEL -> double
default: 0.0Latitude and longitude patterns
double(record.lat)record.lat = "39.7392"39.7392double() when the API returns coordinates as strings.record.geometry.coordinates[1]record.geometry.coordinates = [-104.99, 39.74, 1609.0]39.74[0] is longitude, [1] is latitude, [2] is altitude.record.position.latituderecord.position = {"latitude": 39.74, "longitude": -104.99}39.74Timestamp field
timestamp
CEL -> timestamp
requiredAvailable timestamp functions:
| Function | Input | Description |
|---|---|---|
now() | none | Current time (use as fallback only) |
unix_ms(expr) | integer | Epoch milliseconds |
unix_s(expr) | integer | Epoch seconds |
parse_rfc3339(expr) | string | RFC 3339 format: "2024-01-15T00:00:00Z" |
parse_iso8601(expr) | string | ISO 8601 format (appends Z if no timezone) |
parse_rfc2822(expr) | string | RFC 2822 format (RSS feeds) |
parse_datetime(expr, layout) | string, string | Go time layout (e.g., "2006-01-02 15:04:05") |
timestamp(expr) | string | Parse standard timestamp string |
unix_ms(record.time)record.time = 17052768000002024-01-15T00:00:00Zparse_rfc3339(record.date)record.date = "2024-01-15T12:30:00Z"2024-01-15T12:30:00Zhas(record.pubDate) ? parse_rfc2822(record.pubDate) : now()record.pubDate = "Mon, 15 Jan 2024 12:30:00 GMT"2024-01-15T12:30:00ZEvent time fields
For time-bounded events (weather alerts, conflict events), you can specify a start and end time:
event_time
CEL -> timestampevent_end
CEL -> timestampobservation:
timestamp: >
parse_rfc3339(record.updated)
event_time: >
parse_rfc3339(record.start_time)
event_end: >
parse_rfc3339(record.end_time)
Velocity fields
velocity
map[string]CEL -> numberspeed, heading, climb). Each value is a CEL expression that must evaluate to a number – it is stored as map[string]float64 via numeric evaluation. A component whose expression returns a non-numeric value (or errors) is silently dropped from the velocity map.velocity:
speed: >
has(record.speed) ? double(record.speed) : 0.0
heading: >
has(record.heading) ? double(record.heading) : 0.0
climb: >
has(record.vertical_rate) ? double(record.vertical_rate) : 0.0
display.icon.interpolation: true, the frontend interpolates entity positions between poll updates for smooth movement on the globe.Observation metadata
observation.metadata
map[string]CEL -> stringContent hash
content_hash
CEL -> stringrecording.mode is dedupe. Should produce a unique string for each distinct observation state. A new observation is only recorded when the content hash changes.# Dedupe by article URL -- only re-record if the URL changes
content_hash: >
record.link
# Dedupe by composite state -- re-record if position or brightness changes
content_hash: >
record.lat + "_" + record.lon + "_" + record.brightness
Common CEL patterns
Optional field guards
Always guard access to fields that may not exist in every record. Without has(), accessing a missing field causes a runtime error that silently skips the record.
has(record.field) ? record.field : "default"record = {}"default"Type coercion
string(record.mag)record.mag = 4.2"4.2"double(record.latitude)record.latitude = "39.7392"39.7392int(record.count)record.count = "42"42Safe numeric conversion
coerce_double(record.value, 0.0)record.value = "not_a_number"0.0Lookup tables
When you have static reference data (country codes, station metadata), define a lookup table and query it in CEL expressions:
lookup_tables:
- name: country_codes
key_field: code
entries:
- code: US
name: "United States"
- code: GB
name: "United Kingdom"
entity:
metadata:
country_name: >
lookup("country_codes", record.country, "name")
lookup("country_codes", record.country, "name")record.country = "US""United States"dyn (the entry value’s native type), or a CEL error if the table, key, or field is not found.The lookup family also includes a presence check and a default-aware variant:
| Function | Returns | Description |
|---|---|---|
lookup(table, key, field) | dyn | Retrieve a field from a lookup table entry. Errors if the table, key, or field is missing. |
has_lookup(table, key) | bool | Report whether key exists in the named lookup table. Returns false if the table is unknown. |
lookup_or(table, key, field, default) | dyn | Like lookup, but returns default instead of erroring when the table, key, or field is missing. |
has_lookup("country_codes", record.country)record.country = "US"truelookup when a key may be absent.lookup_or("country_codes", record.country, "name", "unknown")record.country = "ZZ""unknown"TLE orbital functions
For satellite TLE data, use SGP4 propagation functions. Each propagates the TLE to the current time. All four return 0.0 on a malformed or invalid TLE.
| CEL Function | Returns | Description |
|---|---|---|
sgp4_lat(line1, line2) | double | Current latitude in degrees |
sgp4_lon(line1, line2) | double | Current longitude in degrees |
sgp4_alt_m(line1, line2) | double | Current altitude in meters |
sgp4_vel_mps(line1, line2) | double | Current velocity in meters per second |
sgp4_lat(record.line1, record.line2)record.line1 = "1 25544U ..."42.35