npx claudepluginhub wyre-technology/msp-claude-plugins --plugin halopsaThis skill uses the workspace's default tool permissions.
Assets (also called Configuration Items or CIs) in HaloPSA represent managed devices, software, and other trackable items. Effective asset management is crucial for MSPs to track what's deployed at client sites, manage hardware lifecycle, and link service tickets to affected equipment.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Migrates code, prompts, and API calls from Claude Sonnet 4.0/4.5 or Opus 4.1 to Opus 4.5, updating model strings on Anthropic, AWS, GCP, Azure platforms.
Analyzes BMad project state from catalog CSV, configs, artifacts, and query to recommend next skills or answer questions. Useful for help requests, 'what next', or starting BMad.
Assets (also called Configuration Items or CIs) in HaloPSA represent managed devices, software, and other trackable items. Effective asset management is crucial for MSPs to track what's deployed at client sites, manage hardware lifecycle, and link service tickets to affected equipment.
The primary entity representing a managed device or configuration item.
| Field | Type | Required | Description |
|---|---|---|---|
id | int | System | Unique identifier |
inventory_number | string | No | Internal asset tag |
client_id | int | Yes | Associated client |
site_id | int | No | Physical location |
user_id | int | No | Assigned user |
devicetype_id | int | No | Asset type category |
status_id | int | No | Asset status |
| Field | Type | Required | Description |
|---|---|---|---|
devicename | string(255) | Yes | Device hostname/name |
serialnumber | string | No | Manufacturer serial |
assettag | string | No | Company asset tag |
barcode | string | No | Barcode identifier |
macaddress | string | No | Network MAC address |
| Field | Type | Required | Description |
|---|---|---|---|
manufacturer | string | No | Device manufacturer |
model | string | No | Device model |
operatingsystem | string | No | OS name/version |
operatingsystemversion | string | No | OS detailed version |
ipaddress | string | No | IP address |
| Field | Type | Required | Description |
|---|---|---|---|
purchasedate | date | No | Date acquired |
purchaseprice | decimal | No | Purchase cost |
warrantyexpires | date | No | Warranty end date |
lastauditdate | datetime | No | Last RMM sync |
inactive | bool | No | Active status |
| Field | Type | Required | Description |
|---|---|---|---|
contract_id | int | No | Associated contract |
supplier_id | int | No | Vendor/supplier |
notes | text | No | Asset notes |
Common asset types in HaloPSA:
| Type ID | Name | Examples |
|---|---|---|
| 1 | Workstation | Desktop, laptop |
| 2 | Server | Physical/virtual server |
| 3 | Network Device | Router, switch, firewall |
| 4 | Printer | Network/local printer |
| 5 | Mobile Device | Phone, tablet |
| 6 | Software | License, subscription |
| 7 | Other | Miscellaneous |
Note: Asset types are configurable per instance. Query /api/AssetType for your values.
| Status ID | Name | Description |
|---|---|---|
| 1 | Active | In production use |
| 2 | Spare | Available backup |
| 3 | In Repair | Under maintenance |
| 4 | Retired | End of life |
| 5 | On Order | Pending delivery |
| 6 | Lost/Stolen | Missing |
Note: Status IDs are configurable per instance. Query /api/AssetStatus for your values.
POST /api/Asset
Authorization: Bearer {token}
Content-Type: application/json
[
{
"devicename": "ACME-WS-001",
"client_id": 123,
"site_id": 456,
"user_id": 789,
"devicetype_id": 1,
"status_id": 1,
"manufacturer": "Dell",
"model": "OptiPlex 7090",
"serialnumber": "ABC123XYZ",
"assettag": "ACME-001",
"operatingsystem": "Windows 11 Pro",
"operatingsystemversion": "22H2",
"ipaddress": "192.168.1.100",
"macaddress": "00:1A:2B:3C:4D:5E",
"purchasedate": "2024-01-15",
"purchaseprice": 1299.99,
"warrantyexpires": "2027-01-15"
}
]
{
"assets": [
{
"id": 5001,
"devicename": "ACME-WS-001",
"client_id": 123,
"client_name": "Acme Corporation",
"site_name": "Acme HQ",
"status_id": 1,
"status_name": "Active"
}
]
}
By client:
GET /api/Asset?client_id=123
By site:
GET /api/Asset?site_id=456
By type:
GET /api/Asset?devicetype_id=1
Active assets only:
GET /api/Asset?inactive=false
Search by name:
GET /api/Asset?search=ACME-WS
Warranty expiring soon:
GET /api/Asset?warrantyexpires_before=2024-03-31
GET /api/Asset/5001
With related data:
GET /api/Asset/5001?includedetails=true
POST /api/Asset
Authorization: Bearer {token}
Content-Type: application/json
[
{
"id": 5001,
"status_id": 3,
"notes": "Sent to vendor for motherboard repair"
}
]
[
{ "id": 5001, "status_id": 1 },
{ "id": 5002, "status_id": 1 },
{ "id": 5003, "status_id": 4, "inactive": true }
]
When creating or updating a ticket, reference the asset:
[
{
"summary": "Workstation not booting",
"client_id": 123,
"asset_id": 5001,
"tickettype_id": 1,
"status_id": 1
}
]
Parent-child relationships (e.g., server and its VMs):
[
{
"id": 5010,
"parent_id": 5009,
"notes": "VM hosted on ACME-SRV-001"
}
]
Track changes via the audit log or custom fields.
HaloPSA integrates with RMM tools to auto-sync assets.
When integrated with an RMM, these fields typically auto-populate:
devicenameoperatingsystemoperatingsystemversionipaddressmacaddresslastauditdate| Field | Description |
|---|---|
ncentral_device_id | N-central device ID |
datto_device_id | Datto RMM device ID |
connectwise_device_id | Automate device ID |
async function matchOrCreateAsset(rmmDevice) {
// 1. Try to match by RMM ID
let asset = await findAssetByRmmId(rmmDevice.id);
if (!asset) {
// 2. Try to match by serial number
asset = await findAssetBySerial(rmmDevice.serialNumber);
}
if (!asset) {
// 3. Try to match by hostname + client
asset = await findAssetByHostname(
rmmDevice.hostname,
rmmDevice.clientId
);
}
if (asset) {
// Update existing
return updateAsset(asset.id, rmmDevice);
} else {
// Create new
return createAsset(rmmDevice);
}
}
Create asset as "On Order"
[{ "devicename": "New Laptop", "status_id": 5, "client_id": 123 }]
Receive and configure
Deploy to user
user_id and site_idIdentify aging assets
GET /api/Asset?purchasedate_before=2020-01-01&inactive=false
Generate refresh report
Plan replacement
Find expiring warranties
GET /api/Asset?warrantyexpires_before=2024-06-30&warrantyexpires_after=2024-01-01
Generate renewal quotes
Update warranty dates after renewal
async function auditClientAssets(clientId) {
const assets = await getClientAssets(clientId);
const report = {
total: assets.length,
active: 0,
noWarranty: [],
missingSerial: [],
oldOS: []
};
assets.forEach(asset => {
if (!asset.inactive) report.active++;
if (!asset.warrantyexpires || new Date(asset.warrantyexpires) < new Date()) {
report.noWarranty.push(asset);
}
if (!asset.serialnumber) {
report.missingSerial.push(asset);
}
if (asset.operatingsystem?.includes('Windows 10') &&
new Date(asset.purchasedate) < new Date('2020-01-01')) {
report.oldOS.push(asset);
}
});
return report;
}
| Code | Message | Resolution |
|---|---|---|
| 400 | devicename required | Asset must have a name |
| 400 | client_id required | Asset must be linked to client |
| 400 | Invalid devicetype_id | Query /api/AssetType for valid IDs |
| 404 | Asset not found | Verify asset ID exists |
| 409 | Duplicate serial number | Serial already in use |
function validateAsset(asset) {
const errors = [];
if (!asset.devicename || asset.devicename.trim() === '') {
errors.push('Device name is required');
}
if (!asset.client_id) {
errors.push('Client ID is required');
}
if (asset.macaddress && !isValidMac(asset.macaddress)) {
errors.push('Invalid MAC address format');
}
if (asset.ipaddress && !isValidIP(asset.ipaddress)) {
errors.push('Invalid IP address format');
}
return {
isValid: errors.length === 0,
errors
};
}
GET /api/Asset?groupby=devicetype_id&count=true
GET /api/Asset?groupby=client_id&count=true
async function getAssetValueByClient() {
const clients = await fetchAllClients();
const results = [];
for (const client of clients) {
const assets = await getClientAssets(client.id);
const totalValue = assets.reduce(
(sum, a) => sum + (a.purchaseprice || 0), 0
);
results.push({
client_id: client.id,
client_name: client.name,
asset_count: assets.length,
total_value: totalValue
});
}
return results.sort((a, b) => b.total_value - a.total_value);
}