670 lines
22 KiB
Markdown
670 lines
22 KiB
Markdown
|
|
# Webhook Pipeline Engine
|
||
|
|
|
||
|
|
The Pipeline Engine is a DB-driven automation system that processes incoming webhooks through configurable multi-step pipelines. It supports ticket creation, cross-system enrichment, RMM quick jobs, AI analysis, notifications (Teams, Telegram, ntfy), and human-in-the-loop approvals.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Architecture
|
||
|
|
|
||
|
|
```
|
||
|
|
Webhook Source → Raw Log → Trigger Match → Pipeline Steps → Actions
|
||
|
|
│ │ │ │
|
||
|
|
Datto RMM conditions on Filter, Create ticket,
|
||
|
|
Autotask payload fields Enrich, Run quick job,
|
||
|
|
Veeam Transform, Send notification,
|
||
|
|
Manual AI analyze, Wait for approval,
|
||
|
|
Delay Update Autotask
|
||
|
|
```
|
||
|
|
|
||
|
|
When a webhook arrives at `/api/webhooks/datto-rmm`, the route:
|
||
|
|
1. Logs the raw payload to `datto_rmm_webhook_logs`
|
||
|
|
2. Calls `pipelineEngine.processTrigger('datto_rmm', payload)` (fire-and-forget)
|
||
|
|
3. Returns `200 OK` immediately
|
||
|
|
|
||
|
|
The engine then finds all active pipelines matching the trigger source and conditions, and executes each one sequentially.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Concepts
|
||
|
|
|
||
|
|
### Pipelines
|
||
|
|
A pipeline is a named workflow triggered by a specific webhook source. Each pipeline has:
|
||
|
|
- **Trigger source** — `datto_rmm`, `autotask`, `veeam`, or `manual`
|
||
|
|
- **Trigger conditions** — JSON array of conditions that the incoming payload must match
|
||
|
|
- **Steps** — ordered list of actions to execute
|
||
|
|
- **Active/inactive toggle** — disabled pipelines are skipped
|
||
|
|
|
||
|
|
### Steps
|
||
|
|
Each step has a type, a name, a JSON config, and failure handling. Steps execute in order. Each step can read from and write to a shared **context** object.
|
||
|
|
|
||
|
|
### Context
|
||
|
|
The context is a JSONB object that accumulates data as steps execute:
|
||
|
|
- `context.trigger` — the original webhook payload
|
||
|
|
- Step outputs are merged into the top level (e.g., `context.company_id`, `context.ticket_id`)
|
||
|
|
|
||
|
|
### Template Variables
|
||
|
|
Step configs support `{{variable}}` syntax that is resolved before execution:
|
||
|
|
- `{{trigger.device_hostname}}` — field from the webhook payload
|
||
|
|
- `{{context.company_id}}` — field set by a previous step
|
||
|
|
- Nested paths work: `{{trigger.nested.field}}`
|
||
|
|
- Templates resolve recursively through objects and arrays
|
||
|
|
|
||
|
|
### Notification Channels
|
||
|
|
Channels are configured in the UI and referenced by ID in `notify` and `approval` steps. Each channel stores its type-specific credentials:
|
||
|
|
|
||
|
|
| Type | Config Fields |
|
||
|
|
|------|--------------|
|
||
|
|
| **Teams** | `webhook_url` |
|
||
|
|
| **Telegram** | `bot_token`, `chat_id`, `parse_mode` |
|
||
|
|
| **ntfy** | `server_url`, `topic`, `auth_token`, `default_priority` |
|
||
|
|
| **Webhook** | `url`, `method`, `headers` |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Step Types
|
||
|
|
|
||
|
|
### Logic
|
||
|
|
|
||
|
|
#### `filter`
|
||
|
|
Evaluate conditions against the current context. If conditions fail, the step fails (and with `on_failure: stop`, the pipeline stops).
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"conditions": [
|
||
|
|
{ "field": "trigger.alert_priority", "operator": "equals", "value": "CRITICAL" }
|
||
|
|
]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Operators:** `equals`, `not_equals`, `contains`, `not_contains`, `in`, `not_in`, `regex`, `exists`, `not_exists`
|
||
|
|
|
||
|
|
#### `transform`
|
||
|
|
Map payload fields into context variables. All values are template-resolved.
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"mappings": {
|
||
|
|
"device_hostname": "{{trigger.device_hostname}}",
|
||
|
|
"alert_type": "{{trigger.alert_type}}",
|
||
|
|
"site_name": "{{trigger.site_name}}"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `set_variable`
|
||
|
|
Set a single context variable.
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"key": "severity",
|
||
|
|
"value": "{{trigger.alert_priority}}"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `delay`
|
||
|
|
Wait before continuing to the next step.
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"seconds": 30
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Enrichment
|
||
|
|
|
||
|
|
#### `enrich_device`
|
||
|
|
Look up a device from the local `datto_rmm_devices` table by UID. Outputs: `device`, `device_found`, `device_hostname`, `device_os`, `device_ip`, `company_id`, `company_name`.
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"lookup_by": "device_uid",
|
||
|
|
"source_field": "{{trigger.device_uid}}"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `enrich_company`
|
||
|
|
Look up an Autotask company from `datto_rmm_sites` by site name or UID. Falls back to fuzzy match on `companies` table. Outputs: `company_id`, `company_name`, `company_found`.
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"lookup_by": "site_name",
|
||
|
|
"source_field": "{{context.site_name}}"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `enrich_ticket`
|
||
|
|
Look up a ticket from the local `tickets` table. Outputs: `ticket`, `ticket_found`, `ticket_id`, `ticket_number`, `ticket_title`.
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"lookup_by": "ticket_number",
|
||
|
|
"source_field": "{{context.ticket_number}}"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `enrich_vspc`
|
||
|
|
Query local Veeam VSPC data for a device's backup status. Searches backup agent jobs, server jobs, protected workloads, and active alarms. Outputs: `vspc_found`, `vspc_last_job_status`, `vspc_last_success`, `vspc_hours_since_success`, `vspc_failure_message`, `vspc_restore_points`, `vspc_backed_up_size`, `vspc_alarm_count`, `vspc_failed_job_count`, `vspc_summary`, `vspc_agent_jobs`, `vspc_server_jobs`, `vspc_workloads`, `vspc_alarms`.
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"lookup_by": "device_name",
|
||
|
|
"source_field": "{{context.device_hostname}}"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Data
|
||
|
|
|
||
|
|
#### `db_query`
|
||
|
|
Run a parameterized read-only SQL query against local Postgres. Only `SELECT` statements are allowed — mutations are blocked. Outputs: `{output_key}` (rows or single row), `{output_key}_count`.
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"query": "SELECT status, COUNT(*) as count FROM veeam_backup_agent_jobs WHERE LOWER(name) LIKE LOWER($1) AND last_run > NOW() - INTERVAL '7 days' GROUP BY status",
|
||
|
|
"params": ["%{{context.device_hostname}}%"],
|
||
|
|
"output_key": "backup_trend",
|
||
|
|
"single_row": false
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Autotask Actions
|
||
|
|
|
||
|
|
#### `create_ticket`
|
||
|
|
Create a ticket in Autotask via the API. Numeric fields (`companyID`, `ticketType`, `priority`, `queueID`, etc.) are auto-converted. Outputs: `ticket_id`, `ticket_number`, `created_ticket`.
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"template": {
|
||
|
|
"title": "[RMM {{context.alert_type}}] {{context.device_hostname}}",
|
||
|
|
"description": "Alert: {{context.alert_message}}\nDevice: {{context.device_hostname}}\nSite: {{context.site_name}}",
|
||
|
|
"companyID": "{{context.company_id}}",
|
||
|
|
"ticketType": 2,
|
||
|
|
"priority": 1,
|
||
|
|
"queueID": 29682833,
|
||
|
|
"status": 1
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `update_ticket`
|
||
|
|
Update fields on an existing Autotask ticket.
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"ticket_id": "{{context.ticket_id}}",
|
||
|
|
"fields": {
|
||
|
|
"priority": 4,
|
||
|
|
"queueID": 29682833
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `create_note`
|
||
|
|
Add an internal note to an Autotask ticket.
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"ticket_id": "{{context.ticket_id}}",
|
||
|
|
"title": "Pipeline Note",
|
||
|
|
"body": "AI Analysis:\n{{context.ai_response}}",
|
||
|
|
"note_type": 1,
|
||
|
|
"publish": 1
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### AI
|
||
|
|
|
||
|
|
#### `ai_analyze`
|
||
|
|
Send a prompt to OpenAI or Anthropic. Uses AI settings from `workflow_settings` table. Outputs: `ai_response`, `ai_provider`, `ai_model`.
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"system_prompt": "You are an IT operations assistant.",
|
||
|
|
"prompt": "Summarize this RMM alert for a technician:\n\nType: {{context.alert_type}}\nDevice: {{context.device_hostname}}\nMessage: {{context.alert_message}}",
|
||
|
|
"provider": "openai",
|
||
|
|
"model": "gpt-4o",
|
||
|
|
"max_tokens": 500
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Optionally reference a saved prompt template:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"prompt_template_id": 1,
|
||
|
|
"prompt": "..."
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Notifications
|
||
|
|
|
||
|
|
#### `notify`
|
||
|
|
Send a notification to a configured channel. The channel is referenced by `channel_id` (from the Notification Channels UI).
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"channel_id": 1,
|
||
|
|
"message": "RMM Alert: {{context.device_hostname}} - {{context.alert_message}}",
|
||
|
|
"title": "RMM Alert"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
For Teams, you can provide a custom Adaptive Card:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"channel_id": 1,
|
||
|
|
"card_template": {
|
||
|
|
"type": "message",
|
||
|
|
"attachments": [{ "contentType": "application/vnd.microsoft.card.adaptive", "content": { ... } }]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
For ntfy, you can set priority and title:
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"channel_id": 2,
|
||
|
|
"message": "Alert on {{context.device_hostname}}",
|
||
|
|
"title": "Critical Alert",
|
||
|
|
"priority": "urgent"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `approval`
|
||
|
|
Send an approval request and **pause the pipeline** until a human responds. The response comes via a callback URL.
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"channel_id": 1,
|
||
|
|
"message": "Auto-remediate {{context.device_hostname}}?",
|
||
|
|
"options": ["Approve", "Reject", "Escalate"],
|
||
|
|
"timeout_min": 60
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
When the approval is sent:
|
||
|
|
- **Teams** — Adaptive Card with action buttons (each opens the callback URL)
|
||
|
|
- **Telegram** — Message with inline keyboard buttons
|
||
|
|
- **ntfy** — Push notification with action buttons
|
||
|
|
|
||
|
|
The callback URL is `POST /api/pipelines/approval/{approval_id}?response=Approve`. After the response, the pipeline resumes with `context.approval_result` containing the response data.
|
||
|
|
|
||
|
|
### RMM Actions
|
||
|
|
|
||
|
|
#### `rmm_quick_job`
|
||
|
|
Run a Datto RMM quick job (automation component) on a device. Outputs: `quick_job_result`, `job_uid`.
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"device_uid": "{{context.device_uid}}",
|
||
|
|
"component_uid": "comp-dns-flush-001",
|
||
|
|
"job_name": "DNS Cache Flush",
|
||
|
|
"variables": [
|
||
|
|
{ "name": "LogPath", "value": "C:\\Logs" }
|
||
|
|
]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
To find available components, use `GET /api/rmm/components`.
|
||
|
|
|
||
|
|
#### `rmm_get_job_results`
|
||
|
|
Poll for quick job results. Outputs: `job_results`, `job_status`.
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"job_uid": "{{context.job_uid}}",
|
||
|
|
"device_uid": "{{context.device_uid}}"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Failure Handling
|
||
|
|
|
||
|
|
Each step has an `on_failure` setting:
|
||
|
|
|
||
|
|
| Value | Behavior |
|
||
|
|
|-------|----------|
|
||
|
|
| `stop` | Stop the pipeline, mark as failed (default) |
|
||
|
|
| `continue` | Log the error and continue to the next step |
|
||
|
|
| `skip_to` | Jump to a specific step number |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## API Reference
|
||
|
|
|
||
|
|
### Pipelines
|
||
|
|
|
||
|
|
| Method | Endpoint | Description |
|
||
|
|
|--------|----------|-------------|
|
||
|
|
| `GET` | `/api/pipelines` | List all pipelines (optional `?source=datto_rmm`) |
|
||
|
|
| `POST` | `/api/pipelines` | Create a pipeline |
|
||
|
|
| `GET` | `/api/pipelines/{id}` | Get pipeline with steps and recent executions |
|
||
|
|
| `PUT` | `/api/pipelines/{id}` | Update pipeline settings |
|
||
|
|
| `DELETE` | `/api/pipelines/{id}` | Delete pipeline and all steps |
|
||
|
|
| `GET` | `/api/pipelines/{id}/steps` | List steps |
|
||
|
|
| `POST` | `/api/pipelines/{id}/steps` | Add a step |
|
||
|
|
| `PUT` | `/api/pipelines/{id}/steps` | Replace all steps (body: `{ steps: [...] }`) |
|
||
|
|
| `GET` | `/api/pipelines/{id}/executions` | Execution history (optional `?limit=50`) |
|
||
|
|
| `POST` | `/api/pipelines/{id}/test` | Test with sample payload (body: `{ payload: {...} }`) |
|
||
|
|
|
||
|
|
### Notification Channels
|
||
|
|
|
||
|
|
| Method | Endpoint | Description |
|
||
|
|
|--------|----------|-------------|
|
||
|
|
| `GET` | `/api/notification-channels` | List all channels |
|
||
|
|
| `POST` | `/api/notification-channels` | Create a channel |
|
||
|
|
| `GET` | `/api/notification-channels/{id}` | Get channel |
|
||
|
|
| `PUT` | `/api/notification-channels/{id}` | Update channel |
|
||
|
|
| `DELETE` | `/api/notification-channels/{id}` | Delete channel |
|
||
|
|
| `POST` | `/api/notification-channels/{id}/test` | Send test notification |
|
||
|
|
|
||
|
|
### Approval Callback
|
||
|
|
|
||
|
|
| Method | Endpoint | Description |
|
||
|
|
|--------|----------|-------------|
|
||
|
|
| `GET/POST` | `/api/pipelines/approval/{id}?response=Approve` | Respond to approval request |
|
||
|
|
|
||
|
|
Query params: `response` (required), `by` (optional — who approved).
|
||
|
|
|
||
|
|
### RMM Components
|
||
|
|
|
||
|
|
| Method | Endpoint | Description |
|
||
|
|
|--------|----------|-------------|
|
||
|
|
| `GET` | `/api/rmm/components` | List available Datto RMM automation components |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## UI Pages
|
||
|
|
|
||
|
|
### Workflow Dashboard (`/admin/workflow`)
|
||
|
|
The main workflow page now includes two new navigation cards:
|
||
|
|
- **Webhook Pipelines** — manage automation pipelines
|
||
|
|
- **Notification Channels** — configure notification destinations
|
||
|
|
|
||
|
|
### Pipeline List (`/admin/workflow/pipelines`)
|
||
|
|
- View all pipelines with trigger source, step count, and active status
|
||
|
|
- Toggle pipelines on/off
|
||
|
|
- Create new pipelines
|
||
|
|
- Delete pipelines
|
||
|
|
|
||
|
|
### Pipeline Editor (`/admin/workflow/pipelines/{id}`)
|
||
|
|
Four tabs:
|
||
|
|
- **Steps** — visual step builder with drag ordering, inline JSON config editor, add/remove/reorder steps
|
||
|
|
- **Trigger** — edit pipeline name, description, trigger source, and trigger conditions (JSON)
|
||
|
|
- **Test** — paste a sample payload and run the pipeline in real-time, see step-by-step results
|
||
|
|
- **History** — view recent execution results with status and timing
|
||
|
|
|
||
|
|
### Notification Channels (`/admin/workflow/channels`)
|
||
|
|
- Add channels: Teams (webhook URL), Telegram (bot token + chat ID), ntfy (topic + server), Generic Webhook (URL + method)
|
||
|
|
- Edit and delete channels
|
||
|
|
- **Test button** — sends a test notification to verify the channel works
|
||
|
|
- Toggle channels active/inactive
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Database Schema
|
||
|
|
|
||
|
|
### `notification_channels`
|
||
|
|
| Column | Type | Description |
|
||
|
|
|--------|------|-------------|
|
||
|
|
| `id` | SERIAL PK | |
|
||
|
|
| `name` | VARCHAR(200) | Human-readable name |
|
||
|
|
| `channel_type` | VARCHAR(20) | `teams`, `telegram`, `ntfy`, `webhook` |
|
||
|
|
| `config` | JSONB | Type-specific credentials and settings |
|
||
|
|
| `is_active` | BOOLEAN | |
|
||
|
|
| `created_at` / `updated_at` | TIMESTAMP | |
|
||
|
|
|
||
|
|
### `webhook_pipelines`
|
||
|
|
| Column | Type | Description |
|
||
|
|
|--------|------|-------------|
|
||
|
|
| `id` | SERIAL PK | |
|
||
|
|
| `name` | VARCHAR(200) | |
|
||
|
|
| `description` | TEXT | |
|
||
|
|
| `is_active` | BOOLEAN | |
|
||
|
|
| `trigger_source` | VARCHAR(50) | `datto_rmm`, `autotask`, `veeam`, `manual` |
|
||
|
|
| `trigger_conditions` | JSONB | Array of `{field, operator, value}` |
|
||
|
|
| `sort_order` | INTEGER | Lower = higher priority |
|
||
|
|
| `created_at` / `updated_at` | TIMESTAMP | |
|
||
|
|
|
||
|
|
### `pipeline_steps`
|
||
|
|
| Column | Type | Description |
|
||
|
|
|--------|------|-------------|
|
||
|
|
| `id` | SERIAL PK | |
|
||
|
|
| `pipeline_id` | FK → webhook_pipelines | |
|
||
|
|
| `step_order` | INTEGER | Execution order |
|
||
|
|
| `step_type` | VARCHAR(50) | See step types above |
|
||
|
|
| `name` | VARCHAR(200) | Human label |
|
||
|
|
| `config` | JSONB | Step-specific configuration |
|
||
|
|
| `on_failure` | VARCHAR(20) | `stop`, `continue`, `skip_to` |
|
||
|
|
| `skip_to_step` | INTEGER | Target step for `skip_to` |
|
||
|
|
| `is_active` | BOOLEAN | |
|
||
|
|
| `timeout_ms` | INTEGER | Max wait for approval steps |
|
||
|
|
|
||
|
|
### `pipeline_executions`
|
||
|
|
| Column | Type | Description |
|
||
|
|
|--------|------|-------------|
|
||
|
|
| `id` | SERIAL PK | |
|
||
|
|
| `pipeline_id` | FK | |
|
||
|
|
| `trigger_source` | VARCHAR(50) | |
|
||
|
|
| `trigger_payload` | JSONB | Raw webhook data |
|
||
|
|
| `status` | VARCHAR(20) | `pending`, `running`, `waiting`, `completed`, `failed`, `skipped` |
|
||
|
|
| `current_step` | INTEGER | |
|
||
|
|
| `context` | JSONB | Accumulated data from all steps |
|
||
|
|
| `started_at` / `completed_at` | TIMESTAMP | |
|
||
|
|
| `duration_ms` | INTEGER | |
|
||
|
|
| `error_message` | TEXT | |
|
||
|
|
|
||
|
|
### `pipeline_execution_steps`
|
||
|
|
| Column | Type | Description |
|
||
|
|
|--------|------|-------------|
|
||
|
|
| `id` | SERIAL PK | |
|
||
|
|
| `execution_id` | FK | |
|
||
|
|
| `step_order` | INTEGER | |
|
||
|
|
| `step_type` | VARCHAR(50) | |
|
||
|
|
| `step_name` | VARCHAR(200) | |
|
||
|
|
| `status` | VARCHAR(20) | `pending`, `running`, `completed`, `failed`, `waiting`, `skipped` |
|
||
|
|
| `input_data` / `output_data` | JSONB | |
|
||
|
|
| `started_at` / `completed_at` | TIMESTAMP | |
|
||
|
|
| `duration_ms` | INTEGER | |
|
||
|
|
| `error_message` | TEXT | |
|
||
|
|
|
||
|
|
### `approval_requests`
|
||
|
|
| Column | Type | Description |
|
||
|
|
|--------|------|-------------|
|
||
|
|
| `id` | SERIAL PK | |
|
||
|
|
| `execution_id` | FK | |
|
||
|
|
| `step_order` | INTEGER | |
|
||
|
|
| `channel_id` | FK → notification_channels | |
|
||
|
|
| `message` | TEXT | |
|
||
|
|
| `options` | JSONB | e.g., `["Approve", "Reject"]` |
|
||
|
|
| `status` | VARCHAR(20) | `pending`, `approved`, `rejected`, `timeout` |
|
||
|
|
| `responded_by` | TEXT | |
|
||
|
|
| `responded_at` | TIMESTAMP | |
|
||
|
|
| `response_data` | JSONB | |
|
||
|
|
| `expires_at` | TIMESTAMP | |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Example 1: Simple RMM Alert → Ticket
|
||
|
|
|
||
|
|
```
|
||
|
|
Pipeline: "RMM Alert → Autotask Ticket"
|
||
|
|
Trigger: datto_rmm WHERE triggered = "True"
|
||
|
|
|
||
|
|
Step 1: transform → Extract device_uid, site_name, alert_message from payload
|
||
|
|
Step 2: enrich_company → Lookup Autotask company from site_name
|
||
|
|
Step 3: create_ticket → Create Autotask ticket with alert details
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Example 2: Veeam Backup Failure → Full Diagnostic Pipeline
|
||
|
|
|
||
|
|
This is the flagship pipeline demonstrating the full power of the engine. When a Veeam backup failure alert arrives from Datto RMM, it:
|
||
|
|
|
||
|
|
1. Extracts and enriches from multiple sources (RMM, VSPC, local DB)
|
||
|
|
2. Runs a diagnostic PowerShell script on the affected device
|
||
|
|
3. Feeds everything to AI for root cause analysis
|
||
|
|
4. Creates a rich Autotask ticket with all findings
|
||
|
|
5. Notifies the team via Teams
|
||
|
|
|
||
|
|
```
|
||
|
|
Pipeline: "Veeam Backup Failure → Smart Diagnostic Ticket"
|
||
|
|
Trigger: datto_rmm WHERE triggered = "True" AND alert_message contains "Veeam"
|
||
|
|
|
||
|
|
Step 1: transform → Extract device_hostname, device_uid, site_name, alert fields
|
||
|
|
Step 2: enrich_device → Lookup full device details from datto_rmm_devices
|
||
|
|
Step 3: enrich_company → Lookup Autotask company from site name
|
||
|
|
Step 4: enrich_vspc → Query VSPC for backup agent jobs, server jobs,
|
||
|
|
protected workloads, alarms, last success date
|
||
|
|
Step 5: db_query → Backup failure trend: job status counts over last 7 days
|
||
|
|
Step 6: db_query → Recent RMM alerts for this device (pattern detection)
|
||
|
|
Step 7: rmm_quick_job → Run Veeam diagnostic PowerShell script on device:
|
||
|
|
- Check Veeam services (running/stopped)
|
||
|
|
- Check backup job status via VBR snap-in
|
||
|
|
- Check disk space on all drives
|
||
|
|
- Check Windows Event Log for Veeam errors (48h)
|
||
|
|
- Check for stuck Veeam processes (>48h)
|
||
|
|
- Test network connectivity to backup infrastructure
|
||
|
|
Step 8: delay → Wait 60s for script execution
|
||
|
|
Step 9: rmm_get_job_results → Retrieve structured JSON diagnostic output
|
||
|
|
Step 10: ai_analyze → Feed ALL data to AI:
|
||
|
|
"Given the alert, VSPC status, backup trends,
|
||
|
|
recent alerts, and on-device diagnostics —
|
||
|
|
what is the root cause? Is it recurring?
|
||
|
|
What are the remediation steps?"
|
||
|
|
Step 11: create_ticket → Create rich Autotask ticket with:
|
||
|
|
- VSPC backup status summary
|
||
|
|
- AI root cause analysis
|
||
|
|
- On-device diagnostic results
|
||
|
|
- Backup trend data
|
||
|
|
- Recent alert history
|
||
|
|
Step 12: create_note → Add AI analysis as internal ticket note
|
||
|
|
Step 13: notify (Teams) → Adaptive Card with summary + ticket link
|
||
|
|
```
|
||
|
|
|
||
|
|
### What the ticket looks like
|
||
|
|
|
||
|
|
Instead of the generic alert ticket:
|
||
|
|
> "A Veeam Backup & Replication monitoring policy reported a backup job as missing or stalled for device pgbvsywnp01."
|
||
|
|
|
||
|
|
The pipeline produces a ticket like:
|
||
|
|
|
||
|
|
> **[Veeam Backup Failure] pgbvsywnp01 - V-Systems - Main Office**
|
||
|
|
>
|
||
|
|
> ## VSPC Backup Status
|
||
|
|
> Agent Jobs: 2 (1 success, 1 failed, 0 warning)
|
||
|
|
> Latest Job: "pgbvsywnp01 Backup" — Failed at 2026-02-20 19:30:00
|
||
|
|
> Failure: "Failed to process disk 0 of VM. Error: The backup infrastructure..."
|
||
|
|
> Last Success: 2026-02-19 03:15:00 (40h ago)
|
||
|
|
> Active Alarms: 1
|
||
|
|
>
|
||
|
|
> ## AI Root Cause Analysis
|
||
|
|
> **Root Cause:** The Veeam Backup Service (VeeamBackupSvc) is stopped on the device.
|
||
|
|
> This was likely caused by a Windows Update that restarted the server but the
|
||
|
|
> Veeam services did not auto-start due to a delayed start configuration...
|
||
|
|
>
|
||
|
|
> **Remediation Steps:**
|
||
|
|
> 1. Start the VeeamBackupSvc service
|
||
|
|
> 2. Set startup type to Automatic (not Delayed Start)
|
||
|
|
> 3. Trigger a manual backup run to verify
|
||
|
|
> 4. Monitor for 24h to confirm resolution
|
||
|
|
>
|
||
|
|
> ## On-Device Diagnostics
|
||
|
|
> - Services: VeeamBackupSvc STOPPED, VeeamBrokerSvc Running
|
||
|
|
> - Disk: C: 45% used (55GB free), D: 78% used (220GB free)
|
||
|
|
> - Event Log: 3 Veeam errors in last 48h
|
||
|
|
> - Network: SQL server reachable, REST API port open
|
||
|
|
|
||
|
|
### Diagnostic PowerShell Script
|
||
|
|
|
||
|
|
The script at `scripts/rmm-diagnostics/veeam-backup-diagnostic.ps1` must be uploaded to Datto RMM as a component. It checks:
|
||
|
|
|
||
|
|
| Check | What it does |
|
||
|
|
|-------|-------------|
|
||
|
|
| **Veeam Services** | Checks 11 Veeam service names, reports stopped critical services |
|
||
|
|
| **Backup Jobs** | Loads VBR PowerShell snap-in, gets all jobs with last session status |
|
||
|
|
| **Disk Space** | All fixed drives, flags >90% used |
|
||
|
|
| **Event Logs** | Veeam Backup, Veeam Agent, Application log — errors in last 48h |
|
||
|
|
| **Processes** | Running Veeam processes, flags any >48h (stuck) |
|
||
|
|
| **Network** | Tests SQL server connectivity, Veeam service ports (9392, 9419, 6180) |
|
||
|
|
|
||
|
|
Output is structured JSON with `checks`, `issues_found`, `recommendations`, and a `severity` rating (OK/WARNING/CRITICAL).
|
||
|
|
|
||
|
|
### Activation Steps
|
||
|
|
|
||
|
|
1. Upload `veeam-backup-diagnostic.ps1` to Datto RMM as a component
|
||
|
|
2. Copy the component UID
|
||
|
|
3. Edit pipeline step 7 → replace `REPLACE_WITH_COMPONENT_UID` with the real UID
|
||
|
|
4. Create a notification channel (Teams webhook) at `/admin/workflow/channels`
|
||
|
|
5. Update step 13 `channel_id` to match
|
||
|
|
6. Toggle the pipeline active
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## File Structure
|
||
|
|
|
||
|
|
```
|
||
|
|
lib/
|
||
|
|
types/
|
||
|
|
pipeline.ts # TypeScript types
|
||
|
|
services/
|
||
|
|
pipeline-engine.ts # Core engine
|
||
|
|
pipeline-steps/
|
||
|
|
index.ts # Registers all executors
|
||
|
|
filter.ts
|
||
|
|
transform.ts
|
||
|
|
set-variable.ts
|
||
|
|
delay.ts
|
||
|
|
enrich-device.ts
|
||
|
|
enrich-company.ts
|
||
|
|
enrich-ticket.ts
|
||
|
|
enrich-vspc.ts # Veeam VSPC backup status lookup
|
||
|
|
db-query.ts # Parameterized SQL queries
|
||
|
|
create-ticket.ts
|
||
|
|
update-ticket.ts
|
||
|
|
create-note.ts
|
||
|
|
ai-analyze.ts
|
||
|
|
notify.ts
|
||
|
|
approval.ts
|
||
|
|
rmm-quick-job.ts
|
||
|
|
|
||
|
|
scripts/
|
||
|
|
rmm-diagnostics/
|
||
|
|
veeam-backup-diagnostic.ps1 # Veeam diagnostic script for RMM quick job
|
||
|
|
|
||
|
|
app/
|
||
|
|
api/
|
||
|
|
pipelines/
|
||
|
|
route.ts # List + create pipelines
|
||
|
|
[id]/
|
||
|
|
route.ts # Get/update/delete pipeline
|
||
|
|
steps/route.ts # Manage steps
|
||
|
|
executions/route.ts # Execution history
|
||
|
|
test/route.ts # Test with sample payload
|
||
|
|
approval/
|
||
|
|
[id]/route.ts # Approval callback
|
||
|
|
notification-channels/
|
||
|
|
route.ts # List + create channels
|
||
|
|
[id]/
|
||
|
|
route.ts # Get/update/delete channel
|
||
|
|
test/route.ts # Send test notification
|
||
|
|
rmm/
|
||
|
|
components/route.ts # List RMM components
|
||
|
|
admin/
|
||
|
|
workflow/
|
||
|
|
pipelines/
|
||
|
|
page.tsx # Pipeline list
|
||
|
|
[id]/page.tsx # Pipeline editor
|
||
|
|
channels/
|
||
|
|
page.tsx # Channel management
|
||
|
|
|
||
|
|
components/
|
||
|
|
admin/
|
||
|
|
pipeline/
|
||
|
|
StepConfigEditor.tsx # Visual step config editors
|
||
|
|
|
||
|
|
migrations/
|
||
|
|
033_create_pipeline_engine_tables.sql
|
||
|
|
034_seed_veeam_backup_failure_pipeline.sql
|
||
|
|
```
|