# PRD: Integration Workflow Modules - **Status:** Draft - **Owner:** Robert Isaacs - **Created:** 2026-06-12 - **Design:** `../2026-06-12-integration-workflow-modules-design.md` (architecture authority) - **Branch:** `feature/integration-workflow-modules` off `main` ## 1. Problem statement & user value The workflow designer can host integration-specific "app" modules — palette tiles exposing an integration's operations as typed workflow actions, shown only when that integration is connected — but exactly one exists (NinjaOne, 6 actions), its availability check is a hardcoded if-chain, and its module wiring is inlined in `core.ts`. Meanwhile the operations MSPs actually orchestrate across tools (run a script on an endpoint, trigger an RMM automation, resolve a security incident, post to a Teams channel, put a technician on the schedule) are absent from the palette, so the "self-healing alert" loop the RMM alert events enable cannot be finished inside Alga. This project generalizes the module plumbing so each integration is one self-contained registration, then ships four new modules (Tactical RMM, Level, Huntress, Microsoft Teams), expands NinjaOne with script execution, and adds a core `scheduling.create_entry` action. Incumbent PSAs offer no user-composable canvas over third-party tools; the market's answer is a separate $500–1,500/mo orchestration product. This puts it in the PSA. ## 2. Goals - Adding integration #6 requires one file plus one `core.ts` line — no framework edits, no availability if-chain. - A connected Tactical RMM tenant can run a stored script or raw command on an agent from a workflow and use its output in later steps. - A connected Level tenant can trigger a Level automation (optionally scoped to specific devices) and check its run status. - A connected Huntress tenant can enrich tickets from incident/agent/org data and resolve the Huntress incident when work completes. - A tenant with the Teams app active can notify a user's activity feed, DM a user via the bot, and post to a channel where the app is installed. - Workflows can create dispatch-board schedule entries that ride the existing calendar sync to a technician's connected external calendar. - Palette tiles appear if and only if the integration is available for the tenant; NinjaOne behavior is unchanged (parity regression). ## 3. Non-goals - QuickBooks Online / Stripe modules (financial follow-up, deliberately deferred), Tanium (pre-release). - Email app tile (`email.send` already covers tenant-provider send) and a direct Calendar app tile (schedule-entry approach chosen instead). - Huntress remediation approve/reject (sharp edge; revisit on demand). - DB-backed module catalog or per-tenant module curation. - Prebuilt workflow recipe gallery (launch follow-up, separate effort). - New vendor capabilities beyond those listed (e.g. Tactical service control, NinjaOne patch apply). ## 4. Personas & primary flows - **Automation-minded MSP engineer:** builds "disk-full self-remediation": RMM alert trigger → `tacticalrmm.agents.run_script` (cleanup script) → ticket note with output → `levelio.alerts.resolve` / alert reset → ticket closes. Builds "Huntress enrichment": ticket-created trigger → `huntress.incidents.get` + `huntress.agents.get` → ticket fields/note → on close, `huntress.incidents.resolve`. - **Dispatcher:** escalation workflow posts to the service-desk Teams channel and creates a schedule entry for the on-call technician. - **Tenant admin:** connects/disconnects integrations and sees palette tiles appear/disappear accordingly; no configuration beyond the integration itself. ## 5. Framework changes (build first) ### 5.1 Availability resolver registry New registry in `shared/workflow/runtime/registries/` (sibling of `integrationModuleRegistry.ts`): resolver type `(knex, tenantId) => Promise` registered under a module's `availabilityKey`. `loadAvailableFirstPartyIntegrationAppKeys` (`ee/packages/workflows/src/actions/workflow-runtime-v2-actions.ts`) replaces its if-chain with registry lookups; a module whose key has no registered resolver is **not** available (fail closed). Resolver errors are caught and treated as unavailable (palette listing must not 500 because one vendor table is missing). ### 5.2 RMM availability factory `rmmIntegrationAvailability(provider)` returns a resolver checking `rmm_integrations` for `(tenant, provider)` with `is_active = true` and `connected_at IS NOT NULL` — used by all four RMM modules. ### 5.3 One-call module registration + NinjaOne migration `registerIntegrationWorkflowModule({ module, availability, registerActions })` in `ee/packages/workflows`: registers actions, the module tile, and the availability resolver, idempotently. `core.ts` becomes one call per integration. NinjaOne migrates onto the helper and the RMM factory; the hardcoded `'rmm:ninjaone'` branch is deleted. Regression bar: identical palette/catalog output for connected and disconnected NinjaOne tenants. ## 6. Modules Conventions for every action: `provider.noun.verb` ID, Zod input/output schemas, `ui: { label, description, category, icon }`, correct `sideEffectful`, `idempotency: { mode: 'engineProvided' }`, handler errors thrown (engine normalizes). Vendor endpoint paths marked *verify* must be confirmed against vendor docs during implementation. ### 6.1 NinjaOne expansion (`app:ninjaone`) `FetchNinjaOneWorkflowClient` gains `runScript` (`POST /v2/device/{id}/script/run`; type SCRIPT|ACTION, parameters, runAs) and a scripting-options discovery call (*verify*, expected `GET /v2/device/{id}/scripting/options`). New actions `ninjaone.devices.run_script` (side-effectful) and `ninjaone.devices.scripting_options` (read) join the existing six in `allowedActionIds`. ### 6.2 Tactical RMM (`app:tacticalrmm`) Client: extend `TacticalRmmClient` (`packages/integrations/src/lib/rmm/tacticalrmm/tacticalApiClient.ts`) with agent-detail, script-list, run-script, run-command, reboot wrappers (*verify* paths: `/scripts/`, `/agents/{agent_id}/runscript/`, `/agents/{agent_id}/cmd/`, `/agents/{agent_id}/reboot/`). The workflows package takes a dependency on `packages/integrations`; runtime support resolves the integration row + tenant secrets (`tacticalrmm_api_key` or Knox trio) exactly as the existing sync path does. Actions: `agents.find`, `agents.get`, `scripts.list`, `agents.run_script` (returns output), `agents.run_command`, `agents.reboot` (last three side-effectful). ### 6.3 Level (`app:levelio`) Client: thin fetch client inside the workflows package (NinjaOne pattern — `ee/server` client not importable). Tenant secret `levelio_api_key`; cursor pagination as in the existing client. Actions: `devices.find`, `devices.get`, `alerts.list_active`, `alerts.resolve` (side-effectful, `POST /v2/alerts/{id}/resolve`), `updates.list`, `automations.list` (automations + their webhook tokens), `automations.trigger` (side-effectful, `POST /v2/automations/webhooks/{token}` with optional `device_ids[]`; actionable error when the automation has no webhook trigger configured in Level), `automations.run_status`. ### 6.4 Huntress (`app:huntress`) Client: thin fetch client in the workflows package; Basic auth from tenant secrets `huntress_api_key`/`huntress_api_secret`; replicate the 60 req/min throttle and 429 backoff of the `ee/server` client. Actions: `incidents.find`, `incidents.get`, `incidents.resolve` (side-effectful; Huntress write API, *verify* exact endpoint), `organizations.list`, `agents.get`, `account.get`. ### 6.5 Microsoft Teams (`app:teams`) Availability: own resolver — `teams_integrations.install_status = 'active'` AND the Teams add-on active for the tenant (mirror the delivery path's checks). The workflows package depends on `ee/packages/microsoft-teams`. Actions (all side-effectful): - `teams.notify_user` — Graph `sendActivityNotification` through the existing delivery path with a generic template; target is an Alga user with a linked Microsoft account; actionable error otherwise. - `teams.send_dm` — proactive Bot Framework message (text + optional card) via stored `teams_conversation_references`; actionable error if the user has never opened the bot. - `teams.post_to_channel` — requires **new** `createConversation` support in `ee/packages/microsoft-teams` (proactive channel conversation via Bot Framework; Graph app-only channel posting is Microsoft-protected, the bot route is deliberate). Works in any channel of a team where the Alga Teams app is installed; actionable error otherwise. ## 7. Core action: `scheduling.create_entry` Shared (CE+EE) action in the `scheduling.*` business-operations family. Inputs: assigned user(s), title, start/end, optional ticket/project link, optional status/notes. Creates a dispatch-board schedule entry through the same model layer the UI uses; the existing calendar sync pushes it outward to a connected user calendar. Validation errors (unknown user, end before start) are explicit. ## 8. Designer surface Icon tokens for `tacticalrmm`, `levelio`, `huntress`, `teams` added to the designer icon set, reusing the integration logos already shipped in the settings UI. Palette grouping/ordering verified for the new `app:*` groups (extend `PALETTE_CATEGORY_ORDER` handling only if required). ## 9. Error handling conventions Handlers throw; the Temporal activity layer normalizes to the runtime error payload and stamps `workflow_action_invocations` FAILED. Vendor HTTP errors surface status + vendor message, never credentials. Most-common failures get explicit messages: integration not connected/inactive; Level automation lacks webhook trigger; Teams user unlinked / no bot conversation / app not in team. Side-effectful actions rely on engine-provided idempotency so retries are safe. ## 10. Testing & rollout Unit: handler tests per integration with mocked clients (NinjaOne handler test pattern); availability-registry and resolver tests; helper idempotency; catalog gating matrix. Regression: NinjaOne palette parity. Manual smoke on the local-test stack: palette gating on connect/disconnect, Tactical mock run-script round-trip, Teams notify/DM/channel in a test tenant, schedule entry on the dispatch board. No migrations and no new tables; all gating is read-path, so rollout is inert until an integration is connected. ## 11. Editor behavior for disconnected integrations Availability gates ADDING, not VIEWING. The designer catalog keeps first-party integration records when the integration is disconnected, flagged `available: false`; extension-app filtering is unchanged. The palette excludes unavailable records (the original hide-when-not-enabled decision stands). Existing steps referencing a disconnected integration keep full group context: the grouped action config section renders with an amber "not connected" banner naming the integration, the step card shows a "Disconnected" badge with an explanatory tooltip, and the input-mapping editor remains fully usable (it always resolved schemas from the unfiltered registry). Publish stays allowed; runs fail with the handlers' actionable INTEGRATION_INACTIVE errors. Publish-time and disconnect-time warnings were considered and deferred.