Extract incident tickets and CMDB assets onto the map via the DLT sidecar, write back create / update / close directly to the Table REST API, and poll on a timer for live updates. The C# client talks to ServiceNow directly — no Python middleware process, no new port allocation, no install-hint surface for the write path. Reads land as standard layers; writes never touch the read-only DuckDB hub.
The ServiceNow poller composes a DLT REST-API profile pointing at <instance>/api/now/table and re-runs the pipeline on a timer — one storage contract (the staging DuckDB the hub already ATTACHes read-only). Write-back is a focused C# HTTP client that posts straight to ServiceNow, so the hub never sees a write.
RestApi source, paginated (sysparm_limit, default page 10 000), incremental via sys_updated_on cursor → staging DuckDB → FeatureServer / MVT / OGC layers.POST /records/{profile}/{table} (create), PATCH .../{table}/{sysId} (update), POST /incidents/{profile}/{sysId}/close (sets state=6 + close notes). Direct-to-source; never through DuckDB.Task.Delay-cancelable, idempotent on profile switch. Default interval 60 s (min 5 s). Pushes state on an SSE change feed.// create an incident from the map curl -X POST http://localhost:5059/api/servicenow/records/prod/incident \ -H 'Content-Type: application/json' \ -d '{ "short_description": "Power outage - Sector 4", "urgency": "1", "location": "a1b2c3d4e5f6..." }' → OAuth2 token applied per-request (cached) → POST <instance>/api/now/table/incident → returns sys_id; hub untouched (read-only)
Most incidents carry a location reference to a cmn_location record with lat/lon. The default LocationJoin mode resolves that join client-side via DuckDB — so a ticket lights up where the asset actually is.
Default. Incident location ref → cmn_location lat/lon, joined client-side. The majority case.
Two named columns on the table mapped directly to a point.
Freeform address geocoded post-pipeline via DltAddressGeocoder → a dlt_<id>_geocoded GPKG point layer.
Attribute table only — still in the catalog, just not on the map.
| Group | Endpoint | Notes |
|---|---|---|
| Profiles | GET / POST / PUT / DELETE /api/servicenow/profiles | Instance URL + OAuth2 + tables + geometry + poll interval |
| Test | POST /api/servicenow/test/{profileId} | Probes oauth_token.do + one-row sys_user fetch |
| Sync | POST /api/servicenow/sync/{profileId} | One-shot DLT re-run (full refresh, ~60 s latency) |
| Poller | POST /api/servicenow/polling/{start,stop} | Opt-in timer; Task.Delay-cancelable |
| Events | GET /api/servicenow/events | SSE change feed: (profileId, state, rowCount, ts) |
| Write | POST / PATCH /api/servicenow/records/{profile}/{table}[/{sysId}] | Create / update direct to Table API |
| Write | POST /api/servicenow/incidents/{profile}/{sysId}/close | Sets state=6 + close notes |
Base URL http://localhost:5059. Profile store at <dataDir>/servicenow/profiles.json (plaintext non-secret + DPAPI secret envelope). Disabled by default (ServiceNow:Enabled=false) — write-capable surface, opt-in only. Documented in SERVICENOW_GUIDE.md.
servicenow.duckdb with a sys_updated_on cursor is the documented Phase-2 upgrade for sub-minute freshness. No write-back audit log in v1.Incidents and CMDB on the map, writes straight back to ServiceNow — no middleware, no new Python process.