SG-JAMF Software Usage Import: Schedule, Configuration, and Data FlowIssue <!-- /*NS Branding Styles*/ --> .ns-kb-css-body-editor-container { p { font-size: 12pt; font-family: Lato; color: #000000; } span { font-size: 12pt; font-family: Lato; color: #000000; } h2 { font-size: 24pt; font-family: Lato; color: black; } h3 { font-size: 18pt; font-family: Lato; color: black; } h4 { font-size: 14pt; font-family: Lato; color: black; } a { font-size: 12pt; font-family: Lato; color: #00718F; } a:hover { font-size: 12pt; color: #024F69; } a:target { font-size: 12pt; color: #032D42; } a:visited { font-size: 12pt; color: #00718f; } ul { font-size: 12pt; font-family: Lato; } li { font-size: 12pt; font-family: Lato; } img { display: ; max-width: ; width: ; height: ; } } SG-JAMF Software usage data load and transform process explained Release<!-- /*NS Branding Styles*/ --> .ns-kb-css-body-editor-container { p { font-size: 12pt; font-family: Lato; color: #000000; } span { font-size: 12pt; font-family: Lato; color: #000000; } h2 { font-size: 24pt; font-family: Lato; color: black; } h3 { font-size: 18pt; font-family: Lato; color: black; } h4 { font-size: 14pt; font-family: Lato; color: black; } a { font-size: 12pt; font-family: Lato; color: #00718F; } a:hover { font-size: 12pt; color: #024F69; } a:target { font-size: 12pt; color: #032D42; } a:visited { font-size: 12pt; color: #00718f; } ul { font-size: 12pt; font-family: Lato; } li { font-size: 12pt; font-family: Lato; } img { display: ; max-width: ; width: ; height: ; } } Service Graph Connector for JAMF Version 2.12.1 and above Resolution<!-- /*NS Branding Styles*/ --> .ns-kb-css-body-editor-container { p { font-size: 12pt; font-family: Lato; color: #000000; } span { font-size: 12pt; font-family: Lato; color: #000000; } h2 { font-size: 24pt; font-family: Lato; color: black; } h3 { font-size: 18pt; font-family: Lato; color: black; } h4 { font-size: 14pt; font-family: Lato; color: black; } a { font-size: 12pt; font-family: Lato; color: #00718F; } a:hover { font-size: 12pt; color: #024F69; } a:target { font-size: 12pt; color: #032D42; } a:visited { font-size: 12pt; color: #00718f; } ul { font-size: 12pt; font-family: Lato; } li { font-size: 12pt; font-family: Lato; } img { display: ; max-width: ; width: ; height: ; } } Overview: How SG-JAMF SAMP Usage Works The SG-JAMF Software Usage import collects application usage data from the Jamf environment and processes it into ServiceNow's Software Asset Management Professional (SAMP) tables. This data is used to identify software that is being used infrequently, hasn't been used recently, or isn't being used at all — enabling license reclamation decisions. The process involves three stages, multiple tables, and two types of reclamation rules. Understanding the end-to-end flow is essential for correct configuration. Stage 1: Load Data from JAMF into Staging Table Function: loadSoftwareUsages() What it does: Connects to the JAMF API, retrieves all computers via getComputerStream(), and for each computer, calls getSoftwareUsages() to fetch that computer's application usage data. The username is pulled from the computer record in JAMF (computer.username). Each application's usage data is inserted into the staging table as individual records. Key behaviors during this stage: Computer-driven iteration: The import iterates through all computers in JAMF. For each computer, it fetches software usage via the JAMF Classic API (/JSSResource). The computer's username and UDID are attached to every usage record. If a computer has no user assigned in JAMF, the username field will be empty and the record will fail user matching during processing.Reclamation rule check: Before loading any data, the script checks if ANY reclamation rule exists (either total_usage or last_used_date) via _isAnyReclamationRuleExists(). If no rules exist at all, the entire import is skipped immediately. For each individual application, it checks _isReclamationRuleExists() which returns true if a rule of EITHER type exists for that app name. Only matching apps are loaded into the staging table.Timekey from source: Each record's timekey is set by parsing the date from JAMF data via _getTimeKeyDataSource(). For example, a JAMF date of "2026/1/15" produces timekey "202601". TableNameRoleStaging Tablesn_jamf_integrate_jamf_software_usageRaw JAMF usage data with timekey, app name, username, UDID, foreground time, usage count Stage 2: Process Data (processSW) Function: processSW() This is the main processing pipeline that runs after data is loaded. It performs four operations in sequence: Step 2a — Clean up expired data: Deletes usage breakdown records older than 6 months (controlled by MONTH_COVERAGE = 6). Identifies products affected by the deletion for re-aggregation. Step 2b — Move data to breakdown table (moveDataToUsageBreakdown): Reads from the staging table, filtering by the default timekey (current month minus 1). For each record, it validates the user, CI, and checks if a "total_usage" reclamation rule exists for the application. If all conditions pass, it creates or updates a record in the SG-JAMF Software Usage Breakdown table with aggregated foreground time and usage counts. Step 2c — Aggregate breakdown to usage table (aggregateDataToUsage): Aggregates all changed records in the breakdown table by product, user, and CI. Creates or updates records in the SAMP Software Usage table (samp_sw_usage) with reclamation_type = "total_usage". This produces the final usage records used for license reclamation based on total usage time. Step 2d — Insert last used time (_insertLastUsedTime): Reads from the staging table again, filtering by the default timekey. For each record, it checks if a "last_used_date" reclamation rule exists for the application. If the rule exists, it creates or updates a record in the SAMP Software Usage table (samp_sw_usage) with reclamation_type = "last_used_date" and tracks the most recent usage date. If a record already exists and the new date is more recent, it updates the last_used_time field. Tables Involved TableNamePurposeStaging Tablesn_jamf_integrate_jamf_software_usageRaw imported data from JAMF with timekey, app name, user, UDID, usage metricsBreakdown Tablesn_jamf_sw_usage_breakdownIntermediate table with validated, aggregated usage data per app/user/CI combinationUsage Tablesamp_sw_usageFinal SAMP usage records used for reclamation decisions. Contains both total_usage and last_used_date record types.Product Processsamp_sw_product_processMaps JAMF application file names to ServiceNow software products Data Flow Diagram JAMF API │ ├── getComputerStream() → Get all computers │ │ For EACH computer: │ ├── getSoftwareUsages(computer.id) │ │ → Fetches apps used on this computer │ ├── username = computer.username (from JAMF) │ ├── udid = computer.udid │ │ │ └── For EACH app: │ Check _isReclamationRuleExists(app.name) │ If rule exists → Insert into staging table │ Set timekey from JAMF date ▼ Staging Table (sn_jamf_integrate_jamf_software_usage) │ ├── moveDataToUsageBreakdown() │ - Filters by timekey (current month - 1) │ - Checks "total_usage" reclamation rules │ - Validates user + CI ▼ Breakdown Table (sn_jamf_sw_usage_breakdown) │ ├── aggregateDataToUsage() │ - Groups by product/user/CI │ - SUMs usage time and counts ▼ Usage Table (samp_sw_usage) → reclamation_type = "total_usage" Staging Table (read again) │ ├── _insertLastUsedTime() │ - Filters by timekey (current month - 1) │ - Checks "last_used_date" reclamation rules │ - Validates user + CI ▼ Usage Table (samp_sw_usage) → reclamation_type = "last_used_date" Reclamation Rule Types The SG-JAMF software usage import uses two types of reclamation rules. Both are checked during the initial data load, but they serve different purposes during processing. TypeChecked DuringCreates Records WithWhat It TracksApplies Tototal_usageloadSoftwareUsages() AND moveDataToUsageBreakdown()reclamation_type = "total_usage"Total foreground usage time and usage count per app/user/CI per monthInstalled Softwarelast_used_dateloadSoftwareUsages() AND _insertLastUsedTime()reclamation_type = "last_used_date"Most recent date the application was used per app/user/CIInstalled Software, Subscription Software During the initial data load (loadSoftwareUsages), the script checks _isReclamationRuleExists() for each application, which returns true if EITHER a total_usage or last_used_date rule exists. This means an app only needs one rule type to be loaded into the staging table, but it needs the specific rule type to be processed in the subsequent steps. How Reclamation Rules Are Matched to Applications The script builds a lookup map of file names at initialization by: Querying samp_m2m_rule_product for reclamation rules of each typeFor each linked software product, querying samp_sw_product_process for active file name recordsBuilding a map of file_name → file_name for fast lookup during processing If no products or file names are found for a rule type, that reclamation type's lookup map is set to undefined, and all apps will be skipped for that type during processing. Import Schedule: Should Run Monthly on the 1st or 2nd for a smooth transformation Issue After running the SG-JAMF Software Usage import, no records are created in the SAMP Software Usage table or the SG-JAMF Software Usage Breakdown table. The import appears to complete successfully but data is silently skipped during processing. No error messages are generated. Cause: Timekey Mismatch The processing step (moveDataToUsageBreakdown and _insertLastUsedTime) filters the staging table by a default timekey calculated as current month minus 1. Only records whose source timekey (from JAMF date) matches this default are processed. Running the import later in the month causes a mismatch because JAMF data contains current month dates while the system expects last month's timekey. Correct Schedule SettingValueRun FrequencyMonthlyDay of Month1st or 2ndTime00:00:00 (midnight)ConditionalEnabled Example: Impact of Schedule Timing Import Run DateDefault TimekeyJAMF Data DatesSource TimekeyResultFeb 2, 2026202601Mostly January dates202601✅ Match — Records processedFeb 15, 2026202601Mix of Jan and Feb dates202601 and 202602⚠ Partial — Only Jan records processedFeb 28, 2026202601Mostly February dates202602❌ No match — Most records silently skipped How the Timekey Is Calculated Source timekey — _getTimeKeyDataSource(): Parses the date field from each JAMF usage record and extracts year + zero-padded month. For example, "2026/1/15" produces "202601". Default timekey — getTimeKeyToPull(): Takes the current system date, subtracts 1 month using GlideDateTime.addMonthsUTC(-1), then extracts the year and month. For example, running on February 12, 2026 produces "202601". Matching filter: Both moveDataToUsageBreakdown() and _insertLastUsedTime() filter the staging table with: addQuery("u_time_key", timeKeyToPull). Only records matching the default timekey are processed. Prerequisites Before Running the Import Computers must have users assigned in JAMF: The software usage import is computer-driven. The script calls getComputerStream() to iterate through all computers in JAMF, and for each computer it fetches software usage data via the JAMF API (getSoftwareUsages). The username is pulled directly from the computer record in JAMF (computer.username), not from a separate user lookup. If a computer in JAMF has no user assigned, the username will be null or empty, and the record will be silently skipped during processing because the user match will fail. Ensure all computers that need usage tracking have users assigned in the JAMF console before running the import.Run SG-JAMF Computers import first: The computer CI import must complete before the software usage import. During processing, the script matches CIs by querying sys_object_source where name = "SG-JAMF" and id = instanceName|UDID (via the findComputer() function). If the computer CI hasn't been imported into ServiceNow, the CI match fails and the record is skipped. Since the usage import iterates through JAMF computers to fetch their software usage, the same computers must already exist as CIs in ServiceNow for the data to be processed.Reclamation rules must be configured: At least one reclamation rule (total_usage or last_used_date) must exist, otherwise the entire import is skipped at the very start by _isAnyReclamationRuleExists(). For each application you want to track, a reclamation rule of the appropriate type must exist with the application's file name in the samp_sw_product_process table. During the initial data load (loadSoftwareUsages), apps are only loaded into the staging table if _isReclamationRuleExists() returns true for that app, which checks both rule types.Software product process records must exist: For each JAMF application, the exact file name reported by JAMF (e.g., "Microsoft Word.app") must exist as an active record in samp_sw_product_process linked to a software product. This mapping is used both during the initial load (to check reclamation rules) and during processing (to resolve the app to a product via _getProductFromFileName()). Without this mapping, the record is skipped.User matching must be configured: The username from the JAMF computer record must match a user in sys_user. The matching field is controlled by the property glide.discovery.assigned_user_match_field (default: user_name). The script first attempts an exact match on the configured field, and if that fails, performs a secondary lookup matching user_name STARTSWITH username@ (via findUserFromUsername()). If your organization uses staff ID or employee number in JAMF, update this property accordingly. All Conditions Required for Usage Record Creation For a record to be created in the SAMP Software Usage table, ALL applicable conditions must be met. Failure of any single condition results in the record being silently skipped. For total_usage Records (via moveDataToUsageBreakdown → aggregateDataToUsage) ConditionDetailsComputer has user assigned in JAMFThe username is pulled from the computer record in JAMF (computer.username). If no user is assigned to the computer, username is null and the record fails user matching.Timekey matchesSource timekey from JAMF data must equal getTimeKeyToPull() (current month - 1)User exists in ServiceNowUsername from JAMF computer record must match the configured field on sys_user (per glide.discovery.assigned_user_match_field)CI exists in ServiceNowinstanceName|UDID must match the "id" column in sys_object_source where name = "SG-JAMF"total_usage reclamation rule existsA total_usage reclamation rule must exist with the app's file name in samp_sw_product_processSoftware product resolvedThe file name must map to a valid product via samp_sw_product_process For last_used_date Records (via _insertLastUsedTime) ConditionDetailsComputer has user assigned in JAMFThe username is pulled from the computer record in JAMF (computer.username). If no user is assigned to the computer, username is null and the record fails user matching.Timekey matchesSource timekey from JAMF data must equal getTimeKeyToPull() (current month - 1)User exists in ServiceNowUsername from JAMF computer record must match the configured field on sys_user (per glide.discovery.assigned_user_match_field)CI exists in ServiceNowinstanceName|UDID must match the "id" column in sys_object_source where name = "SG-JAMF"last_used_date reclamation rule existsA last_used_date reclamation rule must exist with the app's file name in samp_sw_product_process. Applies to: Installed Software or Subscription Software.Software product resolvedThe file name must map to a valid product via samp_sw_product_process Data Retention The script maintains a rolling window of 6 months of usage data (MONTH_COVERAGE = 6). Each time the import runs: getTimeKeyForExpiredData() calculates the timekey for data older than 7 months (current month - 1 - 6)Expired records are deleted from the breakdown table (sn_jamf_sw_usage_breakdown)Expired records are also cleaned from the usage table (samp_sw_usage) using month-based deletion Running the import monthly on the 1st or 2nd ensures clean rotation without gaps in the 6-month window. Troubleshooting SymptomLikely CauseResolutionNo records created in samp_sw_usage or sn_jamf_sw_usage_breakdown after importImport ran mid-month or late in the month causing timekey mismatchReschedule to run on the 1st or 2nd of every month. To fix existing data, update the u_time_key field in the staging table to match the expected default timekey and re-run processing.Staging table has data but breakdown table is emptyu_time_key values in staging table do not match the default timekey (current month - 1)Check u_time_key values in the staging table. Compare with expected default timekey. If mismatched, correct the staging data and re-trigger the transform.Import is completely skipped with no data loadedNo reclamation rules of any type existCreate at least one reclamation rule (total_usage or last_used_date) with associated software products and file names in samp_sw_product_process.Records skipped with User ID null in logsComputer in JAMF has no user assigned, or the JAMF username does not match any sys_user recordVerify that computers in JAMF have users assigned. Check the glide.discovery.assigned_user_match_field property and confirm the JAMF username exists in the configured sys_user field (user_name, staff_id, etc.)."SAMP reclamation rule exist" in logs and record skippedNo total_usage reclamation rule exists for that specific application (misleading log message)Create a total_usage reclamation rule and ensure the app's file name exists in samp_sw_product_process. Note: the log message is misleading — it prints when the rule does NOT exist.CI ID is null in logsComputer CI not found in sys_object_source for the given instanceName|UDIDEnsure SG-JAMF Computers import has run successfully before the software usage import. Verify the CI exists in sys_object_source with name = "SG-JAMF" and id = instanceName|UDID.No software usage data fetched from JAMF API at allNo computers exist in JAMF or computers have no software usage in the requested date rangeVerify computers exist in JAMF and have software usage history. Check the date range calculated by _getStartDate() and _getEndDate() to ensure it covers the expected period. Related System Property PropertyDefaultPurposeglide.discovery.assigned_user_match_fielduser_nameControls which sys_user field is used to match usernames from JAMF import data. Change to staff_id or employee_number if your organization uses a different identifier in JAMF. Related Links<!-- /*NS Branding Styles*/ --> .ns-kb-css-body-editor-container { p { font-size: 12pt; font-family: Lato; color: #000000; } span { font-size: 12pt; font-family: Lato; color: #000000; } h2 { font-size: 24pt; font-family: Lato; color: black; } h3 { font-size: 18pt; font-family: Lato; color: black; } h4 { font-size: 14pt; font-family: Lato; color: black; } a { font-size: 12pt; font-family: Lato; color: #00718F; } a:hover { font-size: 12pt; color: #024F69; } a:target { font-size: 12pt; color: #032D42; } a:visited { font-size: 12pt; color: #00718f; } ul { font-size: 12pt; font-family: Lato; } li { font-size: 12pt; font-family: Lato; } img { display: ; max-width: ; width: ; height: ; } } Related Documentation Service Graph Connector for Jamf — Product DocumentationConfigure Service Graph Connector for Jamf — Configure using SGC Central Configure using guided setupSoftware Reclamation Rules — SAMP AdministrationCommunity: Service Graph Connector for JAMF Software Usages Data ImportServiceNow Store: Service Graph Connector for Jamf