Repository navigation
Asset Reconciliation Developer Guide
Since 3.1.0 Β· Built by Sandy (@srinivasansanthosh) in PR #164, merged with the changes in the review Β· User side: Assets β Help β Discovery & reconciliation
How FreeITSM decides whether a device reported by the inventory agent or Intune is an asset it already has. Before 3.1.0 every source matched on hostname alone, so a renamed computer arrived as a second asset with none of its history. Now a device is matched by what it is (its serial number) before what it is called.
| File | Role |
|---|---|
includes/services/assets.php |
getIgnoredServiceTags(), isUsableServiceTag(), resolveAssetIdentity() (reads only), updateAssetHostname(), reconcileAsset() (resolve + apply a rename), createDiscoveredAsset()
|
api/external/system-info/submit/index.php |
The inventory agent: reconcileAsset() then update, or createDiscoveredAsset()
|
api/external/software-inventory/submit/index.php |
The software agent: the same two calls (it only sends a hostname, so it always lands on tier 3) |
includes/intune.php |
intuneLinkDevicesToAssets() β renames for linked devices, then match-or-create for unlinked ones |
includes/service_context.php |
ActorContext::system('Inventory agent' / 'Intune') β the actor for changes no person made |
asset-management/settings/ |
Discovery & reconciliation tab (the placeholder list, Cap::ASSETS_RECONCILIATION); Intune tab (company, renaming) |
tests/asset-reconciliation.php |
24 checks, one rolled-back transaction |
π Every path that creates an asset now goes through AssetsService β createAsset() for people, the REST API and imports; createDiscoveredAsset() for the agents and Intune. INSERT INTO assets appears in that one file and nowhere else. Keep it that way: it is what makes the tag, the transaction and the history row the same whichever source made the asset.
Tier 1 an authoritative connector link the caller already holds -> that asset
Tier 2 a usable serial -> the one asset with it
...but two or more assets share it -> no guess (ambiguous), unless serial AND name pick one
Tier 3 hostname -> that asset
...unless a usable serial CONTRADICTS it -> new asset, flagged hostname_reused
Tier 4 nothing -> new asset
It returns ['asset_id', 'matched_by', 'ambiguous', 'hostname_reused'] and writes nothing. reconcileAsset() wraps it and, for a match made by link or serial, applies a reported rename through updateAssetHostname().
A replacement laptop is very often given the old one's name. It reports a good serial nobody has seen, so tier 2 finds nothing β and a plain hostname match would then write the new serial onto the old laptop's record. One record would describe two machines: the old one's purchase date and warranty now belong to the new one, and the old one, sitting in a cupboard for reuse, has no record at all.
So a hostname match only counts if it cannot contradict the serial:
$contradicts = $serialUsable && self::isUsableServiceTag($theirs, $ignoredTags)
&& strcasecmp($theirs, $serial) !== 0;Both serials usable and different β a different machine β tier 4, with hostname_reused = the old asset's id. createDiscoveredAsset() records that in the new asset's history (hostname_reused, value #<id>) so a person sees it and renames or retires the old one.
The case this must not break: an asset entered without a serial (by hand, or before the agent ran) still matches by name. That is how the agent finds a hand-entered asset the first time, and fills its serial in.
Two assets with the same serial (old duplicates, cloned VMs) are a question for a person. The resolver does not pick one. It accepts a match only if serial and hostname together identify exactly one β Sandy's original fell back to the hostname alone, which could still pick the wrong twin.
DEFAULT_IGNORED_SERVICE_TAGS β TO BE FILLED BY O.E.M., DEFAULT STRING, NONE, SYSTEM SERIAL NUMBER, NOT SPECIFIED, 123456789 β is what a motherboard reports when the maker never filled the field in. Matching on one would merge every such machine into one asset.
The list is editable (asset_reconciliation_ignored_serials, one per line, compared case-insensitively). A stored list replaces the defaults rather than adding to them, so an administrator can remove one that turns out to be a real serial on their estate.
- Hostname is unique per company, so a rename onto a name another asset in the asset's own company already has is skipped and reported as
hostname_conflict. The asset's own company β not the caller's β because a person may have moved the asset since the source last saw it. - Truncated to 50 characters (
assets.hostnameisVARCHAR(50); Intune names can be 256). - Recorded in history through
auditWrite()with no analyst (ActorContext::system()has actorId 0, written as NULL) and the source in the new value:LON-LT-042 (inventory agent). - π The history write is best-effort. Before Database Verification has relaxed
asset_history.analyst_id(it wasNOT NULL), the write fails β and an agent's whole report must not fail over its own audit row. It is logged witherror_log()instead.
Takes a whitelist of discovered columns, the company, the source and an optional hostname_reused. It runs through AssetTagsService::createWithTag(), so a company with automatic tags switched on gets one here too, inside the same transaction as the insert.
π΄ Its history row is asset_discovered, not asset_created. Database Verification's last_seen repair (#1583, api/system/db_verify.php) treats an asset_created row as proof that a person or an import made the record, and blanks last_seen wherever it equals first_seen β which it does on every machine found by an agent. Writing asset_created here would have blanked last_seen on every newly discovered machine at the next verification. Don't "tidy" the two names together.
| Behaviour | |
|---|---|
An existing link (intune_devices.asset_id) |
Trusted, wherever the asset now is. The MSP workflow is: a stub lands in one company, an analyst moves it to the client it belongs to (Moving an asset between companies). A link is never second-guessed because its asset is in a different company β a person put it there. |
| A renamed device | Found with one query (device_name β the asset's hostname), not a reconcile per device per sync. The asset takes the new name only if intune_sync_hostnames is on (below). |
| An unlinked device |
reconcileAsset() β serial, then name β in the company set under Settings β Intune. Unset (every install after upgrade) matches across all companies, exactly as the old hostname-only link did. No match β createDiscoveredAsset() in that company (Default if unset). |
On the install this was developed against, 65 of 582 linked devices had been renamed or reassigned in Intune since they were linked (NWUNIFI01 in Intune, still BWBUNIFI01 on the asset). Renaming them is the right result β it's what the feature is for β but doing it silently on the first sync after an upgrade breaks the rule that an upgrade changes nothing until somebody chooses to.
So it's a switch on the Intune tab. freeitsm.sql seeds it '1' (a new install has nothing to rename) and Database Verification seeds '0'; INSERT IGNORE means a new install keeps its '1' and an upgraded one gets '0'.
-
The agents match only within their key's company (
tenant_id <=> ?, NULL-safe so a Default-company key matches NULL rows). Unchanged from before. -
Intune β see the table above.
resolveAssetIdentity(..., $anyCompany = true)drops the company condition; nothing else should pass it. -
idx_assets_tenant_service_tag (tenant_id, service_tag)serves tier 2.
php tests/asset-reconciliation.php β 24 checks inside one transaction that is always rolled back, rows prefixed ZZREC-: placeholders; a renamed machine found by serial and renamed, with an analyst-less history row; the ambiguity guard with a positive control; the laptop refresh (new asset, flagged, old serial untouched) with a positive control; a rename collision; createDiscoveredAsset() writing asset_discovered; company isolation and "any company"; and Intune β a moved asset keeps its link, an unlinked device links by serial with no stub, and renaming follows the setting. It runs against the install's real Intune devices too, inside the transaction, and leaves them as they were.
-
Another source (vCenter, Jamf, a network scan): build an
ActorContext::system('<source>'), callreconcileAsset(), andcreateDiscoveredAsset()on no match. Don't write anINSERT. - A stronger identifier β the SMBIOS UUID is better than the serial for virtual machines (cloned VMs can share a serial). It would slot in between tier 1 and tier 2. Not MAC addresses: docks and Wi-Fi randomisation move them between machines.
See also: Asset tag numbering β Developer Guide Β· The review (PR #164) Β· Inventory agent Β· Multi-Tenancy β Developer Guide Β· Developer Tests β Assets
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- π§ͺ Developer tests
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- π Date & Time Formats
- Theming & Dark Mode
- ποΈ Recent β getting back to what you were doing
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
-
MobileβFriendly
- β³ π« Mobile: Tickets
- β³ π» Mobile: Assets
- β³ π Mobile: Calendar
- β³ π Mobile: Knowledge
- β³ π¦ Mobile: Service Status
- β³ πΌ Mobile: Watchtower
- β³ π§© Mobile: Problem Management
- β³ π Mobile: Change Management
- β³ πΏ Mobile: Software
- β³ β Mobile: Tasks
- β³ π Mobile: Forms
- β³ π Mobile: Contracts
- β³ π Mobile: Domains
- β³ π Mobile: People
- β³ π Mobile: Projects
- β³ π Mobile: LMS
- β³ πΊοΈ Mobile: CMDB
- β³ πΊοΈ Mobile: Network Mapper
- β³ π§ Mobile: Process Mapper
- β³ βοΈ Mobile: Workflow
- β³ π₯οΈ Mobile: System
- β³ π Mobile: Reporting
- β³ π Mobile: System Wiki
- β³ π Mobile: Self-Service Portal
- β³ π§° Mobile: Techniques & Tricks
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- β³ π‘οΈ CSRF protection (S4) β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- π CardDAV contact sync
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π REST API: Domains
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ π·οΈ REST API: Cost centres
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ π Rota copy and paste β Developer Deep Dive
- β³ β Checklists & SOPs
- β³ βοΈ Mandatory fields
- β³ π·οΈ Ticket categories
- β³ π₯ Assigning tickets to a team, and escalation
- β³ π’ One board across every company
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
-
β³
βοΈ Telegram channel - β³ β CSAT company scope and filters β Developer Guide
- β³ π₯ Microsoft Teams channel
- β³ π¨οΈ Mattermost channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ β Record previews
- β³ π Ticket notes: internal or shared
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ π¨ Telling the analyst a ticket is theirs
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ π Confidential tickets
- β³ π₯ Portal managers
- β³ π Who has seen a ticket
- β³ π Reading long tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ ποΈ The folder pane
- β³ π½ Just my tickets, or no closed ones
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- β³ π Scheduled work in your own calendar
- Problem Management
- Tasks
- π Projects
-
Assets
- β³ π’ Moving an asset between companies
- β³ π Shared asset locations
- β³ π§βπΌ Assigning assets to analysts
- β³ π Warranty and lease alerts
- β³ π Saved table views
- β³ π¨οΈ Recording anything, and importing it
- β³ π·οΈ QR asset labels
- β³ π Who holds what, and handover documents
- β³ π₯οΈ The inventory agent (PowerShell)
- β³ ποΈ Proxmox VE servers
- β³ βοΈ VMware Cloud Director servers
- β³ π Linking equipment to tickets
- β³ βοΈ Follow-up tasks on a ticket
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
-
Forms
- β³ π¨ The form designer β Developer Guide
- β³ π Layout & the grid β Developer Guide
- β³ ποΈ Collections β grouping submissions
- β³ π Submissions as PDFs
- β³ β‘ What happens next β a form's own actions
- β³ π οΈ Sections & conditional logic β Developer Guide
- β³ π οΈ Lookup fields β Developer Guide
- β³ π‘οΈ Catalogue request approvals
- People
- Domains
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ π’ One board across every company
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)
- What this is
-
π Bugs resolved
- β³ πΌοΈ Logo and courses broke on Apache with PHP-FPM
- β³ π’ Chat tickets ignored your ticket numbering
- β³ π Dates shown as a dash, or in server time
- β³ π Assets β Users showed people from other companies
- β³ π Restricted analysts could read other modules' data
- β³ πΌοΈ Replies with a picture in the thread failed to send
- β³ π Reply attachments never reached the customer
- β³ π οΈ Outbound email attachments β Developer Guide
- β³ π A global SSO provider was missing from the portal
- β³ π Behind a proxy, the SSO redirect said http
- β³ βοΈ The portal tagline moved when you saved it
- β³ π¨ The portal settings screen forgot what you saved
- β³ π‘οΈ The approvals inbox said "Error" and nothing else
- β³ π A table's answers were missing from the PDF
- β³ β A single-select column let you tick every option
- β³ π The portal ignored a form's field widths
- β³ π The tasks board stopped taking clicks
- β³ ποΈ #121 The index list is out of date after upgrading
- β³ π #133 The calendar subscription was empty
- β³ π #131 Tasks always reopened on the board
- β³ π₯ #129 Every page returned HTTP 500 after upgrading
- β³ π³ #127 A PHP warning above the System page
- β³ π #126 Notes stamped with the server's clock
- β³ π Storing every date in UTC
- β³ πͺ The portal was down for everyone signed in
- β³ βοΈ #120 Workflow notes could never be written
- β³ βοΈ #123 Three errors when running Database Verification
- β³ π #122 The description box was a stub in the corner
- β³ π£ Demo data deleted real accounts
- β³ π #117 Sign-in redirected to the wrong address
- β³ π¨ #108 The priority dot was invisible
- β³ β±οΈ #116 Time logged from the right-click menu
- β³ π #114 API keys refused by our own guard
- β³ ποΈ #110 Assigning a task told nobody
- β³ πͺ #107 Signed out while still working
- β³ π #103 "Share with Requester" reached nobody
- β³ π #102 Search found nothing for hyphens
- β³ πͺ #101 Source code editor opened behind
- β³ βοΈ #88 Subtasks could not be ticked off
- β³ π» #84 Asset deep link selected nothing
- β³ π« #79 A new ticket arrived with no status
- β³ π§ #79 A ticket from email did not say so
- β³ π #78 Bell opened to nothing
- β³ π¬ #77 Mail only collected from Inbox
- β³ π #74 The default password could not be changed
- β³ π¦ #70 Renaming an impact level
- β³ π€ #67 App-only mailboxes could not send
- β³ π #45 Verify only ever worked for Microsoft
- β³ π #45 IMAP reported as not authenticated
- β³ βοΈ An email template stopped escaping itself
- β³ π The portal dashboard showed the wrong time
- β³ π’ The folder said 99 and the list showed 96