Skip to main content

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:

  1. GPS accuracy is poor (common near tall buildings in urban areas)
  2. Geofence is too small for the location's GPS conditions
  3. 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: true on 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