Field Service Tracking Guide
Build an end-to-end field service tracking system with SpatialFlow. This guide walks through creating service area geofences for job sites, dispatching technicians with automated arrival/departure tracking, triggering job status updates, monitoring time-on-site, and receiving completion notifications via webhooks.
Use Case
Manage field technicians visiting customer locations:
- Create service area geofences for job sites and customer locations
- Dispatch technicians with automated arrival/departure tracking
- Trigger job status updates when technicians arrive at or leave job sites
- Monitor technician movements and calculate time-on-site
- Receive automated completion notifications via webhooks
Architecture Overview
Dispatch System --> Assign Job --> SpatialFlow API
|
Create Job Site Geofence
|
Technician App --> Location Updates --> Geofence Engine
|
Workflow Triggers
|
Webhook --> Dispatch Backend
|
Job Status Dashboard
Step 1: Create Service Area Geofences
Create geofences around each job site so SpatialFlow can detect when technicians arrive and depart.
Job Site Geofence
Create a circular service area (polygon approximating a 200m radius) around a customer address:
curl -X POST https://api.spatialflow.io/api/v1/geofences \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Job Site - 425 Park Ave HVAC Repair",
"description": "Service area for HVAC repair at 425 Park Avenue",
"geometry": {
"type": "Polygon",
"coordinates": [[
[-73.9717, 40.7580],
[-73.9700, 40.7590],
[-73.9680, 40.7590],
[-73.9663, 40.7580],
[-73.9663, 40.7565],
[-73.9680, 40.7555],
[-73.9700, 40.7555],
[-73.9717, 40.7565],
[-73.9717, 40.7580]
]]
},
"metadata": {
"job_id": "JOB-2025-1847",
"customer_name": "Meridian Office Group",
"service_type": "hvac_repair",
"priority": "high",
"scheduled_time": "2025-11-15T09:00:00Z"
}
}'
Response:
{
"id": "gf_job_park_ave_1847",
"name": "Job Site - 425 Park Ave HVAC Repair",
"geometry": { "type": "Polygon", "coordinates": [[ ... ]] },
"metadata": {
"job_id": "JOB-2025-1847",
"customer_name": "Meridian Office Group",
"service_type": "hvac_repair",
"priority": "high",
"scheduled_time": "2025-11-15T09:00:00Z"
},
"is_active": true,
"created_at": "2025-11-14T16:30:00Z"
}
Regional Service Territory
Create a larger geofence for a regional coverage area to track which territory a technician is operating in:
curl -X POST https://api.spatialflow.io/api/v1/geofences \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Service Territory - Manhattan Midtown",
"description": "Regional service coverage for Midtown Manhattan",
"geometry": {
"type": "Polygon",
"coordinates": [[
[-73.9950, 40.7480],
[-73.9950, 40.7650],
[-73.9680, 40.7650],
[-73.9680, 40.7480],
[-73.9950, 40.7480]
]]
},
"metadata": {
"territory_code": "MAN-MID",
"region": "manhattan",
"assigned_team": "team_alpha",
"max_technicians": 12
}
}'
Response:
{
"id": "gf_territory_man_mid",
"name": "Service Territory - Manhattan Midtown",
"geometry": { "type": "Polygon", "coordinates": [[ ... ]] },
"metadata": {
"territory_code": "MAN-MID",
"region": "manhattan",
"assigned_team": "team_alpha",
"max_technicians": 12
},
"is_active": true,
"created_at": "2025-11-14T16:35:00Z"
}
Step 2: Set Up Dispatch Workflows
Create workflows to automate job tracking when technicians arrive at and depart from job sites.
Technician Arrival Workflow (Entry)
curl -X POST https://api.spatialflow.io/api/v1/workflows \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Technician Arrival - Park Ave Job Site",
"description": "Log technician arrival and update job status",
"trigger": {
"type": "geofence_entry",
"geofence_id": "gf_job_park_ave_1847"
},
"actions": [
{
"type": "webhook",
"config": {
"url": "https://your-backend.com/api/technician-arrival",
"method": "POST",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_BACKEND_API_KEY"
},
"body": {
"technician_id": "{{device.id}}",
"technician_name": "{{device.name}}",
"job_site_id": "{{geofence.id}}",
"job_id": "{{geofence.metadata.job_id}}",
"customer_name": "{{geofence.metadata.customer_name}}",
"arrival_time": "{{event.timestamp}}",
"location": {
"lat": "{{location.latitude}}",
"lng": "{{location.longitude}}"
}
}
}
},
{
"type": "slack",
"config": {
"webhook_url": "https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK",
"message": "Technician {{device.name}} arrived at {{geofence.name}} for job {{geofence.metadata.job_id}}",
"channel": "#dispatch"
}
}
]
}'
Technician Departure Workflow (Exit)
curl -X POST https://api.spatialflow.io/api/v1/workflows \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Technician Departure - Park Ave Job Site",
"description": "Log departure and calculate time-on-site",
"trigger": {
"type": "geofence_exit",
"geofence_id": "gf_job_park_ave_1847"
},
"actions": [
{
"type": "webhook",
"config": {
"url": "https://your-backend.com/api/technician-departure",
"method": "POST",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_BACKEND_API_KEY"
},
"body": {
"technician_id": "{{device.id}}",
"job_site_id": "{{geofence.id}}",
"job_id": "{{geofence.metadata.job_id}}",
"departure_time": "{{event.timestamp}}",
"service_type": "{{geofence.metadata.service_type}}"
}
}
},
{
"type": "slack",
"config": {
"webhook_url": "https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK",
"message": "Technician {{device.name}} departed from {{geofence.name}} - job {{geofence.metadata.job_id}} visit complete",
"channel": "#dispatch"
}
}
]
}'
Step 3: Register Technicians as Devices
Register each field technician as a device in SpatialFlow. The technician's mobile app will send location updates under this device identity.
curl -X POST https://api.spatialflow.io/api/v1/devices \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"device_id": "tech_201",
"name": "Sarah Johnson - HVAC Specialist",
"metadata": {
"employee_id": "EMP-4521",
"specialization": "hvac",
"vehicle_plate": "NY-TRK-8842",
"phone": "+1-555-0147",
"certification_level": "senior"
}
}'
Response:
{
"id": "d_tech_201_uuid",
"device_id": "tech_201",
"name": "Sarah Johnson - HVAC Specialist",
"metadata": {
"employee_id": "EMP-4521",
"specialization": "hvac",
"vehicle_plate": "NY-TRK-8842",
"phone": "+1-555-0147",
"certification_level": "senior"
},
"is_active": true,
"created_at": "2025-11-14T17:00:00Z"
}
Step 4: Send Location Updates
Send location updates from the technician's mobile app to SpatialFlow.
Single Location Update
Use the device UUID (id) returned from the registration response:
curl -X POST https://api.spatialflow.io/api/v1/devices/{device_uuid}/location \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"latitude": 40.7580,
"longitude": -73.9690,
"accuracy": 8,
"timestamp": "2025-11-15T09:12:00Z",
"metadata": {
"speed_kmh": 0,
"heading": 0,
"battery_level": 82
}
}'
Mobile App Integration (React Native Example)
Integrate location tracking into your technician app with 30-second intervals for active job tracking:
import * as Location from 'expo-location';
// Request location permissions
const { status } = await Location.requestForegroundPermissionsAsync();
// Start tracking with 30-second intervals during active jobs
Location.watchPositionAsync(
{
accuracy: Location.Accuracy.High,
timeInterval: 30000, // Every 30 seconds
distanceInterval: 20, // Every 20 meters
},
async (location) => {
await fetch('https://api.spatialflow.io/api/v1/devices/{device_uuid}/location', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
latitude: location.coords.latitude,
longitude: location.coords.longitude,
accuracy: location.coords.accuracy,
timestamp: new Date(location.timestamp).toISOString(),
metadata: {
speed_kmh: location.coords.speed * 3.6,
heading: location.coords.heading,
battery_level: await getBatteryLevel(),
},
}),
});
}
);
Step 5: Handle Webhook Events
Build backend endpoints to process technician arrival and departure events from SpatialFlow.
Express (Node.js) Backend
const express = require('express');
const app = express();
app.use(express.json());
// Technician arrival endpoint
app.post('/api/technician-arrival', async (req, res) => {
const {
technician_id,
technician_name,
job_site_id,
job_id,
customer_name,
arrival_time,
location,
} = req.body;
// Log arrival to database
await db.jobVisits.insert({
technician_id,
job_id,
job_site_id,
arrival_time: new Date(arrival_time),
latitude: location.lat,
longitude: location.lng,
status: 'on_site',
});
// Update job status to in_progress
await db.jobs.update(job_id, {
status: 'in_progress',
assigned_technician: technician_id,
actual_arrival: new Date(arrival_time),
});
// Calculate ETA vs actual arrival
const job = await db.jobs.findById(job_id);
const scheduledTime = new Date(job.scheduled_time);
const actualTime = new Date(arrival_time);
const delayMinutes = (actualTime - scheduledTime) / 60000;
if (delayMinutes > 15) {
console.log(`WARNING: ${technician_name} arrived ${delayMinutes.toFixed(0)}min late for ${job_id}`);
await notifyManager(job_id, delayMinutes);
}
console.log(`Technician ${technician_name} arrived at ${customer_name} for ${job_id}`);
res.json({ success: true, job_id, status: 'in_progress' });
});
// Technician departure endpoint
app.post('/api/technician-departure', async (req, res) => {
const {
technician_id,
job_site_id,
job_id,
departure_time,
service_type,
} = req.body;
// Calculate time-on-site
const visit = await db.jobVisits.findLast({
technician_id,
job_id,
status: 'on_site',
});
const timeOnSiteMinutes = (new Date(departure_time) - new Date(visit.arrival_time)) / 60000;
// Update visit record
await db.jobVisits.update(visit.id, {
departure_time: new Date(departure_time),
time_on_site_minutes: timeOnSiteMinutes,
status: 'completed',
});
// Update job to completed
await db.jobs.update(job_id, {
status: 'completed',
completed_at: new Date(departure_time),
time_on_site_minutes: timeOnSiteMinutes,
});
// Trigger invoice generation
await invoiceService.generate({
job_id,
service_type,
technician_id,
time_on_site_minutes: timeOnSiteMinutes,
});
console.log(`Job ${job_id} completed - ${timeOnSiteMinutes.toFixed(0)} minutes on site`);
res.json({
success: true,
job_id,
time_on_site_minutes: timeOnSiteMinutes,
status: 'completed',
});
});
app.listen(3000, () => console.log('Dispatch backend listening on port 3000'));
Best Practices
1. Service Area Sizing
Size job site geofences to account for GPS drift near buildings:
- Residential: 50-80 meter radius (single-family homes, smaller yards)
- Commercial: 80-150 meter radius (office buildings with parking lots)
- Industrial: 150-300 meter radius (large facilities, campuses)
GPS accuracy degrades near tall buildings. Add a 50-100 meter buffer beyond the actual property boundary to avoid missed triggers.
2. Multi-Stop Routes
For technicians with multiple jobs per day:
- Create geofences for each job site before the technician starts their route
- Use metadata to track the planned stop sequence
- Monitor arrival order against the planned sequence to detect route deviations
- Delete or deactivate geofences after job completion to reduce noise
3. SLA Monitoring
Compare actual arrival times to scheduled times:
const slaCompliance = (scheduledTime, actualArrival) => {
const delayMinutes = (actualArrival - scheduledTime) / 60000;
if (delayMinutes <= 0) return { status: 'early', delay: 0 };
if (delayMinutes <= 15) return { status: 'on_time', delay: delayMinutes };
if (delayMinutes <= 30) return { status: 'late', delay: delayMinutes };
return { status: 'sla_breach', delay: delayMinutes };
};
4. Offline Handling for Rural Areas
Technicians may lose connectivity in rural or underground locations:
const locationQueue = [];
async function sendLocation(location) {
try {
await fetch(API_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(location),
});
} catch (error) {
// Queue for later when back online
locationQueue.push({ ...location, queued_at: new Date().toISOString() });
}
}
// Flush queue when connectivity restores
async function flushQueue() {
while (locationQueue.length > 0) {
const location = locationQueue.shift();
try {
await fetch(API_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(location),
});
} catch {
locationQueue.unshift(location);
break; // Still offline, stop retrying
}
}
}
Monitoring and Reporting
List Active Technicians
curl -X GET https://api.spatialflow.io/api/v1/devices \
-H "Authorization: Bearer YOUR_API_KEY"
Check Workflow Executions
curl -X GET https://api.spatialflow.io/api/v1/workflows/{workflow_id}/executions?limit=50 \
-H "Authorization: Bearer YOUR_API_KEY"
Dashboard KPIs
Track these field service metrics:
- Average response time: Time from job dispatch to technician arrival
- Time-on-site: Average duration per service type (HVAC, plumbing, electrical)
- Jobs per day: Technician throughput and utilization
- SLA compliance: Percentage of jobs where technician arrived within scheduled window
- First-time fix rate: Combine with your job management system to track repeat visits
Troubleshooting
Technician at Job Site but No Trigger
Causes:
- GPS accuracy is poor (common near tall buildings in urban areas)
- Geofence is too small for the location's GPS conditions
- Geofence or workflow is inactive
Solutions:
- Increase geofence radius by 50-100 meters
- Check GPS accuracy in the location update metadata (filter updates with accuracy > 50m)
- Verify
is_active: trueon both the geofence and workflow
Test with point-in-polygon:
curl -X POST https://api.spatialflow.io/api/v1/geofences/test-point \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"geometry": {
"type": "Point",
"coordinates": [-73.9690, 40.7580]
}
}'
Multiple Triggers at Same Location
Cause: GPS signal bounces across geofence boundary, triggering repeated entry/exit events.
Solutions:
- Use dwell time filtering: configure workflows to require 30+ seconds inside the geofence before triggering
- Increase geofence size so the boundary is farther from where the technician is working
- Implement deduplication in your webhook handler (ignore arrival events within 5 minutes of the last one for the same job)
Delayed Notifications
Causes:
- Webhook endpoint is slow to respond (should be < 5 seconds)
- Technician's phone has limited network connectivity
- Workflow execution queue is backed up
Solutions:
- Ensure webhook endpoints return 200 quickly and process work asynchronously
- Check SpatialFlow workflow execution logs for delivery failures
- Verify the technician's device is sending location updates at the expected frequency
Next Steps
- Geofences - Learn about geofence types, properties, and spatial queries
- Workflows - Build complex multi-step automations with conditions
- Devices - Advanced device management and bulk operations
- Webhook Integration - Build robust webhook receivers with retries
- Error Handling - Handle failures and edge cases gracefully
- Signals - Detect anomalies in technician routes and behavior patterns