Add a new chat channel's layer-1 setup guide — the in-dashboard <Channel>SetupGuide that walks a developer through connecting the channel itself (create app/bot → save credentials → install/verify → send first message) with live connection detection — in apps/dashboard, following the existing Slack, MS Teams, and Telegram guides. Use when a new agent channel needs its numbered setup stepper, credential drawer wiring, and connectedAt polling under components/agents.
76
96%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
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.
<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.
All from setup-guide-primitives.tsx and setup-guide-step-utils.ts:
| Symbol | Role |
|---|---|
SetupStepperRail | Vertical numbered rail wrapping the steps column |
SetupStep | One step: index, status, title, description, rightContent?, extraContent?, fullWidthContent?, headerSlot?, dimmed?, sectionLabel?, inlineSectionLabel? |
SetupButton | Secondary outline action; href opens a new tab, else onClick; supports leadingIcon, disabled |
SetupModeToggle + SetupMode ('quick' | 'manual') | Optional dual-path setup (Slack) |
IntegrationCredentialsSidebar | Drawer that saves provider credentials via the generic integration form; onSaveSuccess, agentOnboarding |
ListeningStatus | Polls listAgentIntegrations; fires onConnected + confetti on connectedAt |
deriveStepStatus(i, firstIncomplete) | → 'completed' | 'current' | 'upcoming' |
hasIntegrationCredentials(credentials) | True once any string credential is saved |
ChatProviderIdEnum.<Channel> exists in @novu/shared, and the provider exists in packages/providers (brand-new providers are ask-first).integrationId).Copy the closest sibling, then rename. Full template: see reference.md.
apps/dashboard/src/components/agents/<channel>-setup-guide.tsx — export <Channel>SetupGuideagent-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)@/api/agents or @/api/integrations only if the channel needs webhook registration / quick-setup / subscriber-link beyond a plain credential saveProps: { 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:
selectedIntegration from useFetchIntegrations() by _id === integrationId && providerId === ChatProviderIdEnum.<Channel>.hasIntegrationCredentials(...) ‖ a local credentialsSavedLocally, plus any install/connected flags. Reset all of it in useEffect([integrationId]).const base = stepOffset → compute firstIncompleteStep → deriveStepStatus(stepIndex, firstIncompleteStep) per SetupStep.SetupStepperRail, then ListeningStatus (in a pl-8 wrapper), then IntegrationCredentialsSidebar.ListeningStatus.onConnected → mark connected and call onStepsCompleted?.().embedded return (no Overview chrome — used by the resolver) and the standalone return.SetupButton href= the provider console. Optional: a manifest (CodeBlock, escape injected values) or a quick-setup input that calls a server action.SetupButton onClick={() => setIsCredentialsSidebarOpen(true)}; the sidebar's onSaveSuccess flips credentialsSavedLocally (and may trigger a provider action like webhook registration).ListeningStatus watches for connectedAt.connectedAt advances the final step (see the Slack guide's comments).getAgentIntegrationsQueryKey(currentEnvironment?._id, agent.identifier).integrationId changes so switching integrations doesn't leak progress.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.<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.67de0fa
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.