Your First Source

Create a data source that ingests earthquake data and displays it on the globe

This guide walks you through creating a data source from scratch. By the end, you will have real-time earthquake data from the European-Mediterranean Seismological Centre (EMSC) displayed on the Respondent globe.

What you will build

A source definition that:

  • Polls a public earthquake API every 2 minutes
  • Parses GeoJSON features into entities
  • Filters out minor earthquakes (magnitude < 2.0)
  • Maps each earthquake to a positioned entity on the globe
  • Displays earthquakes as red ripple icons with magnitude, depth, and location details

Step 1: Create the source file

Create a new file at sources.d/my_earthquakes.yaml with the source identity:

schema_version: 2

name: my_earthquakes
source_type: my_earthquakes
layer_type: earthquakes
display_name: "My Earthquakes"
What these fields mean
  • schema_version – always use 2 for new sources. Version 2 enables filtering, caching, and advanced parser options.
  • name – unique identifier for this source. Lowercase and underscores only.
  • source_type – maps to the internal source registry. By convention, use the same value as name.
  • layer_type – groups entities on the globe. Multiple sources can feed the same layer (e.g., both USGS and EMSC earthquakes share the earthquakes layer).
  • display_name – human-readable name for the SOURCE (used in source listings, logs, and AI prompts). It does not set the layer-picker label; that label derives from layer_type (title-cased, e.g. earthquakes -> Earthquakes).
  • layer_display_name (optional) – overrides the title-cased layer_type as the layer label shown in the UI layers panel. Set it when the title-cased layer_type loses meaning, e.g. "Traffic Stations (Mexico)".

Step 2: Configure the transport

Add the HTTP polling transport to fetch earthquake data from the EMSC Seismic Portal API:

transport:
  type: http_poll
  url: "https://www.seismicportal.eu/fdsnws/event/1/query?format=json&limit=100&orderby=time"
  method: GET
  headers:
    Accept: "application/json"
  timeout: "10s"
  interval: "120s"
  max_response_bytes: 52428800
  retry:
    max_attempts: 3
    backoff: "exponential"
    initial_delay: "1s"
    max_delay: "30s"

This tells Respondent to make a GET request to the EMSC API every 120 seconds (2 minutes), with a 10-second timeout and automatic retries on failure.

Step 3: Configure the parser

The EMSC API returns GeoJSON, which wraps records in a features array. Add the parser:

parser:
  format: geojson
  records_path: "features"
  max_records: 10000
Tip
For GeoJSON APIs, the records_path is almost always "features". Each feature has a properties object (metadata), a geometry object (coordinates), and an id field.

Step 4: Add a filter

Filter out small earthquakes to keep the display focused on significant events. Add a CEL (Common Expression Language) filter:

filter: >
  has(record.properties.mag) && double(record.properties.mag) >= 2.0
Expression has(record.properties.mag) && double(record.properties.mag) >= 2.0
Input record.properties.mag = 3.5
Output true
The has() function checks that the magnitude field exists before accessing it. double() converts the value to a floating-point number for comparison. Records where the magnitude is missing or below 2.0 are discarded.

Step 5: Map the entity

Define how each earthquake record becomes an entity in Respondent. Entity mappings use CEL expressions that receive each parsed record as the record variable:

entity:
  external_id: >
    record.id
  name: >
    has(record.properties.flynn_region) ? record.properties.flynn_region : record.id
  metadata:
    magnitude: >
      string(record.properties.mag)
    depth: >
      string(record.properties.depth)
    place: >
      has(record.properties.flynn_region) ? record.properties.flynn_region : "Unknown"
    source: >
      "EMSC"
Metadata values must be strings
All metadata values must be CEL expressions that return strings. Use string() to convert numeric fields. These values appear in the entity detail panel.

Step 6: Map the observation

Map each record to a spatial observation – the position, time, and per-observation metadata:

observation:
  latitude: >
    record.geometry.coordinates[1]
  longitude: >
    record.geometry.coordinates[0]
  altitude: >
    record.geometry.coordinates[2] * -1000.0
  timestamp: >
    timestamp(record.properties.time)
  velocity: {}
  metadata:
    magnitude: >
      string(record.properties.mag)
  content_hash: ""
Expression record.geometry.coordinates[1]
Input record.geometry.coordinates = [-122.5, 37.8, 10.0]
Output 37.8
GeoJSON stores coordinates as [longitude, latitude, depth]. Index [0] is longitude, [1] is latitude, and [2] is depth in kilometers.
Altitude and depth
The altitude field converts depth (km below surface) to altitude (meters). Multiplying by -1000.0 converts positive depth-in-km to negative altitude-in-meters, placing the entity below the surface on the globe.

Step 7: Set the recording mode

Configure how observations are persisted:

recording:
  mode: upsert

cache:
  ttl: "3600s"
  • upsert – each poll updates the existing observation for an entity rather than appending a new row. This keeps one record per earthquake with the latest data.
  • cache.ttl – entities remain visible on the globe for 3600 seconds (1 hour) after the last successful fetch. Set this higher than the polling interval to survive missed polls.

Step 8: Configure the display

Define how earthquakes appear on the globe and in the entity detail panel:

display:
  icon:
    shape: ripple
    rotatable: false
    interpolation: false
    scale: 1.0
  trail:
    color: "#ff006e"
    width: 1.5
    opacity: 0.7
  style:
    color: "#ff006e"
    point_size: 6
  field_renderers:
    - keys: [magnitude, mag]
      label: "MAGNITUDE"
      format:
        type: float
        precision: 1
        prefix: "M"
      priority: 0
    - keys: [depth, depth_km]
      label: "DEPTH"
      format:
        type: float
        precision: 1
        suffix: " km"
      priority: 1
    - keys: [place]
      label: "LOCATION"
      format:
        type: string
      priority: 2
  • shape: ripple – an expanding ring icon, fitting for seismic events.
  • field_renderers – define how metadata fields are formatted in the entity detail panel. The keys array matches against both entity and observation metadata. A magnitude of 4.2 renders as M4.2. Priority controls display order (lower = first).

Step 9: Restart Respondent

Apply the new source by restarting the container:

docker compose restart

Check the logs to confirm the source loaded successfully:

docker compose logs -f

Look for log lines mentioning my_earthquakes – you should see the source being loaded and the first poll being scheduled.

Step 10: See earthquakes on the globe

Open http://localhost:8090 in your browser. After the first polling interval (up to 2 minutes), red ripple icons will appear on the globe at earthquake locations. Click any earthquake to see the detail panel with magnitude, depth, and location fields.

Complete source file

Here is the full sources.d/my_earthquakes.yaml for reference:

schema_version: 2

name: my_earthquakes
source_type: my_earthquakes
layer_type: earthquakes
display_name: "My Earthquakes"

transport:
  type: http_poll
  url: "https://www.seismicportal.eu/fdsnws/event/1/query?format=json&limit=100&orderby=time"
  method: GET
  headers:
    Accept: "application/json"
  timeout: "10s"
  interval: "120s"
  max_response_bytes: 52428800
  retry:
    max_attempts: 3
    backoff: "exponential"
    initial_delay: "1s"
    max_delay: "30s"

parser:
  format: geojson
  records_path: "features"
  max_records: 10000

filter: >
  has(record.properties.mag) && double(record.properties.mag) >= 2.0

entity:
  external_id: >
    record.id
  name: >
    has(record.properties.flynn_region) ? record.properties.flynn_region : record.id
  metadata:
    magnitude: >
      string(record.properties.mag)
    depth: >
      string(record.properties.depth)
    place: >
      has(record.properties.flynn_region) ? record.properties.flynn_region : "Unknown"
    source: >
      "EMSC"

observation:
  latitude: >
    record.geometry.coordinates[1]
  longitude: >
    record.geometry.coordinates[0]
  altitude: >
    record.geometry.coordinates[2] * -1000.0
  timestamp: >
    timestamp(record.properties.time)
  velocity: {}
  metadata:
    magnitude: >
      string(record.properties.mag)
  content_hash: ""

recording:
  mode: upsert

cache:
  ttl: "3600s"

display:
  icon:
    shape: ripple
    rotatable: false
    interpolation: false
    scale: 1.0
  trail:
    color: "#ff006e"
    width: 1.5
    opacity: 0.7
  style:
    color: "#ff006e"
    point_size: 6
  field_renderers:
    - keys: [magnitude, mag]
      label: "MAGNITUDE"
      format:
        type: float
        precision: 1
        prefix: "M"
      priority: 0
    - keys: [depth, depth_km]
      label: "DEPTH"
      format:
        type: float
        precision: 1
        suffix: " km"
      priority: 1
    - keys: [place]
      label: "LOCATION"
      format:
        type: string
      priority: 2

Next steps

Now that you have a working source, explore further:

  • Add labels for categorization: labels: { category: geological, priority: medium }
  • Enable AI enrichment to generate risk assessments for each earthquake (see the Configuration guide to set up an LLM provider first)
  • Create additional sources for other data – flights, ships, weather, satellites – using the same pattern
Edit this page on GitHub