Workforce for ArcGIS – scheduling recurring facility inspections

April 8, 2020 Chaim Schwartz

Workforce for ArcGIS is a great app for dispatching field work and monitoring status of staff and assignments in real time. In this article I’ll show how with a bit of customization, you can have your assignments auto-scheduled on a recurring interval.

Workforce for ArcGIS allows users to (1) create assignments in the office (2) dispatch assignments to field staff and (3) complete assignments in the field. To support these, Workforce offers a user interface that is designed to provide maximum feedback of job status and user locations in real time. Workforce also allows assignments to be integrated with other ArcGIS apps such as Collector and Survey123, acting as a launch pad to perform a wide range of field work.

In this article I’ll show how with a bit of customization, you can have your Workforce assignments auto-recur at a set frequency which you can easily control. In the scenario that I will use, a supervisor on a municipal facilities team is responsible for completing several recurring inspections on facilities owned by the municipality:

Inspection type Interval
Facility inspection 6 months
HVAC Inspection 12 months (Spring)
Furnace Inspection 12 months (Fall)
Safety Inspection 3 months

While these can be manually created on an as-needed basis, being able to automate the creation of these recurring assignments will improve the efficiency and reliability of operations. Below are the steps required to do just that.

Step 1 – Create a Workforce Project

Create your Workforce Project as you would for any other Workforce project and configure it as needed such as by adding your buildings layer to the dispatcher’s web map.

Step 2 – Create Assignment Types

Next, create the assignment types following the usual process through the “Assignment Types” tab on the Workforce Configuration page. If you plan to have both auto-scheduled and manually created assignments in your Workforce project it’s s a good idea to have designated assignment types and descriptions for each, to make it easier to identify and manage them. 

Step 3 – Create Assignments

Create the first set of assignments for each facility. The due date for each assignment would be the due date for the upcoming inspection. 

Step 4 – Create fields to control the frequency

On the “assignments” layer that was created as part of our Workforce project (read more about the Workforce data model here) we want to add two new fields. The first field, which we will call “cycle” will be used to flag assignments that need to be auto-scheduled on a recurring basis. The second field which we will call “cycleFrequency” will contain the frequency, in days. 

Step 5 – Define frequency for cycle

Once these two fields are added, we will populate the “cycle” field with the number 1 – which indicates that these are to be cycled – and the “cycleFrequency” field with the frequency, in days.

Step 6 – The Script

Lastly, we will use a Python script to run on a server in the background, to reschedule these recurring assignments as they are completed in the field. Luckily for us, we don’t need to reinvent this script, as we can pretty much repurpose the “Create Workforce assignments” script already provided with the Citizen Problem Reporter ArcGIS Solution. This script along with the detailed documentation on how to configure the script and run it on a scheduled basis using Windows Task Scheduler, was designed to generate Workforce assignments based off incoming public requests from the Citizen Problem Reporter template. Since essentially this means creating Workforce Assignments from an online feature layer, we can reuse that script to create Workforce assignments from...the exact same Workforce assignments layer.

You can follow the documentation provided at the above link to configure the basic parameters within the script. Here are a few specific recommendations to support the creation of recurring assignments:

  • ‘source url’ and ‘target url’: We will be referencing the same Workforce assignments layer for both.
  • ‘query’: We can set the following SQL expression: 'status=3 AND cycle=1'. This will identify any completed Workforce assignments (status = 3) which are flagged as assignments that should be cycled (cycle=1).
  • ‘fields’: these pairs of fields define which information will be copied over from the completed assignment to the assignment that will be created. Since we want to make sure the assignment keeps cycling, we will pass the assignmentType, cycle, cycleFrequency and location fields at the minimum.
  • ‘update field’: will be the “cycle” field
  • ‘update value’: will be 2. This way any assignments to be cycled are flagged by the number 1, and number 2 indicates these have been cycled already.

Lastly, at the stage in the script where the Workforce assignments are created, we want to make sure that the new assignment calculates the due date based off the completion date of the current assignment. This is done by first calculating a “nextDueDate” parameter and then passing that as the due date for the new assignment:

nextDueDate = row.attributes['completedDate']+(row.attributes['cycleFrequency']*3600000)                    
attributes = {'status': 0,
'priority': 0,
'dueDate': nextDueDate}

The script below provides an example of how such a final script would look like, following the above recommendations. Using this approach, you can set your recurring assignments and preferred frequencies directly through the feature layer, and as long as the “cycle” and “cycleFrequency” are populated – the script will pick them up and reschedule them upon completion.

# ------------------------------------------------------------------------------
# Name:        create_workforce_assignments.py
# Purpose:     generates identifiers for features

# Copyright 2017 Esri

#   Licensed under the Apache License, Version 2.0 (the "License");
#   you may not use this file except in compliance with the License.
#   You may obtain a copy of the License at
#
#       http://www.apache.org/licenses/LICENSE-2.0
#
#   Unless required by applicable law or agreed to in writing, software
#   distributed under the License is distributed on an "AS IS" BASIS,
#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#   See the License for the specific language governing permissions and
#   limitations under the License.

# ------------------------------------------------------------------------------

from datetime import datetime as dt
from os import path, sys
from arcgis.gis import GIS
from arcgis.features import FeatureLayer

orgURL = 'https://XXXXX.maps.arcgis.com/'     # URL to ArcGIS Online organization or ArcGIS Portal
username = 'XXXXX'   # Username of an account in the org/portal that can access and edit all services listed below
password = 'XXXXX'   # Password corresponding to the username provided above

services = [{'source url': 'https://services9.arcgis.com/Yu2UzT0Yprgwp7dx/arcgis/rest/services/assignments_839f4e6983a148fd9307b0a48053fe6b/FeatureServer/0',
             'target url': 'https://services9.arcgis.com/Yu2UzT0Yprgwp7dx/arcgis/rest/services/assignments_839f4e6983a148fd9307b0a48053fe6b/FeatureServer/0',
             'query': 'status=3 AND cycle=1',
             'fields': {
                 'assignmentType': 'assignmentType',
				 'cycleFrequency':'cycleFrequency',
				 'cycle':'cycle',
                 'location':'location'},
             'update field': 'cycle',
             'update value': '2'
             }]

def main():
    # Create log file
    with open(path.join(sys.path[0], 'attr_log.log'), 'a') as log:
        log.write('\n{}\n'.format(dt.now()))

        # connect to org/portal
        gis = GIS(orgURL, username, password)

        for service in services:
            try:
                # Connect to source and target layers
                fl_source = FeatureLayer(service['source url'], gis)
                fl_target = FeatureLayer(service['target url'], gis)

                # get field map
                fields = [[key, service['fields'][key]] for key in service['fields'].keys()]

                # Get source rows to copy
                rows = fl_source.query(service['query'])
                adds = []
                updates = []

                for row in rows:
                    # Build dictionary of attributes & geometry in schema of target layer
                    # Default status and priority values can be overwritten if those fields are mapped to reporter layer
                    nextDueDate = row.attributes['completedDate']+(row.attributes['cycleFrequency']*86400000)                    
                    attributes = {'status': 0,
                                  'priority': 0,
                                  'dueDate': nextDueDate}

                    for field in fields:
                        attributes[field[1]] = row.attributes[field[0]]

                    new_request = {'attributes': attributes,
                                   'geometry': {'x': row.geometry['x'],
                                                'y': row.geometry['y']}}
                    adds.append(new_request)

                    # update row to indicate record has been copied
                    if service['update field']:
                        row.attributes[service['update field']] = service['update value']
                        updates.append(row)

                # add records to target layer
                if adds:
                    add_result = fl_target.edit_features(adds=adds)
                    for result in add_result['updateResults']:
                        if not result['success']:
                            raise Exception('error {}: {}'.format(result['error']['code'],
                                                                  result['error']['description']))

                # update records:
                if updates:
                    update_result = fl_source.edit_features(updates=updates)
                    for result in update_result['updateResults']:
                        if not result['success']:
                            raise Exception('error {}: {}'.format(result['error']['code'],
                                                                  result['error']['description']))

            except Exception as ex:
                msg = 'Failed to copy feature from layer {}'.format(service['url'])
                print(ex)
                print(msg)
                log.write('{}\n{}\n'.format(msg, ex))

if __name__ == '__main__':
    main()

About the Author

Chaim Schwartz

Chaim Schwartz is a Technical Solutions Specialist (TSS) with the public works team at Esri Canada. Chaim’s efforts are focused on helping customers leverage the ArcGIS and Cityworks platforms for infrastructure management, specifically asset inventory management, work management, field mobility and asset life cycle analysis. In his role as a TSS, Chaim works on introducing new technologies and workflows for improved asset management. Passionate about everything GIS and driven by his experience with municipal infrastructure management, Chaim is motivated by customers being able to derive value from geospatial technology.

More Content by Chaim Schwartz
Previous Article
Supporting public works staff and work activities during the COVID-19 pandemic
Supporting public works staff and work activities during the COVID-19 pandemic

This article reviews methods and tools that public works can use to protect staff and ensure business conti...

Next Video
Analyzing Main Breaks in the Region of Peel to Ensure Safe Reliable Water
Analyzing Main Breaks in the Region of Peel to Ensure Safe Reliable Water

This recording demonstrates how the Region of Peel in Ontario performed analysis on some of its asset manag...

Have a comment or question?

Contact Us