# 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 ```