feat: add Autotask API guide, Pulse DB skill, and Datto RMM OpenClaw skill
- AUTOTASK_API_GUIDE.md — auth, query patterns, entity examples, gotchas - PULSE_DATABASE_SKILL.md — full DB schema reference for all data domains - DATTO_RMM_OPENCLAW_SKILL.md — read-only OpenClaw API for devices/sites/alerts
This commit is contained in:
commit
d1fb9db9d4
3 changed files with 1802 additions and 0 deletions
748
AUTOTASK_API_GUIDE.md
Normal file
748
AUTOTASK_API_GUIDE.md
Normal file
|
|
@ -0,0 +1,748 @@
|
|||
# Autotask REST API Development Guide
|
||||
|
||||
## Overview
|
||||
This guide provides comprehensive instructions for developing applications that integrate with the Autotask REST API v1.0. It's based on real-world implementation experience and includes patterns for authentication, caching, error handling, and user impersonation.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Required Credentials
|
||||
You'll need the following from your Autotask instance:
|
||||
- **API Username**: Format typically `apiuser@YOURDOMAIN.COM`
|
||||
- **API Secret/Password**: Strong password for API authentication
|
||||
- **API Integration Code**: Tracking identifier for your integration
|
||||
- **API Base URL**: Usually `https://webservices{X}.autotask.net/atservicesrest/v1.0` where X is your zone number
|
||||
|
||||
### Environment Variables
|
||||
Store these securely in a `.env` file:
|
||||
```bash
|
||||
AUTOTASK_API_URL=https://webservices1.autotask.net/atservicesrest/v1.0
|
||||
AUTOTASK_USERNAME=your-api-username@yourdomain.com
|
||||
AUTOTASK_SECRET=your-api-password
|
||||
AUTOTASK_API_INTEGRATION_CODE=your-tracking-code
|
||||
```
|
||||
|
||||
## Core Implementation
|
||||
|
||||
### 1. Basic Authentication Headers
|
||||
|
||||
Every API request requires these headers:
|
||||
|
||||
```javascript
|
||||
getAuthHeaders(impersonationResourceId = null) {
|
||||
const credentials = Buffer.from(
|
||||
`${this.username}:${this.password}`
|
||||
).toString('base64');
|
||||
|
||||
const headers = {
|
||||
'Authorization': `Basic ${credentials}`,
|
||||
'ApiIntegrationcode': this.apiIntegrationCode,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
};
|
||||
|
||||
// Add impersonation header if needed
|
||||
if (impersonationResourceId) {
|
||||
headers['ImpersonationResourceId'] = impersonationResourceId.toString();
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Common API Patterns
|
||||
|
||||
#### Query Pattern
|
||||
Most Autotask entities support query operations with filters:
|
||||
|
||||
```javascript
|
||||
async queryEntity(entityName, filters = {}) {
|
||||
const filterArray = [];
|
||||
|
||||
// Build filter array based on provided criteria
|
||||
if (filters.fieldName) {
|
||||
filterArray.push({
|
||||
op: 'eq', // Operations: eq, noteq, gt, lt, gte, lte, contains, beginsWith, endsWith
|
||||
field: 'fieldName',
|
||||
value: filters.fieldName
|
||||
});
|
||||
}
|
||||
|
||||
// Construct query
|
||||
const query = filterArray.length > 0 ? { filter: filterArray } : {};
|
||||
const queryString = filterArray.length > 0
|
||||
? `?search=${encodeURIComponent(JSON.stringify(query))}`
|
||||
: '';
|
||||
|
||||
const url = `${this.baseUrl}/${entityName}/query${queryString}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: this.getAuthHeaders()
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
return result.items || [];
|
||||
}
|
||||
```
|
||||
|
||||
#### Get by ID Pattern
|
||||
```javascript
|
||||
async getEntityById(entityName, id) {
|
||||
const url = `${this.baseUrl}/${entityName}/${id}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: this.getAuthHeaders()
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
return result.item;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Working with Specific Entities
|
||||
|
||||
#### Resources (Users)
|
||||
```javascript
|
||||
// Get resource by email
|
||||
async getResourceByEmail(email) {
|
||||
const query = {
|
||||
filter: [{
|
||||
op: 'eq',
|
||||
field: 'email',
|
||||
value: email
|
||||
}]
|
||||
};
|
||||
|
||||
const url = `${this.baseUrl}/Resources/query?search=${encodeURIComponent(JSON.stringify(query))}`;
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: this.getAuthHeaders()
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.items && result.items.length > 0) {
|
||||
const resource = result.items[0];
|
||||
return {
|
||||
id: resource.id,
|
||||
firstName: resource.firstName,
|
||||
lastName: resource.lastName,
|
||||
email: resource.email
|
||||
};
|
||||
}
|
||||
throw new Error('Resource not found');
|
||||
}
|
||||
```
|
||||
|
||||
#### Tickets
|
||||
```javascript
|
||||
// Get open tickets for a resource (exclude status 5 = Complete)
|
||||
async getOpenTicketsByResource(resourceId) {
|
||||
const query = {
|
||||
filter: [
|
||||
{ op: 'eq', field: 'assignedResourceID', value: resourceId },
|
||||
{ op: 'noteq', field: 'status', value: 5 }
|
||||
]
|
||||
};
|
||||
|
||||
return this.queryEntity('Tickets', query);
|
||||
}
|
||||
```
|
||||
|
||||
#### Tasks
|
||||
```javascript
|
||||
// Get tasks for a resource (exclude status 5 = Complete)
|
||||
async getTasksByResource(resourceId) {
|
||||
const query = {
|
||||
filter: [
|
||||
{ op: 'eq', field: 'assignedResourceID', value: resourceId },
|
||||
{ op: 'noteq', field: 'status', value: 5 }
|
||||
]
|
||||
};
|
||||
|
||||
return this.queryEntity('Tasks', query);
|
||||
}
|
||||
```
|
||||
|
||||
#### Companies
|
||||
```javascript
|
||||
// Get all active companies
|
||||
async getAllCompanies() {
|
||||
const query = {
|
||||
filter: [{ op: 'eq', field: 'isActive', value: true }]
|
||||
};
|
||||
|
||||
const companies = await this.queryEntity('Companies', query);
|
||||
return companies.map(company => ({
|
||||
id: company.id,
|
||||
name: company.companyName || company.name
|
||||
})).sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
```
|
||||
|
||||
#### Configuration Items
|
||||
Configuration Items represent physical or virtual assets (computers, servers, network devices, etc.) associated with companies.
|
||||
|
||||
```javascript
|
||||
// Get configuration items for a company
|
||||
async getConfigurationItemsByCompany(companyId) {
|
||||
const query = {
|
||||
filter: [
|
||||
{ op: 'eq', field: 'companyID', value: companyId },
|
||||
{ op: 'eq', field: 'isActive', value: true } // Only active items
|
||||
]
|
||||
};
|
||||
|
||||
return this.queryEntity('ConfigurationItems', query);
|
||||
}
|
||||
|
||||
// Get a single configuration item by ID
|
||||
async getConfigurationItemById(id) {
|
||||
return this.getEntityById('ConfigurationItems', id);
|
||||
}
|
||||
|
||||
// Update a configuration item
|
||||
async updateConfigurationItem(id, updates) {
|
||||
// IMPORTANT: Must fetch current item first to get required fields
|
||||
const currentItem = await this.getConfigurationItemById(id);
|
||||
|
||||
// Autotask requires certain fields even for updates
|
||||
const updateData = {
|
||||
id: id,
|
||||
companyID: currentItem.companyID, // Required
|
||||
productID: currentItem.productID, // Required
|
||||
referenceTitle: updates.referenceTitle ?? currentItem.referenceTitle,
|
||||
isActive: updates.isActive !== undefined ? updates.isActive : currentItem.isActive,
|
||||
// Optional fields
|
||||
serialNumber: updates.serialNumber ?? currentItem.serialNumber,
|
||||
referenceNumber: updates.referenceNumber ?? currentItem.referenceNumber,
|
||||
location: updates.location ?? currentItem.location,
|
||||
notes: updates.notes ?? currentItem.notes
|
||||
};
|
||||
|
||||
const url = `${this.baseUrl}/ConfigurationItems`;
|
||||
|
||||
const response = await this.makeApiCall(url, {
|
||||
method: 'PUT',
|
||||
headers: this.getAuthHeaders(),
|
||||
body: JSON.stringify(updateData)
|
||||
});
|
||||
|
||||
// IMPORTANT: Autotask returns { itemId: X } on successful update, not { item: {...} }
|
||||
// You must fetch the updated item separately
|
||||
if (response.itemId) {
|
||||
return this.getConfigurationItemById(response.itemId);
|
||||
}
|
||||
|
||||
throw new Error('Failed to update configuration item - no itemId in response');
|
||||
}
|
||||
```
|
||||
|
||||
**Configuration Item Key Fields:**
|
||||
- `id` - Unique identifier
|
||||
- `companyID` - Associated company (required)
|
||||
- `productID` - Product/asset type (required, even for updates)
|
||||
- `referenceTitle` - Display name
|
||||
- `serialNumber` - Device serial number
|
||||
- `isActive` - Active status (true/false)
|
||||
- `contactID` - Primary contact for the device
|
||||
- `rmmDeviceUID` - RMM system unique identifier
|
||||
- `rmmDeviceAuditIPAddress` - IP address from RMM
|
||||
- `dattoSerialNumber` - Datto RMM serial number
|
||||
- `dattoInternalIP` / `dattoRemoteIP` - Datto IP addresses
|
||||
|
||||
#### Contacts
|
||||
Contacts can be associated with configuration items to track device ownership/responsibility.
|
||||
|
||||
```javascript
|
||||
// Get contacts for a company
|
||||
async getContactsByCompany(companyId) {
|
||||
const query = {
|
||||
filter: [
|
||||
{ op: 'eq', field: 'companyID', value: companyId },
|
||||
{ op: 'eq', field: 'isActive', value: 1 }
|
||||
]
|
||||
};
|
||||
|
||||
return this.queryEntity('Contacts', query);
|
||||
}
|
||||
|
||||
// Batch fetch contacts to avoid rate limits
|
||||
async getContactsBatch(contactIds) {
|
||||
const contacts = {};
|
||||
|
||||
// Fetch in parallel but respect rate limits
|
||||
const promises = contactIds.map(async (id) => {
|
||||
try {
|
||||
const contact = await this.getEntityById('Contacts', id);
|
||||
if (contact) {
|
||||
contacts[id] = contact;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch contact ${id}:`, error.message);
|
||||
// Continue with other contacts
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
return contacts;
|
||||
}
|
||||
```
|
||||
|
||||
#### Billing Items
|
||||
Billing items represent products/services that can be billed to customers.
|
||||
|
||||
```javascript
|
||||
// Get billing items (products) for a company
|
||||
async getBillingItemsByCompany(companyId) {
|
||||
const query = {
|
||||
filter: [
|
||||
{ op: 'eq', field: 'companyID', value: companyId }
|
||||
]
|
||||
};
|
||||
|
||||
return this.queryEntity('BillingItems', query);
|
||||
}
|
||||
|
||||
// Get all products (for product catalog)
|
||||
async getAllProducts() {
|
||||
return this.queryEntity('Products', {});
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Working with Picklists
|
||||
|
||||
Picklists provide dropdown values for fields like status, priority, etc.
|
||||
|
||||
```javascript
|
||||
async getPicklistValues(entityName, fieldName) {
|
||||
const url = `${this.baseUrl}/${entityName}/entityInformation/fields`;
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: this.getAuthHeaders()
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
const field = result.fields.find(f => f.name === fieldName);
|
||||
|
||||
if (field && field.picklistValues) {
|
||||
const picklistMap = {};
|
||||
field.picklistValues.forEach(item => {
|
||||
picklistMap[item.value] = item.label;
|
||||
});
|
||||
return picklistMap;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
// Example: Get ticket status labels
|
||||
async getTicketStatusPicklist() {
|
||||
return this.getPicklistValues('Tickets', 'status');
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Uploading Attachments
|
||||
|
||||
```javascript
|
||||
async uploadAttachment(entityName, entityId, fileBuffer, fileName, impersonatorEmail = null) {
|
||||
// Get impersonator resource ID if email provided
|
||||
let impersonationResourceId = null;
|
||||
if (impersonatorEmail) {
|
||||
const resource = await this.getResourceByEmail(impersonatorEmail);
|
||||
impersonationResourceId = resource.id;
|
||||
}
|
||||
|
||||
// Convert file to base64
|
||||
const base64Data = fileBuffer.toString('base64');
|
||||
|
||||
// Prepare payload
|
||||
const payload = {
|
||||
id: 0,
|
||||
attachmentType: 'FILE_ATTACHMENT',
|
||||
fullPath: fileName,
|
||||
title: fileName,
|
||||
publish: 1, // 1 = All Autotask Users, 2 = Internal Users Only
|
||||
data: base64Data
|
||||
};
|
||||
|
||||
const url = `${this.baseUrl}/${entityName}/${entityId}/Attachments`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: this.getAuthHeaders(impersonationResourceId),
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to upload attachment: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Implementing Caching with Redis
|
||||
|
||||
To reduce API calls and improve performance:
|
||||
|
||||
```javascript
|
||||
import { createClient } from 'redis';
|
||||
|
||||
class CacheService {
|
||||
constructor() {
|
||||
this.client = createClient({
|
||||
url: process.env.REDIS_URL || 'redis://localhost:6379'
|
||||
});
|
||||
this.client.connect();
|
||||
}
|
||||
|
||||
async getCachedData(key, ttlSeconds = 3600) {
|
||||
const cached = await this.client.get(key);
|
||||
if (cached) {
|
||||
return JSON.parse(cached);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async setCachedData(key, data, ttlSeconds = 3600) {
|
||||
await this.client.setex(key, ttlSeconds, JSON.stringify(data));
|
||||
}
|
||||
|
||||
// Cache with TTL strategy
|
||||
async getWithCache(key, fetchFunction, ttlSeconds = 3600) {
|
||||
const cached = await this.getCachedData(key);
|
||||
if (cached) return cached;
|
||||
|
||||
const fresh = await fetchFunction();
|
||||
await this.setCachedData(key, fresh, ttlSeconds);
|
||||
return fresh;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage example
|
||||
async getCompaniesWithCache() {
|
||||
return cache.getWithCache(
|
||||
'companies:all',
|
||||
() => this.getAllCompanies(),
|
||||
86400 // 24 hour TTL
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Error Handling
|
||||
|
||||
Implement robust error handling for API responses:
|
||||
|
||||
```javascript
|
||||
async makeApiCall(url, options) {
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
const responseText = await response.text();
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`Autotask API error: ${response.status} - ${responseText}`);
|
||||
|
||||
// Try to parse error details
|
||||
let errorMessage = `API Error: ${response.statusText}`;
|
||||
try {
|
||||
const errorData = JSON.parse(responseText);
|
||||
if (errorData.errors && errorData.errors.length > 0) {
|
||||
errorMessage = errorData.errors.map(e => e.message).join(', ');
|
||||
}
|
||||
} catch (parseError) {
|
||||
errorMessage = responseText || errorMessage;
|
||||
}
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
// Parse successful response
|
||||
try {
|
||||
return JSON.parse(responseText);
|
||||
} catch (parseError) {
|
||||
console.error('Failed to parse API response:', parseError.message);
|
||||
throw new Error('Invalid JSON response from Autotask API');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('API call failed:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8. Rate Limiting Considerations
|
||||
|
||||
Autotask has rate limits. Implement throttling:
|
||||
|
||||
```javascript
|
||||
class RateLimiter {
|
||||
constructor(maxRequestsPerSecond = 10) {
|
||||
this.maxRequestsPerSecond = maxRequestsPerSecond;
|
||||
this.requestTimes = [];
|
||||
}
|
||||
|
||||
async throttle() {
|
||||
const now = Date.now();
|
||||
const oneSecondAgo = now - 1000;
|
||||
|
||||
// Remove old request times
|
||||
this.requestTimes = this.requestTimes.filter(t => t > oneSecondAgo);
|
||||
|
||||
// If at limit, wait
|
||||
if (this.requestTimes.length >= this.maxRequestsPerSecond) {
|
||||
const oldestRequest = this.requestTimes[0];
|
||||
const waitTime = 1000 - (now - oldestRequest);
|
||||
if (waitTime > 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, waitTime));
|
||||
}
|
||||
}
|
||||
|
||||
this.requestTimes.push(Date.now());
|
||||
}
|
||||
}
|
||||
|
||||
// Use before API calls
|
||||
await rateLimiter.throttle();
|
||||
const response = await fetch(url, options);
|
||||
```
|
||||
|
||||
## Docker/Container Setup
|
||||
|
||||
### Docker Compose Configuration
|
||||
```yaml
|
||||
services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: app-redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
command: redis-server --save 60 1
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
|
||||
backend:
|
||||
build: ./backend
|
||||
container_name: app-backend
|
||||
ports:
|
||||
- "5001:5001"
|
||||
environment:
|
||||
- AUTOTASK_API_URL=${AUTOTASK_API_URL}
|
||||
- AUTOTASK_USERNAME=${AUTOTASK_USERNAME}
|
||||
- AUTOTASK_SECRET=${AUTOTASK_SECRET}
|
||||
- AUTOTASK_API_INTEGRATION_CODE=${AUTOTASK_API_INTEGRATION_CODE}
|
||||
- REDIS_URL=redis://redis:6379
|
||||
volumes:
|
||||
- ./backend:/app
|
||||
- backend-node-modules:/app/node_modules
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
redis-data:
|
||||
backend-node-modules:
|
||||
```
|
||||
|
||||
## Common Gotchas and Solutions
|
||||
|
||||
### 1. Password Special Characters
|
||||
If your API password contains `$`, escape it in `.env` files (but NOT in docker-compose.yml environment variables).
|
||||
|
||||
### 2. Entity Status Values
|
||||
- Tickets: Status 5 = Complete
|
||||
- Tasks: Status 5 = Complete
|
||||
- Use picklists to get human-readable labels
|
||||
|
||||
### 3. Query Limits
|
||||
- Default max results: 500 items
|
||||
- Implement pagination for large datasets
|
||||
- Use specific filters to reduce result sets
|
||||
|
||||
### 4. Field Names
|
||||
- Field names in queries are case-sensitive
|
||||
- Common fields: `id`, `companyID`, `assignedResourceID`, `status`
|
||||
- Check entity documentation for exact field names
|
||||
|
||||
### 5. Impersonation
|
||||
- Use `ImpersonationResourceId` header to act as another user
|
||||
- Useful for attachments to show correct "uploaded by" user
|
||||
- Requires resource ID, not email (lookup required)
|
||||
|
||||
### 6. Time Zones
|
||||
- Autotask uses UTC for all timestamps
|
||||
- Convert to local timezone for display
|
||||
- Send in UTC for updates
|
||||
|
||||
### 7. Null Values
|
||||
- Many fields can be null
|
||||
- Always check for null before accessing nested properties
|
||||
- Use optional chaining: `ticket?.companyID`
|
||||
|
||||
### 8. Configuration Item Updates - CRITICAL
|
||||
**The update response format is different from other entities:**
|
||||
|
||||
```javascript
|
||||
// ❌ WRONG - This will fail
|
||||
const response = await updateConfigurationItem(id, data);
|
||||
return response.item; // item doesn't exist!
|
||||
|
||||
// ✅ CORRECT - Autotask returns itemId, not item
|
||||
const response = await updateConfigurationItem(id, data);
|
||||
if (response.itemId) {
|
||||
// Must fetch the updated item separately
|
||||
return await getConfigurationItemById(response.itemId);
|
||||
}
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- PUT requests to ConfigurationItems return `{ itemId: 12345 }` not `{ item: {...} }`
|
||||
- You MUST fetch the updated item with a separate GET request
|
||||
- Always include `productID` and `companyID` even for updates (required fields)
|
||||
- Fetch the current item first to preserve required fields you're not updating
|
||||
|
||||
### 9. Rate Limiting with Batch Operations
|
||||
When fetching multiple related entities (like contacts for devices):
|
||||
|
||||
```javascript
|
||||
// ❌ BAD - Sequential requests, very slow
|
||||
for (const contactId of contactIds) {
|
||||
const contact = await getContact(contactId);
|
||||
}
|
||||
|
||||
// ✅ BETTER - Parallel requests, but can hit rate limits
|
||||
await Promise.all(contactIds.map(id => getContact(id)));
|
||||
|
||||
// ✅ BEST - Batch fetch on server side, cache results
|
||||
// Fetch all contacts for a company once, then lookup by ID
|
||||
const allContacts = await getContactsByCompany(companyId);
|
||||
const contactMap = {};
|
||||
allContacts.forEach(c => contactMap[c.id] = c);
|
||||
```
|
||||
|
||||
**Strategy for avoiding rate limits:**
|
||||
1. Fetch related data in bulk when possible (e.g., all contacts for a company)
|
||||
2. Cache frequently accessed data (companies, products, picklists)
|
||||
3. Use batch endpoints when available
|
||||
4. Implement request throttling/queuing for parallel operations
|
||||
|
||||
### 10. Configuration Item Filtering by Status
|
||||
When querying configuration items, the `isActive` field behaves differently:
|
||||
|
||||
```javascript
|
||||
// Get only active items
|
||||
const query = {
|
||||
filter: [
|
||||
{ op: 'eq', field: 'companyID', value: companyId },
|
||||
{ op: 'eq', field: 'isActive', value: true } // Boolean true
|
||||
]
|
||||
};
|
||||
|
||||
// Get only inactive items
|
||||
const query = {
|
||||
filter: [
|
||||
{ op: 'eq', field: 'companyID', value: companyId },
|
||||
{ op: 'eq', field: 'isActive', value: false } // Boolean false
|
||||
]
|
||||
};
|
||||
|
||||
// Get all items (active and inactive)
|
||||
const query = {
|
||||
filter: [
|
||||
{ op: 'eq', field: 'companyID', value: companyId }
|
||||
// Don't filter by isActive
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
### 11. RMM Integration Fields
|
||||
Configuration Items have special fields for RMM system integration:
|
||||
|
||||
- `rmmDeviceUID` - Unique identifier from RMM system (use for matching)
|
||||
- `rmmDeviceAuditIPAddress` - IP address reported by RMM
|
||||
- `rmmDeviceAuditHostname` - Hostname from RMM
|
||||
- `dattoSerialNumber` - Datto-specific serial number
|
||||
- `dattoInternalIP` / `dattoRemoteIP` - Datto-specific IP addresses
|
||||
|
||||
**Best practice for matching RMM devices to Autotask:**
|
||||
1. First try matching by `rmmDeviceUID`
|
||||
2. Fall back to `serialNumber` or `dattoSerialNumber`
|
||||
3. Last resort: match by IP address (less reliable)
|
||||
|
||||
### 12. Contact Association
|
||||
Contacts can be linked to configuration items via `contactID`:
|
||||
|
||||
```javascript
|
||||
// Update configuration item with contact
|
||||
await updateConfigurationItem(itemId, {
|
||||
contactID: contactId // Links device to a specific contact
|
||||
});
|
||||
|
||||
// Remove contact association
|
||||
await updateConfigurationItem(itemId, {
|
||||
contactID: null // Removes contact link
|
||||
});
|
||||
```
|
||||
|
||||
**Note:** Contact must belong to the same company as the configuration item.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
1. **Start with read-only operations** (GET requests)
|
||||
2. **Test with a sandbox/development Autotask instance** if available
|
||||
3. **Cache aggressively** to avoid hitting rate limits during development
|
||||
4. **Log all API calls** during development for debugging
|
||||
5. **Implement retry logic** for transient failures
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Never commit credentials** to version control
|
||||
2. **Use environment variables** for all sensitive data
|
||||
3. **Implement proper authentication** in your application
|
||||
4. **Validate and sanitize** all user inputs
|
||||
5. **Use HTTPS** for all API communications
|
||||
6. **Rotate API credentials** regularly
|
||||
7. **Implement audit logging** for API operations
|
||||
|
||||
## Sample Project Structure
|
||||
|
||||
```
|
||||
project/
|
||||
├── backend/
|
||||
│ ├── src/
|
||||
│ │ ├── server.js
|
||||
│ │ ├── routes/
|
||||
│ │ │ ├── tickets.js
|
||||
│ │ │ └── tasks.js
|
||||
│ │ └── services/
|
||||
│ │ ├── autotask.js
|
||||
│ │ └── cache.js
|
||||
│ ├── package.json
|
||||
│ └── Dockerfile
|
||||
├── frontend/
|
||||
│ ├── src/
|
||||
│ │ └── components/
|
||||
│ ├── package.json
|
||||
│ └── Dockerfile
|
||||
├── docker-compose.yml
|
||||
├── .env
|
||||
└── .env.example
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Autotask REST API Documentation](https://ww1.autotask.net/help/DeveloperHelp/Content/APIs/REST/REST_API_Home.htm)
|
||||
- [Autotask Entity Documentation](https://ww1.autotask.net/help/DeveloperHelp/Content/APIs/REST/Entities/Entities_overview.htm)
|
||||
- Use browser DevTools Network tab to inspect API calls in Autotask UI for field discovery
|
||||
|
||||
---
|
||||
|
||||
This guide should provide a comprehensive starting point for any AI model to help you develop Autotask API integrations. Adapt the patterns to your specific use case and requirements.
|
||||
288
DATTO_RMM_OPENCLAW_SKILL.md
Normal file
288
DATTO_RMM_OPENCLAW_SKILL.md
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
# Datto RMM — OpenClaw Read-Only API Skill
|
||||
|
||||
> **Purpose:** Query Datto RMM data (devices, sites, alerts) from the Pulse platform via authenticated read-only OpenClaw API endpoints. All data is sourced from Pulse's local PostgreSQL DB by default, with an option to fetch live from the Datto RMM API.
|
||||
|
||||
## Base URL
|
||||
|
||||
```
|
||||
https://pulse.wulfconsulting.cloud
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
Every request requires the `x-openclaw-key` header:
|
||||
|
||||
```http
|
||||
x-openclaw-key: <OPENCLAW_API_KEY>
|
||||
```
|
||||
|
||||
Missing or invalid key → `401 Unauthorized`.
|
||||
|
||||
---
|
||||
|
||||
## Data Source Behaviour
|
||||
|
||||
| Parameter | Behaviour |
|
||||
|---|---|
|
||||
| *(default)* | Queries Pulse's local DB — fast, no Datto rate limits, data is as fresh as the last sync |
|
||||
| `?live=true` | Proxies directly to the live Datto RMM API — always current but slower |
|
||||
|
||||
Every response includes `"source": "db"` or `"source": "live"` so you always know data freshness. DB responses also include `synced_at` where available.
|
||||
|
||||
---
|
||||
|
||||
## Endpoints
|
||||
|
||||
### 1. List Sites
|
||||
|
||||
```
|
||||
GET /api/openclaw/datto-rmm/sites
|
||||
GET /api/openclaw/datto-rmm/sites?live=true
|
||||
```
|
||||
|
||||
Returns all Datto RMM sites linked to Autotask companies.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": 376835,
|
||||
"uid": "68431d2c-8327-4d86-a05b-b60d4e7f793e",
|
||||
"name": "Acme Corp",
|
||||
"autotask_company_id": 29683001,
|
||||
"autotask_company_name": "Acme Corp",
|
||||
"number_of_devices": 42,
|
||||
"number_of_online_devices": 38,
|
||||
"number_of_offline_devices": 4,
|
||||
"portal_url": "https://concord.centrastage.net/csm/...",
|
||||
"synced_at": "2026-03-21T20:00:00Z"
|
||||
}
|
||||
],
|
||||
"total": 87,
|
||||
"source": "db"
|
||||
}
|
||||
```
|
||||
|
||||
**Key fields:**
|
||||
- `uid` — Datto RMM site UID (use for device filtering)
|
||||
- `autotask_company_id` — links to Autotask `companies.id` in Pulse DB
|
||||
- `number_of_online_devices` / `number_of_offline_devices` — counts from last sync
|
||||
|
||||
---
|
||||
|
||||
### 2. List Devices
|
||||
|
||||
```
|
||||
GET /api/openclaw/datto-rmm/devices
|
||||
```
|
||||
|
||||
**Query parameters:**
|
||||
|
||||
| Param | Type | Description |
|
||||
|---|---|---|
|
||||
| `siteUid` | string | Filter by Datto RMM site UID |
|
||||
| `online` | boolean | `true` / `false` — filter by online status |
|
||||
| `deleted` | boolean | `true` / `false` — include/exclude deleted devices |
|
||||
| `page` | int | Page number (default: 1) |
|
||||
| `limit` | int | Items per page (default: 100, max: 500) |
|
||||
| `live` | boolean | Use live Datto API instead of DB |
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"uid": "abc123-...",
|
||||
"hostname": "WS-SMITH-01",
|
||||
"site_uid": "68431d2c-...",
|
||||
"site_name": "Acme Corp",
|
||||
"device_type_category": "Desktop",
|
||||
"device_type": "Windows Workstation",
|
||||
"operating_system": "Windows 11 Pro",
|
||||
"domain": "acme.local",
|
||||
"int_ip_address": "10.1.1.50",
|
||||
"ext_ip_address": "203.0.113.5",
|
||||
"online": true,
|
||||
"last_seen": "2026-03-21T19:45:00Z",
|
||||
"last_logged_in_user": "jsmith",
|
||||
"antivirus_product": "Windows Defender",
|
||||
"antivirus_status": "Fully Protected",
|
||||
"patch_status": "Fully Patched",
|
||||
"patches_approved_pending": 0,
|
||||
"reboot_required": false,
|
||||
"udf": { "udf1": "...", "udf2": "..." }
|
||||
}
|
||||
],
|
||||
"total": 3597,
|
||||
"page": 1,
|
||||
"limit": 100,
|
||||
"source": "db"
|
||||
}
|
||||
```
|
||||
|
||||
**Key fields:**
|
||||
- `uid` — Datto RMM device UID (also stored as `configuration_items.reference_number` in Autotask)
|
||||
- `device_type_category` — Server, Desktop, Laptop, Network Device
|
||||
- `antivirus_status` — "Fully Protected", "At Risk", etc.
|
||||
- `patch_status` — "Fully Patched", "Patches Available", "Reboot Required", etc.
|
||||
- `udf` — JSONB object with up to 30 user-defined fields
|
||||
|
||||
---
|
||||
|
||||
### 3. Get Single Device
|
||||
|
||||
```
|
||||
GET /api/openclaw/datto-rmm/devices/{uid}
|
||||
GET /api/openclaw/datto-rmm/devices/{uid}?live=true
|
||||
```
|
||||
|
||||
Returns full device record by Datto RMM UID.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"data": { /* same fields as list, plus: a64_bit, snmp_enabled, network_probe, software_status, warranty_date, cag_version, display_version, web_remote_url */ },
|
||||
"source": "db"
|
||||
}
|
||||
```
|
||||
|
||||
Returns `404` if not found.
|
||||
|
||||
---
|
||||
|
||||
### 4. Get Device Audit Data
|
||||
|
||||
```
|
||||
GET /api/openclaw/datto-rmm/devices/{uid}/audit
|
||||
```
|
||||
|
||||
> **Always live** — fetches real-time audit data from Datto RMM API. No `?live=true` needed.
|
||||
|
||||
Returns detailed hardware/software audit including:
|
||||
- CPU, RAM, disk, BIOS
|
||||
- Network adapters
|
||||
- Installed software list
|
||||
- Hardware inventory
|
||||
|
||||
Returns `404` if device has no audit data.
|
||||
|
||||
---
|
||||
|
||||
### 5. List Alerts
|
||||
|
||||
```
|
||||
GET /api/openclaw/datto-rmm/alerts
|
||||
```
|
||||
|
||||
**Query parameters:**
|
||||
|
||||
| Param | Type | Description |
|
||||
|---|---|---|
|
||||
| `resolved` | boolean | `true` = resolved only, `false` = open only, omit = all |
|
||||
| `siteUid` | string | Filter by site UID |
|
||||
| `deviceUid` | string | Filter by device UID |
|
||||
| `limit` | int | Max results (default: 200, max: 1000) |
|
||||
| `live` | boolean | Use live Datto API |
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"alert_uid": "a1b2c3...",
|
||||
"alert_category": "Patch Management",
|
||||
"alert_type": "Patch Not Installed",
|
||||
"alert_message_en": "Critical patches pending",
|
||||
"device_uid": "abc123-...",
|
||||
"device_hostname": "WS-SMITH-01",
|
||||
"device_os": "Windows 11 Pro",
|
||||
"site_uid": "68431d2c-...",
|
||||
"site_name": "Acme Corp",
|
||||
"resolved": false,
|
||||
"muted": false,
|
||||
"ticket_number": "T20240315.0042",
|
||||
"timestamp": "2026-03-20T14:22:00Z"
|
||||
}
|
||||
],
|
||||
"total": 847,
|
||||
"source": "db"
|
||||
}
|
||||
```
|
||||
|
||||
**Key fields:**
|
||||
- `alert_category` — Patch Management, Antivirus, Performance, Connectivity, etc.
|
||||
- `ticket_number` — Autotask ticket number if one was auto-created
|
||||
- `resolved` — false = currently active alert
|
||||
|
||||
---
|
||||
|
||||
### 6. List Open Alerts (Shorthand)
|
||||
|
||||
```
|
||||
GET /api/openclaw/datto-rmm/alerts/open
|
||||
GET /api/openclaw/datto-rmm/alerts/open?siteUid=68431d2c-...
|
||||
GET /api/openclaw/datto-rmm/alerts/open?live=true
|
||||
```
|
||||
|
||||
Equivalent to `GET /alerts?resolved=false`. Same parameters and response shape as `/alerts` (excluding `resolved` filter since it's always `false`).
|
||||
|
||||
---
|
||||
|
||||
## Common Usage Patterns
|
||||
|
||||
### Get all offline devices across all sites
|
||||
```
|
||||
GET /api/openclaw/datto-rmm/devices?online=false&limit=500
|
||||
```
|
||||
|
||||
### Get open alerts for a specific client
|
||||
1. `GET /api/openclaw/datto-rmm/sites` → find the site UID for the client
|
||||
2. `GET /api/openclaw/datto-rmm/alerts/open?siteUid={uid}`
|
||||
|
||||
### Check patch status for all devices at a site
|
||||
```
|
||||
GET /api/openclaw/datto-rmm/devices?siteUid={uid}&limit=500
|
||||
```
|
||||
Then filter `data` where `patch_status != "Fully Patched"`.
|
||||
|
||||
### Deep-dive a specific device
|
||||
1. `GET /api/openclaw/datto-rmm/devices/{uid}` — base info from DB
|
||||
2. `GET /api/openclaw/datto-rmm/devices/{uid}/audit` — live hardware/software detail
|
||||
|
||||
### Force fresh data (bypass DB cache)
|
||||
```
|
||||
GET /api/openclaw/datto-rmm/devices?live=true&siteUid={uid}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Linking to Autotask / Pulse DB
|
||||
|
||||
| RMM field | Pulse DB join |
|
||||
|---|---|
|
||||
| `datto_rmm_sites.autotask_company_id` | `companies.id` |
|
||||
| `datto_rmm_devices.site_id` | `datto_rmm_sites.id` |
|
||||
| `datto_rmm_alerts.device_uid` | `datto_rmm_devices.uid` |
|
||||
| `datto_rmm_devices.uid` | `configuration_items.reference_number` (sometimes) |
|
||||
| `datto_rmm_alerts.ticket_number` | `tickets.ticket_number` |
|
||||
|
||||
---
|
||||
|
||||
## Error Responses
|
||||
|
||||
| HTTP | Meaning |
|
||||
|---|---|
|
||||
| 401 | Missing or invalid `x-openclaw-key` |
|
||||
| 404 | Device/resource not found |
|
||||
| 500 | Internal error (DB or Datto API) — check `error` field |
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- **Sync freshness:** DB data is updated by scheduled Datto RMM syncs (typically every hour). Check `synced_at` in site/device responses for last sync time.
|
||||
- **Device UIDs:** Datto `uid` is a UUID string. Do not confuse with `id` (integer DB primary key).
|
||||
- **UDFs:** `udf` is a JSONB field with up to 30 user-defined fields (`udf1`–`udf30`). Field meaning varies per client configuration.
|
||||
- **Alert `site_uid`** is a text field matching `datto_rmm_sites.uid` (not the integer `id`).
|
||||
- **Audit endpoint** always makes a live call to Datto RMM — expect 1–3s latency.
|
||||
766
PULSE_DATABASE_SKILL.md
Normal file
766
PULSE_DATABASE_SKILL.md
Normal file
|
|
@ -0,0 +1,766 @@
|
|||
# Pulse Database Skill — Query Reference
|
||||
|
||||
> **Purpose:** This document describes the PostgreSQL database behind **Pulse**, an MSP operations platform built by Wulf Consulting. Use it to query Autotask PSA data, RMM alerts, security agents, backup status, IT documentation, engagement metrics, and more.
|
||||
|
||||
## Connection
|
||||
|
||||
- **Host:** `pulse-postgres` (Docker) or `localhost:5432`
|
||||
- **Database:** `pulse_autotask`
|
||||
- **User:** `pulse_user`
|
||||
- **Read-only queries only** — no INSERT/UPDATE/DELETE
|
||||
- **Timezone:** PostgreSQL server runs in **UTC**. All `timestamp without time zone` columns are UTC. Convert to ET with `AT TIME ZONE 'America/New_York'`.
|
||||
|
||||
---
|
||||
|
||||
## Data Domains at a Glance
|
||||
|
||||
| Domain | Key Tables | Approx Rows | Description |
|
||||
|---|---|---|---|
|
||||
| **Autotask PSA** | tickets, time_entries, companies, contacts, resources, configuration_items, contracts, projects, tasks, ticket_notes | 152K tickets, 156K time entries, 7K CIs | Service desk, billing, contracts, clients |
|
||||
| **Datto RMM** | datto_rmm_alerts, datto_rmm_devices, datto_rmm_sites | 21K alerts, 3.6K devices | Remote monitoring & management |
|
||||
| **SentinelOne** | s1_agents, s1_threats, s1_sites | 2.8K agents, 4.1K threats | Endpoint security |
|
||||
| **Veeam** | veeam_organizations, veeam_backup_jobs, veeam_backup_agents, veeam_alarms, veeam_protected_workloads, veeam_repositories, veeam_backup_servers | ~2.5K total | Backup & disaster recovery |
|
||||
| **IT Glue** | itg_organizations, itg_configurations, itg_passwords, itg_flexible_assets, itg_contacts, itg_documents, itg_expirations, itg_domains, itg_locations | 14.7K configs | IT documentation |
|
||||
| **Microsoft 365** | graph_users, teams_meetings, teams_meeting_attendees, engagement_snapshots | 14.4K meetings | Teams meetings, activity reports |
|
||||
| **Zoom** | zoom_users, zoom_meetings, zoom_meeting_participants, zoom_calls | 2.8K calls | Zoom calls and meetings |
|
||||
| **QuickBooks Online** | qbo_invoices, qbo_payments, qbo_deposits, qbo_transactions, qbo_reports | 6.9K invoices, 12.1K txns | Accounting — invoices, payments, deposits, financial reports |
|
||||
| **Zabbix NMS** | zabbix_events, zabbix_wan_hosts | 231 events, 75 hosts | Network monitoring — WAN hosts, alert events |
|
||||
| **Billing** | billing_items | 96K items | Invoice line items tied to tickets/projects/tasks |
|
||||
| **Authentication** | "user", session, account, verification, two_factor | 3 users | Better Auth — Entra ID SSO, RBAC, sessions |
|
||||
|
||||
---
|
||||
|
||||
## 1. Autotask PSA — Core Service Desk
|
||||
|
||||
### tickets (~152K rows, 68 columns)
|
||||
|
||||
> **Timezone note:** `create_date`, `due_date_time`, `completed_date`, `resolved_date_time`, `first_response_date_time`, `last_activity_date` are all `timestamp without time zone` stored in **UTC**. To display in ET: `create_date AT TIME ZONE 'America/New_York'`.
|
||||
|
||||
The central table. Each row is a service ticket.
|
||||
|
||||
**Key columns:**
|
||||
- `id` (bigint PK) — Autotask ticket ID
|
||||
- `title` (varchar) — ticket subject line
|
||||
- `description` (text) — full body/description
|
||||
- `status` (int) — FK to `statuses.value`
|
||||
- `priority` (int) — FK to `priorities.value`
|
||||
- `queue_id` (int) — FK to `queues.value`
|
||||
- `source` (int) — how the ticket was created (see Source Codes below)
|
||||
- `company_id` (bigint) — FK to `companies.id`
|
||||
- `contact_id` (bigint) — FK to `contacts.id`
|
||||
- `assigned_resource_id` (bigint) — FK to `resources.id`
|
||||
- `configuration_item_id` (bigint) — FK to `configuration_items.id`
|
||||
- `contract_id` (bigint) — FK to `contracts.id`
|
||||
- `project_id` (bigint) — FK to `projects.id`
|
||||
- `issue_type` (int), `sub_issue_type` (int) — classification
|
||||
- `ticket_type` (int) — 1=Incident, 2=Service Request, 5=Alert
|
||||
- `create_date` (timestamp) — when opened
|
||||
- `due_date_time` (timestamp) — SLA due
|
||||
- `completed_date` (timestamp) — when closed
|
||||
- `resolved_date_time` (timestamp) — when resolved
|
||||
- `first_response_date_time` (timestamp) — first response SLA timestamp
|
||||
- `last_activity_date` (timestamp) — most recent update
|
||||
- `monitor_id` (bigint) — RMM monitor that created this ticket (if source=8)
|
||||
- `monitor_type_id` (int) — type of monitor
|
||||
- `is_deleted` (boolean) — soft delete flag
|
||||
|
||||
**Source codes (tickets.source):**
|
||||
| Value | Meaning |
|
||||
|---|---|
|
||||
| 8 | Monitoring Alert (RMM/Datto) |
|
||||
| 4 | Email |
|
||||
| 21 | Portal |
|
||||
| 2 | Phone/Voice |
|
||||
| -1 | Insourced |
|
||||
| -2 | Outsourced |
|
||||
| 35 | Phish Alert |
|
||||
| 6 | API |
|
||||
| 17 | Internal Alert |
|
||||
|
||||
**Ticket types (tickets.ticket_type):**
|
||||
| Value | Meaning |
|
||||
|---|---|
|
||||
| 1 | Incident |
|
||||
| 2 | Service Request |
|
||||
| 5 | Alert |
|
||||
| NULL | Unclassified |
|
||||
|
||||
### companies (~246 rows, 41 columns)
|
||||
|
||||
Client/customer organizations.
|
||||
|
||||
**Key columns:**
|
||||
- `id` (bigint PK)
|
||||
- `company_name` (varchar) — display name
|
||||
- `company_number` (varchar) — short code
|
||||
- `is_active` (boolean)
|
||||
- `company_type` (int) — 1=Customer, 2=Lead, 3=Prospect, 4=Dead, 6=Cancellation, 7=Vendor, etc.
|
||||
- `owner_resource_id` (bigint) — account manager, FK to `resources.id`
|
||||
- `classification` (varchar) — e.g. "Platinum", "Gold", etc.
|
||||
- Address fields: `address1`, `city`, `state`, `postal_code`
|
||||
- `last_activity_date` (timestamp)
|
||||
|
||||
### contacts (~4.2K rows, 37 columns)
|
||||
|
||||
People at client companies.
|
||||
|
||||
**Key columns:**
|
||||
- `id` (bigint PK)
|
||||
- `first_name`, `last_name`, `email_address`, `phone` (varchar)
|
||||
- `company_id` (bigint) — FK to `companies.id`
|
||||
- `is_active` (boolean)
|
||||
- `title` (varchar) — job title
|
||||
|
||||
### resources (~40 columns)
|
||||
|
||||
Internal staff / technicians.
|
||||
|
||||
**Key columns:**
|
||||
- `id` (bigint PK)
|
||||
- `first_name`, `last_name`, `email` (varchar)
|
||||
- `email_address` (varchar) — primary email
|
||||
- `is_active` (boolean)
|
||||
- `resource_type` (varchar)
|
||||
- `default_service_desk_role_id` (bigint)
|
||||
- `hire_date` (date)
|
||||
- `location_id` (bigint)
|
||||
|
||||
### time_entries (~154K rows, 45 columns)
|
||||
|
||||
Work logged against tickets, tasks, or projects.
|
||||
|
||||
**Key columns:**
|
||||
- `id` (bigint PK)
|
||||
- `resource_id` (bigint) — who did the work, FK to `resources.id`
|
||||
- `ticket_id` (bigint) — FK to `tickets.id` (NULL if task/project entry)
|
||||
- `task_id` (bigint) — FK to `tasks.id`
|
||||
- `project_id` (bigint) — FK to `projects.id`
|
||||
- `company_id` (bigint) — FK to `companies.id`
|
||||
- `entry_date` (timestamp) — date of work
|
||||
- `hours_worked` (numeric) — actual hours
|
||||
- `hours_to_bill` (numeric) — billable hours
|
||||
- `start_date_time`, `end_date_time` (timestamp) — clock in/out
|
||||
- `title` (varchar), `notes` (text), `internal_notes` (text)
|
||||
- `billable` (boolean), `non_billable` (boolean)
|
||||
- `billing_rate` (numeric), `cost_rate` (numeric), `revenue` (numeric)
|
||||
- `contract_id` (bigint), `contract_service_id` (bigint)
|
||||
- `role_id` (bigint)
|
||||
- `is_deleted` (boolean)
|
||||
|
||||
### ticket_notes (~41K rows, 14 columns)
|
||||
|
||||
Notes/comments on tickets.
|
||||
|
||||
**Key columns:**
|
||||
- `id` (bigint PK)
|
||||
- `ticket_id` (bigint) — FK to `tickets.id`
|
||||
- `title` (varchar), `description` (text) — note content
|
||||
- `note_type` (int) — internal, external, etc.
|
||||
- `publish` (int) — visibility
|
||||
- `creator_resource_id` (bigint) — who wrote it
|
||||
- `create_date_time` (timestamptz)
|
||||
|
||||
### configuration_items (~7K rows, 95 columns)
|
||||
|
||||
Devices/assets tracked in Autotask.
|
||||
|
||||
**Key columns:**
|
||||
- `id` (bigint PK)
|
||||
- `reference_title` (varchar) — device name (e.g. "DT037", "SRV-DC01")
|
||||
- `reference_number` (varchar) — often a GUID from RMM
|
||||
- `serial_number` (varchar)
|
||||
- `company_id` (bigint) — FK to `companies.id`
|
||||
- `contact_id` (bigint) — FK to `contacts.id`
|
||||
- `is_active` (boolean)
|
||||
- `device_type` (varchar)
|
||||
|
||||
**Note:** `reference_title` follows a naming convention per client (e.g. DT037 exists at multiple companies as separate CIs). Always filter by both `reference_title` AND `company_id` when searching.
|
||||
|
||||
### contracts (~44 columns)
|
||||
|
||||
Service agreements with clients.
|
||||
|
||||
**Key columns:**
|
||||
- `id` (bigint PK)
|
||||
- `company_id` (bigint) — FK to `companies.id`
|
||||
- `contract_name` (varchar), `contract_number` (varchar)
|
||||
- `contract_type` (int), `status` (int)
|
||||
- `start_date`, `end_date` (date)
|
||||
- `estimated_hours` (numeric), `estimated_revenue` (numeric)
|
||||
|
||||
### contract_services (~8.2K rows)
|
||||
|
||||
Line items on contracts.
|
||||
|
||||
- `contract_id` → `contracts.id`
|
||||
- `company_id` → `companies.id`
|
||||
- `service_name` (text), `unit_price`, `quantity`
|
||||
|
||||
### projects (~291 rows, 36 columns)
|
||||
|
||||
**Key columns:**
|
||||
- `id`, `company_id`, `project_name`, `status`, `type`
|
||||
- `project_lead_resource_id` → `resources.id`
|
||||
- `start_date_time`, `end_date_time`, `actual_hours`, `estimated_time`
|
||||
|
||||
### tasks (~4.3K rows, 34 columns)
|
||||
|
||||
Tasks on tickets or projects.
|
||||
|
||||
- `ticket_id` → `tickets.id`
|
||||
- `project_id` → `projects.id`
|
||||
- `assigned_resource_id` → `resources.id`
|
||||
- `status`, `priority`, `estimated_hours`, `remaining_hours`
|
||||
|
||||
### billing_items (~96K rows)
|
||||
|
||||
Invoice line items linked to tickets, tasks, or projects.
|
||||
|
||||
- `ticket_id` → `tickets.id`, `task_id` → `tasks.id`, `project_id` → `projects.id`
|
||||
- `company_id` → `companies.id`
|
||||
- `quantity`, `rate`, `total_amount`, `unit_cost`, `unit_price`
|
||||
|
||||
---
|
||||
|
||||
## 2. Lookup / Picklist Tables
|
||||
|
||||
These map integer codes to human-readable labels. Join on `value`.
|
||||
|
||||
### statuses (ticket statuses)
|
||||
|
||||
Join: `statuses.value = tickets.status`
|
||||
|
||||
| Value | Label |
|
||||
|---|---|
|
||||
| 1 | New |
|
||||
| 5 | Complete |
|
||||
| 7 | Waiting Customer |
|
||||
| 8 | In Progress |
|
||||
| 9 | Waiting Materials |
|
||||
| 10 | Dispatched |
|
||||
| 11 | Escalate |
|
||||
| 12 | Waiting Vendor |
|
||||
| 13 | Waiting Approval |
|
||||
| 14 | Resource Assigned |
|
||||
| 15 | Ready to Deploy |
|
||||
| 16 | Reopened |
|
||||
| 17 | Info Req |
|
||||
| 19 | End User Note Added |
|
||||
| 20 | Equipment Pulled |
|
||||
| 21 | Waiting Verification |
|
||||
| 25 | On Hold |
|
||||
| 27 | Resolution Plan |
|
||||
| 30 | Service Call Scheduled |
|
||||
| 37 | Client Non-Responsive |
|
||||
| 45 | Pending Next Site Visit |
|
||||
| 46 | Burn-in In Progress |
|
||||
| 47 | Resource Requested |
|
||||
| 48 | Escalate to MC |
|
||||
| 51 | Reconcile Billing |
|
||||
| 54 | Resolved \<CSAT Survey\> |
|
||||
| 55 | Huddle Review |
|
||||
| 56 | Tracking Shipment |
|
||||
| 57 | Escalate to Wulf |
|
||||
| 58 | Pre-Pick Order |
|
||||
| 59 | Need to Order/Fulfill |
|
||||
| 60 | Loading Customer Config |
|
||||
| 61 | Escalate to CSM |
|
||||
| 62 | Escalate to Customer HD |
|
||||
| 64 | Waiting Trivium Employee |
|
||||
| 65 | Waiting Procurement |
|
||||
| 66 | Internal Note Added |
|
||||
| 67 | Quote Delivered |
|
||||
| 68 | Ready for Customer Config |
|
||||
| 69 | Outsourced |
|
||||
| 70 | Waiting Quote Acceptance |
|
||||
| 71 | Waiting Project/Ticket |
|
||||
|
||||
### priorities
|
||||
|
||||
Join: `priorities.value = tickets.priority`
|
||||
|
||||
| Value | Label |
|
||||
|---|---|
|
||||
| 1 | Standard |
|
||||
| 2 | Medium |
|
||||
| 3 | Standard |
|
||||
| 4 | Critical |
|
||||
| 6 | High |
|
||||
| 7 | Security Event |
|
||||
| 8 | Minor Service |
|
||||
| 9 | Major Service |
|
||||
| 10 | Installation |
|
||||
| 11 | Fast Track |
|
||||
|
||||
### queues (46 active)
|
||||
|
||||
Join: `queues.value = tickets.queue_id`
|
||||
|
||||
| Value | Label |
|
||||
|---|---|
|
||||
| 5 | Client Triage |
|
||||
| 6 | Post Sale |
|
||||
| 8 | Monitoring Alert |
|
||||
| 29682833 | Level 1 Support |
|
||||
| 29682969 | Level 2 Support |
|
||||
| 29703428 | Level 3 Support |
|
||||
| 29749490 | Client Success |
|
||||
| 29793481 | App-Care |
|
||||
| 29807035 | Alert II |
|
||||
| 29807036 | Alert I |
|
||||
| 29832283 | Operations Triage |
|
||||
| 29853695 | Follow Up |
|
||||
| 29853697 | Trivium Packaging - Help Desk |
|
||||
| 29853698 | Project Delivery |
|
||||
| 29853699 | Mission Control |
|
||||
| 29853700 | Deployment |
|
||||
| 29853701 | IT Operations |
|
||||
| 29853702 | Recurring Tickets |
|
||||
| 29853706 | Premier Automation Help Desk |
|
||||
| 29853710 | TTG Help Desk |
|
||||
| 29853714 | Glunt Help Desk |
|
||||
| 29853719 | TNT Pizza Help Desk |
|
||||
| 29853720 | TTG Machines |
|
||||
| 29853721 | TTG NetOps |
|
||||
| 29853723 | TTG Finance |
|
||||
| 29853724 | TTG Safety |
|
||||
| 29853726 | TTG Remote |
|
||||
| 29853727 | Purchasing |
|
||||
| 29853728 | TTG New User Access |
|
||||
| 29853729 | TTG Term User Access |
|
||||
| 29853731 | Trivium Packaging - MES |
|
||||
| 29853738 | TTG Security |
|
||||
| 29853740 | TTG Change Mgmt |
|
||||
| 29853741 | Subcontractor |
|
||||
| 29853750 | PER Service Desk |
|
||||
| 29853751 | PER Network Operations Center |
|
||||
| 29853752 | PER Security Operations Center |
|
||||
| 29853753 | LEC Service Desk |
|
||||
| 29853754 | LEC Network Operations Center |
|
||||
| 29853755 | LEC Security Operations Center |
|
||||
| 29853756 | VCF Service Desk |
|
||||
| 29853757 | VCF Network Operations |
|
||||
| 29853758 | VCF Security Operations Center |
|
||||
| 29853766 | Sales |
|
||||
| 29780304 | TaskFire |
|
||||
| 29753333 | Waiting Verification |
|
||||
|
||||
### issue_types
|
||||
|
||||
Join: `issue_types.value = tickets.issue_type`
|
||||
|
||||
| Value | Label |
|
||||
|---|---|
|
||||
| 4 | Upgrade |
|
||||
| 6 | New Install |
|
||||
| 7 | Monitoring Alert |
|
||||
| 10 | Break/Fix |
|
||||
| 11 | Maintenance |
|
||||
| 12 | Help Desk |
|
||||
| 13 | Email |
|
||||
| 14 | Vendor |
|
||||
| 15 | User Education |
|
||||
| 16 | Provision |
|
||||
| 18 | Purchase |
|
||||
| 19 | Deploy |
|
||||
| 20 | Reactive |
|
||||
| 21 | MAC |
|
||||
| 22 | Centralized Services |
|
||||
| 23 | NOC Services |
|
||||
| 25 | Networking |
|
||||
| 27 | WiFi |
|
||||
| 28 | Client Success |
|
||||
| 29 | Automation |
|
||||
| 30 | Server |
|
||||
| 31 | Hardware |
|
||||
| 32 | LOB Software |
|
||||
| 33 | Workstation |
|
||||
| 36 | 24-Hour Emergency Support |
|
||||
|
||||
---
|
||||
|
||||
## 3. Datto RMM
|
||||
|
||||
### datto_rmm_alerts (~20.7K rows, 61 columns)
|
||||
|
||||
- `id` (int PK), `uid` (text) — alert identifiers
|
||||
- `alert_category`, `alert_type`, `alert_message_en` — what triggered
|
||||
- `priority` (text) — Critical, High, Moderate, Low, Information
|
||||
- `resolved` (boolean), `resolved_on` (timestamptz)
|
||||
- `muted` (boolean)
|
||||
- `ticket_number` (text) — linked Autotask ticket
|
||||
- `device_hostname`, `device_ip`, `device_os`, `device_id`
|
||||
- `site_id` (text) — FK to `datto_rmm_sites`
|
||||
- `timestamp` (timestamptz) — when alert fired
|
||||
|
||||
### datto_rmm_devices (~3.6K rows, 42 columns)
|
||||
|
||||
- `id` (int PK), `uid` (text), `hostname`
|
||||
- `device_type_category` (text) — Server, Desktop, Laptop, Network Device
|
||||
- `operating_system`, `domain`, `int_ip_address`, `ext_ip_address`
|
||||
- `online` (boolean), `last_seen` (timestamptz)
|
||||
- `last_logged_in_user` (text)
|
||||
- `antivirus_product`, `antivirus_status`, `patch_status`
|
||||
- `site_id` (int) — FK to `datto_rmm_sites.id`
|
||||
- `udf` (jsonb) — custom fields
|
||||
|
||||
### datto_rmm_sites (~16 columns)
|
||||
|
||||
- `id` (int PK), `uid`, `name`
|
||||
- `autotask_company_id` (int) — **FK to `companies.id`** (links RMM sites to Autotask clients)
|
||||
- `autotask_company_name`
|
||||
- `number_of_devices`, `number_of_online_devices`
|
||||
|
||||
**Join pattern:** `datto_rmm_sites.autotask_company_id = companies.id`
|
||||
|
||||
---
|
||||
|
||||
## 4. SentinelOne
|
||||
|
||||
### s1_agents (~2.8K rows, 42 columns)
|
||||
|
||||
Endpoint security agents.
|
||||
|
||||
- `id` (varchar PK) — S1 agent ID
|
||||
- `computer_name`, `os_name`, `os_type`
|
||||
- `site_id` → `s1_sites.id`, `site_name`
|
||||
- `is_active`, `is_decommissioned`
|
||||
- `infected` (boolean), `active_threats` (int)
|
||||
- `network_status`, `mitigation_mode`, `detection_state`
|
||||
- `external_ip`, `last_active_date`, `last_logged_in_user_name`
|
||||
- `firewall_enabled` (boolean)
|
||||
|
||||
### s1_threats (~4.1K rows, 25 columns)
|
||||
|
||||
Detected threats.
|
||||
|
||||
- `id` (varchar PK)
|
||||
- `threat_name`, `classification`, `confidence_level`
|
||||
- `mitigation_status`, `analyst_verdict`, `incident_status`
|
||||
- `agent_id` → `s1_agents.id`
|
||||
- `agent_computer_name`, `agent_os_name`
|
||||
- `site_id` → `s1_sites.id`
|
||||
|
||||
### s1_sites (~22 columns)
|
||||
|
||||
- `id` (varchar PK), `name`, `account_name`
|
||||
- `health_status`, `active_licenses`, `total_licenses`
|
||||
|
||||
### s1_company_mappings
|
||||
|
||||
Maps S1 sites to Autotask companies for cross-referencing.
|
||||
|
||||
---
|
||||
|
||||
## 5. Veeam Backup
|
||||
|
||||
### veeam_organizations (~17 columns)
|
||||
|
||||
- `instance_uid` (PK), `name`, `company_id`
|
||||
- All other Veeam tables FK to `veeam_organizations.instance_uid`
|
||||
|
||||
### veeam_backup_jobs (~234 rows)
|
||||
|
||||
- `instance_uid`, `name`, `type`, `status`, `last_run`, `next_run`
|
||||
- `organization_uid` → `veeam_organizations`
|
||||
- `backup_server_uid` → `veeam_backup_servers`
|
||||
|
||||
### veeam_backup_agents (~727 rows)
|
||||
|
||||
- Backup agents installed on endpoints
|
||||
- `organization_uid` → `veeam_organizations`
|
||||
|
||||
### veeam_alarms (~581 rows)
|
||||
|
||||
- Active alarms/alerts
|
||||
- `organization_uid` → `veeam_organizations`
|
||||
|
||||
### veeam_protected_workloads, veeam_repositories, veeam_backup_servers
|
||||
|
||||
Supporting tables for backup infrastructure.
|
||||
|
||||
---
|
||||
|
||||
## 6. IT Glue Documentation
|
||||
|
||||
### itg_organizations (~330 rows)
|
||||
|
||||
- `id` (bigint PK), `name`, `short_name`, `organization_type_name`
|
||||
- `psa_id` (varchar) — Autotask company ID (string). Join: `itg_organizations.psa_id::bigint = companies.id`
|
||||
|
||||
### itg_configurations (~14.7K rows)
|
||||
|
||||
Hardware/software assets documented in IT Glue.
|
||||
|
||||
- `id`, `organization_id` → `itg_organizations.id`
|
||||
- `name`, `hostname`, `serial_number`, `asset_tag`
|
||||
- `configuration_type_name`, `configuration_status_name`
|
||||
- `primary_ip`, `mac_address`, `operating_system`
|
||||
- `warranty_expires_at`, `installed_at`
|
||||
|
||||
### itg_passwords (~17 columns)
|
||||
|
||||
- `id`, `organization_id`, `name`, `username`, `password_category_name`
|
||||
- `url`, `notes`
|
||||
|
||||
### itg_flexible_assets (~3.2K rows)
|
||||
|
||||
Custom documentation (e.g. Backup configs, Email configs, LAN/VLAN, Voice/PBX).
|
||||
|
||||
- `id`, `organization_id`, `flexible_asset_type_id`, `flexible_asset_type_name`
|
||||
- `traits` (jsonb) — all custom field values
|
||||
|
||||
### itg_contacts, itg_documents, itg_expirations, itg_domains, itg_locations
|
||||
|
||||
Supporting IT documentation tables.
|
||||
|
||||
---
|
||||
|
||||
## 7. Engagement & Communications
|
||||
|
||||
### graph_users
|
||||
|
||||
Microsoft 365 users synced from Azure AD.
|
||||
|
||||
- `id` (varchar PK), `display_name`, `email`, `job_title`, `department`
|
||||
- `account_enabled` (boolean)
|
||||
|
||||
### teams_meetings (~14.3K rows)
|
||||
|
||||
Teams calendar events / meetings.
|
||||
|
||||
- `id` (int PK), `user_email`, `subject`
|
||||
- `start_time`, `end_time` (timestamptz), `duration_minutes`
|
||||
- `attendee_count`, `client_attendee_count`, `has_client_attendees` (boolean)
|
||||
|
||||
### teams_meeting_attendees (~12.6K rows)
|
||||
|
||||
- `meeting_id` → `teams_meetings.id`
|
||||
- `attendee_email`, `attendee_name`
|
||||
- `matched_contact_id` → `contacts.id`
|
||||
- `matched_company_id` → `companies.id`
|
||||
|
||||
### engagement_snapshots (~1K rows)
|
||||
|
||||
Weekly/monthly aggregates of M365 activity per user.
|
||||
|
||||
- `user_email`, `period_type` (D7, D30, D90, D180)
|
||||
- `teams_chat_messages`, `teams_calls`, `teams_meetings_attended`, `teams_meetings_organized`
|
||||
- `emails_sent`, `emails_received`, `emails_read`
|
||||
|
||||
### zoom_meetings, zoom_meeting_participants
|
||||
|
||||
- `host_email`, `topic`, `start_time`, `end_time`, `duration_minutes`
|
||||
- Participants with `matched_contact_id` → `contacts.id`, `matched_company_id` → `companies.id`
|
||||
- `is_internal` (boolean) — internal vs external attendee
|
||||
|
||||
### zoom_calls (~2.8K rows)
|
||||
|
||||
- `resource_email`, `direction` (inbound/outbound), `call_status`
|
||||
- `other_party_number`, `other_party_name`
|
||||
- `matched_contact_id`, `matched_company_id`
|
||||
|
||||
---
|
||||
|
||||
## 8. QuickBooks Online (Accounting)
|
||||
|
||||
### qbo_invoices (~6.9K rows)
|
||||
|
||||
- `id` (text PK) — QBO invoice ID
|
||||
- `doc_number` (text) — invoice number (e.g. "1042")
|
||||
- `txn_date` (date), `due_date` (date)
|
||||
- `customer_ref_id`, `customer_ref_name` — QBO customer
|
||||
- `total_amt` (numeric), `balance` (numeric) — amounts
|
||||
- `status` (text) — Paid, Overdue, etc.
|
||||
- `line_items` (jsonb) — invoice line detail
|
||||
- `linked_txns` (jsonb) — linked payments
|
||||
|
||||
### qbo_payments (~4.5K rows)
|
||||
|
||||
- `id` (text PK)
|
||||
- `txn_date` (date), `total_amt` (numeric)
|
||||
- `customer_ref_id`, `customer_ref_name`
|
||||
- `payment_method_ref` (text), `deposit_account_ref` (text)
|
||||
- `unapplied_amt` (numeric)
|
||||
- `linked_txns` (jsonb) — linked invoices
|
||||
|
||||
### qbo_deposits (~1.8K rows)
|
||||
|
||||
- `id` (text PK)
|
||||
- `txn_date` (date), `total_amt` (numeric)
|
||||
- `deposit_to_account_ref_id`, `deposit_to_account_ref_name`
|
||||
- `line_items` (jsonb)
|
||||
|
||||
### qbo_transactions (~12.1K rows)
|
||||
|
||||
General ledger transactions (expenses, bills, journal entries, etc.).
|
||||
|
||||
- `id` (text), `txn_type` (text) — composite PK
|
||||
- `txn_date` (date), `doc_number` (text)
|
||||
- `entity_ref_id`, `entity_ref_name`, `entity_type` — vendor/customer
|
||||
- `account_ref_id`, `account_ref_name` — GL account
|
||||
- `total_amt` (numeric)
|
||||
- `line_items` (jsonb)
|
||||
|
||||
**Transaction types:** Bill, BillPayment, Expense, JournalEntry, Transfer, VendorCredit, CreditMemo, SalesReceipt, Estimate, PurchaseOrder, etc.
|
||||
|
||||
### qbo_reports (~36 rows)
|
||||
|
||||
Periodic financial reports stored as JSON.
|
||||
|
||||
- `id` (serial PK)
|
||||
- `report_type` (text) — ProfitAndLoss, BalanceSheet, CashFlow
|
||||
- `period_start` (date), `period_end` (date)
|
||||
- `report_data` (jsonb) — full QBO report payload
|
||||
- Unique on `(realm_id, report_type, period_start, period_end)`
|
||||
|
||||
---
|
||||
|
||||
## 9. Zabbix NMS
|
||||
|
||||
### zabbix_wan_hosts (~75 rows)
|
||||
|
||||
WAN monitoring hosts auto-synced from RMM site public IPs.
|
||||
|
||||
- `host_id` (text) — Zabbix host ID
|
||||
- `host` (text) — hostname in Zabbix
|
||||
- `name` (text) — display name
|
||||
- `ip` (text) — WAN IP address
|
||||
- `status` (int) — 0=enabled, 1=disabled
|
||||
- `company_id` (bigint) — FK to `companies.id`
|
||||
- `rmm_site_id` (int) — FK to `datto_rmm_sites.id`
|
||||
|
||||
### zabbix_events (~228 rows)
|
||||
|
||||
Zabbix alert events.
|
||||
|
||||
- `event_id` (text PK)
|
||||
- `host_id` (text), `host_name` (text)
|
||||
- `trigger_id` (text), `trigger_name` (text)
|
||||
- `severity` (int) — 0=Not classified, 1=Info, 2=Warning, 3=Average, 4=High, 5=Disaster
|
||||
- `value` (int) — 0=OK, 1=Problem
|
||||
- `clock` (timestamptz) — event time
|
||||
|
||||
---
|
||||
|
||||
## 10. Authentication (Better Auth + Entra ID)
|
||||
|
||||
### "user" table (~3 rows)
|
||||
|
||||
**Note:** Table name is `"user"` (quoted) — must be quoted in SQL.
|
||||
|
||||
- `id` (text PK)
|
||||
- `name` (text), `email` (text UNIQUE)
|
||||
- `"emailVerified"` (boolean), `image` (text)
|
||||
- `role` (text) — `super-admin`, `admin`, `user`
|
||||
- `banned` (boolean), `"bannedReason"` (text), `"banExpires"` (timestamp)
|
||||
- `requires_setup` (boolean), `"twoFactorEnabled"` (boolean)
|
||||
- `"createdAt"`, `"updatedAt"` (timestamp)
|
||||
|
||||
**Note:** Better Auth uses **camelCase** column names — must be double-quoted in raw SQL.
|
||||
|
||||
### session
|
||||
|
||||
- `id` (text PK), `"userId"` → `"user".id`
|
||||
- `token` (text UNIQUE), `"expiresAt"` (timestamp)
|
||||
- `"ipAddress"`, `"userAgent"` (text)
|
||||
|
||||
### account
|
||||
|
||||
OAuth provider links (Microsoft Entra ID).
|
||||
|
||||
- `id` (text PK), `"userId"` → `"user".id`
|
||||
- `"providerId"` (text) — e.g. `microsoft`
|
||||
- `"accountId"` (text) — provider-specific user ID
|
||||
- `"accessToken"`, `"refreshToken"`, `"idToken"` (text)
|
||||
|
||||
---
|
||||
|
||||
## 11. Common Join Patterns
|
||||
|
||||
```sql
|
||||
-- Ticket with company, resource, and status label
|
||||
SELECT t.id, t.title, c.company_name,
|
||||
r.first_name || ' ' || r.last_name AS technician,
|
||||
s.label AS status_label, p.label AS priority_label
|
||||
FROM tickets t
|
||||
LEFT JOIN companies c ON c.id = t.company_id
|
||||
LEFT JOIN resources r ON r.id = t.assigned_resource_id
|
||||
LEFT JOIN statuses s ON s.value = t.status
|
||||
LEFT JOIN priorities p ON p.value = t.priority
|
||||
WHERE t.is_deleted IS NOT TRUE;
|
||||
|
||||
-- Time entries for a ticket
|
||||
SELECT te.entry_date, te.hours_worked, te.notes,
|
||||
r.first_name || ' ' || r.last_name AS technician
|
||||
FROM time_entries te
|
||||
JOIN resources r ON r.id = te.resource_id
|
||||
WHERE te.ticket_id = $1 AND te.is_deleted IS NOT TRUE;
|
||||
|
||||
-- RMM device → Autotask company
|
||||
SELECT d.hostname, d.device_type_category, d.operating_system,
|
||||
s.autotask_company_name, d.online, d.last_seen
|
||||
FROM datto_rmm_devices d
|
||||
JOIN datto_rmm_sites s ON s.id = d.site_id;
|
||||
|
||||
-- Config item lookup (always filter by company too)
|
||||
SELECT ci.id, ci.reference_title, ci.serial_number, c.company_name
|
||||
FROM configuration_items ci
|
||||
JOIN companies c ON c.id = ci.company_id
|
||||
WHERE ci.reference_title = 'DT037' AND ci.company_id = $1;
|
||||
|
||||
-- IT Glue org → Autotask company
|
||||
SELECT ig.name, ig.id AS itg_org_id, c.id AS autotask_company_id, c.company_name
|
||||
FROM itg_organizations ig
|
||||
JOIN companies c ON ig.psa_id::bigint = c.id;
|
||||
|
||||
-- Meetings with client attendees
|
||||
SELECT tm.subject, tm.start_time, tm.duration_minutes,
|
||||
tma.attendee_name, c.company_name
|
||||
FROM teams_meetings tm
|
||||
JOIN teams_meeting_attendees tma ON tma.meeting_id = tm.id
|
||||
LEFT JOIN companies c ON c.id = tma.matched_company_id
|
||||
WHERE tm.has_client_attendees = true;
|
||||
|
||||
-- QBO: Revenue by month (from invoices)
|
||||
SELECT DATE_TRUNC('month', txn_date) AS month,
|
||||
SUM(total_amt) AS total_invoiced, COUNT(*) AS invoice_count
|
||||
FROM qbo_invoices
|
||||
GROUP BY 1 ORDER BY 1 DESC;
|
||||
|
||||
-- QBO: Outstanding balances by customer
|
||||
SELECT customer_ref_name, SUM(balance) AS outstanding
|
||||
FROM qbo_invoices WHERE balance > 0
|
||||
GROUP BY 1 ORDER BY 2 DESC;
|
||||
|
||||
-- Zabbix: Active WAN problems with company
|
||||
SELECT ze.host_name, ze.trigger_name, ze.severity, ze.clock,
|
||||
c.company_name
|
||||
FROM zabbix_events ze
|
||||
JOIN zabbix_wan_hosts zwh ON zwh.host_id = ze.host_id
|
||||
LEFT JOIN companies c ON c.id = zwh.company_id
|
||||
WHERE ze.value = 1 ORDER BY ze.clock DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Important Notes
|
||||
|
||||
1. **Soft deletes:** Most Autotask tables have `is_deleted` (boolean) and `deleted_at`. Always add `WHERE is_deleted IS NOT TRUE` unless you want deleted records.
|
||||
|
||||
2. **Picklist joins:** `status`, `priority`, `queue_id`, `source` on tickets are integer codes. Join to `statuses`, `priorities`, `queues` on `.value` for labels.
|
||||
|
||||
3. **Configuration item names are NOT unique globally.** Names like "DT037" are a per-client naming convention. Always pair with `company_id`.
|
||||
|
||||
4. **Timestamps — all UTC:** The PostgreSQL server timezone is UTC. All `timestamp without time zone` columns (Autotask) are stored in UTC. `timestamp with time zone` columns (Teams, Zoom, QBO) are also UTC-normalized. To display in Eastern Time: `column AT TIME ZONE 'America/New_York'`. Example: `create_date AT TIME ZONE 'America/New_York'`.
|
||||
|
||||
5. **Monitor tickets:** `tickets.source = 8` indicates RMM-generated tickets. `monitor_id` links to the specific Datto RMM monitor. These represent ~74% of all tickets.
|
||||
|
||||
6. **Cross-platform linking:**
|
||||
- RMM → Autotask: `datto_rmm_sites.autotask_company_id = companies.id`
|
||||
- IT Glue → Autotask: `itg_organizations.psa_id::bigint = companies.id`
|
||||
- S1 → Autotask: via `s1_company_mappings`
|
||||
- Zoom/Teams → Contacts: `matched_contact_id` / `matched_company_id` columns
|
||||
- Config Items → RMM: `configuration_items.reference_number` sometimes matches RMM device UIDs
|
||||
|
||||
7. **Row counts** (as of March 17, 2026): tickets 152K, time_entries 156K, billing_items 96K, ticket_notes 42K, teams_meetings 14.4K, itg_configurations 14.7K, configuration_items 7K, companies 246, contacts 4.2K, datto_rmm_devices 3.6K, datto_rmm_alerts 21K, s1_agents 2.8K, s1_threats 4.1K, zoom_calls 2.8K, qbo_invoices 6.9K, qbo_transactions 12.1K, qbo_payments 4.5K, qbo_deposits 1.8K, qbo_reports 36, zabbix_wan_hosts 75, zabbix_events 231.
|
||||
Loading…
Add table
Add a link
Reference in a new issue