Cedar GIS Pipeline Agent
An LLM-driven agent that onboards and refreshes GIS data for Cedar's land-siting platform. It took coverage from 2 cities to 25 cities across new states in a couple of months, and turned about a week of manual data capture into a weekly automated run.
- Role: Lead GIS Software Engineer, Cedar. I designed and built it.
- Used by: me, Cedar's data team, and the analyst team. Customers see the results as fresh data on the platform.
- Result: about a week of manual work per update → 5–40 minutes on a weekly schedule, or about 5 minutes to add a new source on demand.
- Coverage: 2 cities → 25 cities across new states, in a couple of months.
The problem
Cedar helps people evaluate land and figure out what they can build. That depends on GIS data from cities and counties: parcels, zoning, and the other layers a siting analysis needs. Every jurisdiction publishes that data differently, with different field names, data types, and formats.
Adding or refreshing a city meant capturing that data by hand: finding the right layers, mapping fields to our schema, fixing types so values displayed correctly, and checking what had changed. It took about a week each time, and it capped how fast we could grow.
What I built
I wrote a YAML and markdown spec that tells an LLM agent how Cedar's data pipeline should work. It defines:
- The critical layers a land-siting analysis needs, so given a new GIS endpoint URL the agent knows exactly which layers to pull.
- Field names, mapped from each source onto Cedar's schema.
- Data types, so values are stored correctly and display correctly on the platform.
- Display rules, such as never truncating long names.
The agent reads the spec, pulls the right layers from the endpoint, maps them, validates them, and loads only what changed. It runs weekly on GitHub Actions, or on demand when a new source comes in.
Illustrative spec (simplified example, not Cedar's actual spec):
layers:
parcels:
required: true
fields:
parcel_id: { source_aliases: [APN, PARCEL_NO, PIN], type: string }
owner_name: { source_aliases: [OWNER, OWN_NAME], type: string, truncate: false }
acreage: { source_aliases: [ACRES, GIS_ACRES], type: float }
zoning:
required: true
fields:
zone_code: { source_aliases: [ZONE, ZONING], type: string }
zone_name: { source_aliases: [ZONE_DESC], type: string, truncate: false }
change_detection:
key: parcel_id
mode: cdc
on_failure:
action: backfill
Guardrails and testing
Making it reliable mattered more than getting it to work once:
- Change data capture. Only records that changed get updated, so weekly runs are fast and history stays clean.
- Type checks. Values are stored with the right types so they look right on the platform.
- No truncated names. Tests confirm long field values are stored and displayed in full.
- Backfill on failure. If values fail validation or a run breaks, the pipeline backfills and reruns instead of leaving gaps.
Results
- About a week of manual data capture per update → 5–40 minutes on a weekly automated run, depending on how much new data comes in.
- About 5 minutes to add a new source on demand.
- Coverage went from 2 cities to 25 cities across new states in a couple of months.
- The data team and analysts stopped spending their time on data capture, and customers got fresher data.
Why it carries over
The spec is the reusable part. The rules about what good data looks like live in one readable file that the agent and the team both use, so adding a new source is mostly configuration, not new code. The same pattern works for any team that keeps pulling messy data from many sources into one system.
This was built for Cedar and runs on internal systems, so there's no public demo. I'm happy to walk through it live.