From kaseya-datto-rmm
Lists, manages, and configures Datto RMM sites for client locations, covering structure, settings, proxy configuration, site variables, device assignment, and scoped operations.
How this skill is triggered — by the user, by Claude, or both
Slash command
/kaseya-datto-rmm:sitesThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Sites in Datto RMM represent client organizations or locations. Each site contains devices, has its own settings, and can have site-level variables. Sites provide organizational hierarchy and enable scoped operations - alerts, jobs, and reports can all be filtered by site.
Sites in Datto RMM represent client organizations or locations. Each site contains devices, has its own settings, and can have site-level variables. Sites provide organizational hierarchy and enable scoped operations - alerts, jobs, and reports can all be filtered by site.
Account
└── Sites (many)
└── Devices (many per site)
└── Alerts, Jobs, Audit Data
Sites can represent:
| Identifier | Type | Description |
|---|---|---|
siteUid | string | Globally unique identifier |
siteId | integer | Legacy numeric ID |
name | string | Display name |
interface Site {
// Identifiers
uid: string; // Unique site ID
siteId: number; // Legacy numeric ID
name: string; // Site display name
description?: string; // Site description
// Configuration
onDemand: boolean; // On-demand site (no scheduled tasks)
splapiEnabled: boolean; // Service Provider Level API enabled
proxySettings?: ProxySettings; // HTTP proxy configuration
// Counts
devicesCount: number; // Number of devices
openAlertsCount: number; // Active alerts
// Timestamps (Unix milliseconds)
createdAt: number; // When site was created
modifiedAt: number; // Last modification
// Settings
settings: SiteSettings;
}
interface ProxySettings {
enabled: boolean;
host: string;
port: number;
username?: string;
bypassList?: string[]; // Hosts to bypass proxy
}
interface SiteSettings {
autoPatchApproval: boolean;
patchWindow: PatchWindow;
notificationEmail?: string;
timezone: string;
}
interface PatchWindow {
dayOfWeek: number; // 0=Sunday, 6=Saturday
startHour: number; // 0-23
durationHours: number;
}
GET /api/v2/sites?max=250
Authorization: Bearer {token}
Response:
{
"sites": [
{
"uid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Acme Corporation",
"description": "Main office",
"devicesCount": 45,
"openAlertsCount": 3,
"onDemand": false
}
],
"pageDetails": {
"count": 1,
"nextPageUrl": null
}
}
GET /api/v2/site/{siteUid}
Authorization: Bearer {token}
Response:
{
"uid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"siteId": 12345,
"name": "Acme Corporation",
"description": "Main office - Downtown",
"devicesCount": 45,
"openAlertsCount": 3,
"onDemand": false,
"splapiEnabled": true,
"createdAt": 1680000000000,
"modifiedAt": 1707991200000,
"proxySettings": {
"enabled": false
},
"settings": {
"autoPatchApproval": false,
"timezone": "America/New_York"
}
}
GET /api/v2/site/{siteUid}/devices?max=250
Authorization: Bearer {token}
GET /api/v2/site/{siteUid}/alerts/open
Authorization: Bearer {token}
GET /api/v2/site/{siteUid}/alerts/resolved?max=250
Authorization: Bearer {token}
POST /api/v2/sites
Authorization: Bearer {token}
Content-Type: application/json
{
"name": "New Client Site",
"description": "Client headquarters",
"onDemand": false
}
POST /api/v2/site/{siteUid}
Authorization: Bearer {token}
Content-Type: application/json
{
"name": "Updated Site Name",
"description": "Updated description"
}
DELETE /api/v2/site/{siteUid}
Authorization: Bearer {token}
Warning: Deleting a site does not delete devices - they become unassigned.
async function findSiteByName(client, name) {
const response = await client.request('/api/v2/sites?max=250');
const sites = response.sites || [];
// Exact match first
const exact = sites.find(s =>
s.name.toLowerCase() === name.toLowerCase()
);
if (exact) return { found: true, site: exact };
// Partial match
const matches = sites.filter(s =>
s.name.toLowerCase().includes(name.toLowerCase())
);
if (matches.length === 0) {
return { found: false, suggestions: [] };
}
if (matches.length === 1) {
return { found: true, site: matches[0] };
}
return {
found: false,
ambiguous: true,
suggestions: matches.map(s => ({
name: s.name,
uid: s.uid,
deviceCount: s.devicesCount
}))
};
}
async function getSiteHealth(client, siteUid) {
const [site, devices, alerts] = await Promise.all([
client.request(`/api/v2/site/${siteUid}`),
client.request(`/api/v2/site/${siteUid}/devices?max=250`),
client.request(`/api/v2/site/${siteUid}/alerts/open`)
]);
const deviceList = devices.devices || [];
const alertList = alerts.alerts || [];
// Device status breakdown
const deviceStatus = {
online: deviceList.filter(d => d.status === 'online').length,
offline: deviceList.filter(d => d.status === 'offline').length,
total: deviceList.length
};
// Alert priority breakdown
const alertsByPriority = {
Critical: alertList.filter(a => a.priority === 'Critical').length,
High: alertList.filter(a => a.priority === 'High').length,
Moderate: alertList.filter(a => a.priority === 'Moderate').length,
Low: alertList.filter(a => a.priority === 'Low').length
};
// Calculate health score
const healthScore = calculateSiteHealthScore(deviceStatus, alertsByPriority);
return {
site: {
name: site.name,
uid: site.uid
},
devices: deviceStatus,
alerts: {
total: alertList.length,
byPriority: alertsByPriority
},
healthScore,
status: healthScore >= 80 ? 'healthy' : healthScore >= 50 ? 'warning' : 'critical'
};
}
function calculateSiteHealthScore(devices, alerts) {
let score = 100;
// Deduct for offline devices
const offlinePercent = (devices.offline / devices.total) * 100;
score -= offlinePercent * 0.5;
// Deduct for alerts
score -= alerts.Critical * 15;
score -= alerts.High * 5;
score -= alerts.Moderate * 2;
score -= alerts.Low * 0.5;
return Math.max(0, Math.round(score));
}
async function getAllSitesSummary(client) {
const response = await client.request('/api/v2/sites?max=250');
const sites = response.sites || [];
return sites.map(site => ({
name: site.name,
uid: site.uid,
devices: site.devicesCount,
openAlerts: site.openAlertsCount,
status: site.openAlertsCount === 0 ? 'healthy' :
site.openAlertsCount <= 5 ? 'warning' : 'critical'
})).sort((a, b) => b.openAlerts - a.openAlerts);
}
async function validateSiteSetup(client, siteUid) {
const site = await client.request(`/api/v2/site/${siteUid}`);
const devices = await client.request(`/api/v2/site/${siteUid}/devices?max=250`);
const variables = await client.request(`/api/v2/site/${siteUid}/variables`);
const checks = [];
// Check site has description
checks.push({
item: 'Site description',
status: site.description ? 'pass' : 'fail',
message: site.description || 'No description set'
});
// Check site has devices
checks.push({
item: 'Devices enrolled',
status: devices.devices?.length > 0 ? 'pass' : 'fail',
message: `${devices.devices?.length || 0} devices`
});
// Check critical variables are set
const requiredVars = ['BACKUP_PATH', 'ADMIN_EMAIL'];
requiredVars.forEach(varName => {
const v = variables.variables?.find(v => v.name === varName);
checks.push({
item: `Variable: ${varName}`,
status: v?.value ? 'pass' : 'fail',
message: v?.value || 'Not set'
});
});
return {
siteUid,
siteName: site.name,
checks,
passed: checks.filter(c => c.status === 'pass').length,
total: checks.length
};
}
| Error | Status | Cause | Resolution |
|---|---|---|---|
| Site not found | 404 | Invalid siteUid | Verify site exists |
| Name already exists | 400 | Duplicate site name | Use unique name |
| Cannot delete | 400 | Site has devices | Move devices first |
| Permission denied | 403 | API restrictions | Check permissions |
async function safeSiteOperation(client, operation, siteUid, data) {
try {
switch (operation) {
case 'get':
return await client.request(`/api/v2/site/${siteUid}`);
case 'update':
return await client.request(`/api/v2/site/${siteUid}`, {
method: 'POST',
body: JSON.stringify(data)
});
case 'delete':
// Check for devices first
const devices = await client.request(`/api/v2/site/${siteUid}/devices`);
if (devices.devices?.length > 0) {
throw new Error(`Cannot delete site with ${devices.devices.length} devices`);
}
return await client.request(`/api/v2/site/${siteUid}`, {
method: 'DELETE'
});
}
} catch (error) {
if (error.status === 404) {
return { error: 'Site not found', siteUid };
}
throw error;
}
}
Recommended Format: {ClientName} - {Location/Purpose}
Examples:
Acme Corp - Main OfficeAcme Corp - Remote WorkersTechStart Inc - Data CenterInternal - IT Departmentnpx claudepluginhub wyre-technology/msp-claude-plugins --plugin datto-rmmManages RunZero sites: list/create/update sites, define scan scopes/exclusions, deploy explorers, organize assets by location or client.
Manages Hudu website records for SSL/TLS monitoring, email security (DMARC, DKIM, SPF), DNS records, and company linking. Covers CRUD, monitoring fields, and verification patterns.
Provides Datto RMM API v2 patterns: OAuth 2.0 authentication, 6-platform URLs, token lifecycle, pagination, rate limiting, error handling.