Back to Novu

Add a Channel's Setup Guide (Layer 1)

.cursor/skills/add-channel-setup-guide/SKILL.md

3.18.06.1 KB
Original Source

Add a Channel's Setup Guide (Layer 1)

Layer 1 = connecting the channel itself: the numbered stepper a developer follows to create the provider app/bot, save its credentials in Novu, install/verify, and send a first message — with a live "Listening… / Connected" indicator. ResolveAgentIntegrationGuide renders this <Channel>SetupGuide as the setup view until the integration is connected, then swaps to the connected/"what's next" view (separate add-channel-whats-next-onboarding skill).

Files live in apps/dashboard/src/components/agents/. Siblings: slack-setup-guide.tsx (quick/manual modes + manifest), teams-setup-guide.tsx, telegram-setup-guide.tsx (simplest), whatsapp-setup-guide.tsx.

How it works

<Channel>SetupGuide
  ├─ useFetchIntegrations()         → find integration by _id + providerId
  ├─ SetupStepperRail
  │    └─ SetupStep × N             → create app · save creds · install/verify
  │         └─ SetupButton / IntegrationCredentialsSidebar trigger
  ├─ ListeningStatus                → polls listAgentIntegrations every 1s;
  │                                    fires onConnected + confetti when connectedAt is set
  └─ IntegrationCredentialsSidebar  → generic credential save (useUpdateIntegration)

Connection is detected, not asserted. A channel is "connected" only when the backend sets connectedAt on the integration link (on the first real inbound message). Saving credentials or finishing OAuth/install does not mean connected — keep those as separate local states.

Building blocks

All from setup-guide-primitives.tsx and setup-guide-step-utils.ts:

SymbolRole
SetupStepperRailVertical numbered rail wrapping the steps column
SetupStepOne step: index, status, title, description, rightContent?, extraContent?, fullWidthContent?, headerSlot?, dimmed?, sectionLabel?, inlineSectionLabel?
SetupButtonSecondary outline action; href opens a new tab, else onClick; supports leadingIcon, disabled
SetupModeToggle + SetupMode ('quick' | 'manual')Optional dual-path setup (Slack)
IntegrationCredentialsSidebarDrawer that saves provider credentials via the generic integration form; onSaveSuccess, agentOnboarding
ListeningStatusPolls listAgentIntegrations; fires onConnected + confetti on connectedAt
deriveStepStatus(i, firstIncomplete)'completed' | 'current' | 'upcoming'
hasIntegrationCredentials(credentials)True once any string credential is saved

Step 0 — Prerequisites

  • ChatProviderIdEnum.<Channel> exists in @novu/shared, and the provider exists in packages/providers (brand-new providers are ask-first).
  • The integration can be created/selected (the "add provider" flow passes you an integrationId).

File checklist

Copy the closest sibling, then rename. Full template: see reference.md.

  • CREATE apps/dashboard/src/components/agents/<channel>-setup-guide.tsx — export <Channel>SetupGuide
  • EDIT agent-integration-guides/resolve-agent-integration-guide.tsx — in the setup switch, render <Channel>SetupGuide ... embedded /> and set setupDisplayName (the add-channel-whats-next-onboarding skill covers the rest of this resolver)
  • (optional) Add a provider-specific server action in @/api/agents or @/api/integrations only if the channel needs webhook registration / quick-setup / subscriber-link beyond a plain credential save

Component contract

Props: { agent: AgentResponse; integrationId: string; stepOffset?: number; onStepsCompleted?: () => void; embedded?: boolean } (stepOffset defaults to 1; Overview mounts the guide at a higher base, the Integrations detail page at 1).

State machine:

  1. Resolve selectedIntegration from useFetchIntegrations() by _id === integrationId && providerId === ChatProviderIdEnum.<Channel>.
  2. Track progress: hasIntegrationCredentials(...) ‖ a local credentialsSavedLocally, plus any install/connected flags. Reset all of it in useEffect([integrationId]).
  3. Derive const base = stepOffset → compute firstIncompleteStepderiveStepStatus(stepIndex, firstIncompleteStep) per SetupStep.
  4. Render the steps in SetupStepperRail, then ListeningStatus (in a pl-8 wrapper), then IntegrationCredentialsSidebar.
  5. ListeningStatus.onConnected → mark connected and call onStepsCompleted?.().
  6. Provide both an embedded return (no Overview chrome — used by the resolver) and the standalone return.

Typical 3-step recipe

  1. Create the app/botSetupButton href= the provider console. Optional: a manifest (CodeBlock, escape injected values) or a quick-setup input that calls a server action.
  2. Save credentials in NovuSetupButton onClick={() => setIsCredentialsSidebarOpen(true)}; the sidebar's onSaveSuccess flips credentialsSavedLocally (and may trigger a provider action like webhook registration).
  3. Install / verify + send first message — a provider connect button or copyable instructions; ListeningStatus watches for connectedAt.

Conventions & gotchas

  • Keep installed/credentialed and connected as distinct states — only connectedAt advances the final step (see the Slack guide's comments).
  • Server state through TanStack Query; after a mutation, invalidate getAgentIntegrationsQueryKey(currentEnvironment?._id, agent.identifier).
  • Escape any value injected into a manifest/snippet (e.g. Slack YAML double-quoted strings).
  • Reset local state when integrationId changes so switching integrations doesn't leak progress.
  • Novu/dashboard conventions: type (not interface) on the frontend, named exports, blank line before every return, no nested ternaries, animations from motion/react. Don't build/start the dashboard (port 4201) — check types via diagnostics.

Build & verify

  1. Don't start the dashboard — check types via Cursor diagnostics.
  2. From the agent Integrations tab, add/open a <Channel> integration: confirm steps advance as credentials save, the credential drawer opens/saves, and "Listening…" flips to "Connected" with confetti once a real message lands.