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:
| 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 |
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.
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:
- Resolve
selectedIntegration from useFetchIntegrations() by _id === integrationId && providerId === ChatProviderIdEnum.<Channel>.
- Track progress:
hasIntegrationCredentials(...) ‖ a local credentialsSavedLocally, plus any install/connected flags. Reset all of it in useEffect([integrationId]).
- Derive
const base = stepOffset → compute firstIncompleteStep → deriveStepStatus(stepIndex, firstIncompleteStep) per SetupStep.
- Render the steps in
SetupStepperRail, then ListeningStatus (in a pl-8 wrapper), then IntegrationCredentialsSidebar.
ListeningStatus.onConnected → mark connected and call onStepsCompleted?.().
- Provide both an
embedded return (no Overview chrome — used by the resolver) and the standalone return.
Typical 3-step recipe
- Create the app/bot —
SetupButton href= the provider console. Optional: a manifest (CodeBlock, escape injected values) or a quick-setup input that calls a server action.
- Save credentials in Novu —
SetupButton => setIsCredentialsSidebarOpen(true)}; the sidebar's onSaveSuccess flips credentialsSavedLocally (and may trigger a provider action like webhook registration).
- 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
- Don't start the dashboard — check types via Cursor diagnostics.
- 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.
1---2name: add-channel-setup-guide3description: 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.4---56# Add a Channel's Setup Guide (Layer 1)78Layer 1 = connecting the **channel itself**: the numbered stepper a developer follows to9create the provider app/bot, save its credentials in Novu, install/verify, and send a first10message — with a live "Listening… / Connected" indicator. `ResolveAgentIntegrationGuide`11renders this `<Channel>SetupGuide` as the **setup view** until the integration is connected,12then swaps to the connected/"what's next" view (separate `add-channel-whats-next-onboarding` skill).1314Files live in `apps/dashboard/src/components/agents/`. Siblings: `slack-setup-guide.tsx` (quick/manual15modes + manifest), `teams-setup-guide.tsx`, `telegram-setup-guide.tsx` (simplest), `whatsapp-setup-guide.tsx`.1617## How it works1819```20<Channel>SetupGuide21 ├─ useFetchIntegrations() → find integration by _id + providerId22 ├─ SetupStepperRail23 │ └─ SetupStep × N → create app · save creds · install/verify24 │ └─ SetupButton / IntegrationCredentialsSidebar trigger25 ├─ ListeningStatus → polls listAgentIntegrations every 1s;26 │ fires onConnected + confetti when connectedAt is set27 └─ IntegrationCredentialsSidebar → generic credential save (useUpdateIntegration)28```2930**Connection is detected, not asserted.** A channel is "connected" only when the backend sets31`connectedAt` on the integration link (on the first real inbound message). Saving credentials or32finishing OAuth/install does **not** mean connected — keep those as separate local states.3334## Building blocks3536All from `setup-guide-primitives.tsx` and `setup-guide-step-utils.ts`:3738| Symbol | Role |39|---|---|40| `SetupStepperRail` | Vertical numbered rail wrapping the steps column |41| `SetupStep` | One step: `index`, `status`, `title`, `description`, `rightContent?`, `extraContent?`, `fullWidthContent?`, `headerSlot?`, `dimmed?`, `sectionLabel?`, `inlineSectionLabel?` |42| `SetupButton` | Secondary outline action; `href` opens a new tab, else `onClick`; supports `leadingIcon`, `disabled` |43| `SetupModeToggle` + `SetupMode` (`'quick' \| 'manual'`) | Optional dual-path setup (Slack) |44| `IntegrationCredentialsSidebar` | Drawer that saves provider credentials via the generic integration form; `onSaveSuccess`, `agentOnboarding` |45| `ListeningStatus` | Polls `listAgentIntegrations`; fires `onConnected` + confetti on `connectedAt` |46| `deriveStepStatus(i, firstIncomplete)` | → `'completed' \| 'current' \| 'upcoming'` |47| `hasIntegrationCredentials(credentials)` | True once any string credential is saved |4849## Step 0 — Prerequisites5051- `ChatProviderIdEnum.<Channel>` exists in `@novu/shared`, and the provider exists in `packages/providers` (brand-new providers are ask-first).52- The integration can be created/selected (the "add provider" flow passes you an `integrationId`).5354## File checklist5556Copy the closest sibling, then rename. Full template: see [reference.md](reference.md).5758- [ ] CREATE `apps/dashboard/src/components/agents/<channel>-setup-guide.tsx` — export `<Channel>SetupGuide`59- [ ] 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)60- [ ] *(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 save6162## Component contract6364Props: `{ agent: AgentResponse; integrationId: string; stepOffset?: number; onStepsCompleted?: () => void; embedded?: boolean }`65(`stepOffset` defaults to 1; Overview mounts the guide at a higher base, the Integrations detail page at 1).6667State machine:681. Resolve `selectedIntegration` from `useFetchIntegrations()` by `_id === integrationId && providerId === ChatProviderIdEnum.<Channel>`.692. Track progress: `hasIntegrationCredentials(...)` ‖ a local `credentialsSavedLocally`, plus any install/connected flags. Reset all of it in `useEffect([integrationId])`.703. Derive `const base = stepOffset` → compute `firstIncompleteStep` → `deriveStepStatus(stepIndex, firstIncompleteStep)` per `SetupStep`.714. Render the steps in `SetupStepperRail`, then `ListeningStatus` (in a `pl-8` wrapper), then `IntegrationCredentialsSidebar`.725. `ListeningStatus.onConnected` → mark connected and call `onStepsCompleted?.()`.736. Provide both an `embedded` return (no Overview chrome — used by the resolver) and the standalone return.7475## Typical 3-step recipe76771. **Create the app/bot** — `SetupButton href=` the provider console. Optional: a manifest (`CodeBlock`, escape injected values) or a quick-setup input that calls a server action.782. **Save credentials in Novu** — `SetupButton onClick={() => setIsCredentialsSidebarOpen(true)}`; the sidebar's `onSaveSuccess` flips `credentialsSavedLocally` (and may trigger a provider action like webhook registration).793. **Install / verify + send first message** — a provider connect button or copyable instructions; `ListeningStatus` watches for `connectedAt`.8081## Conventions & gotchas8283- Keep **installed/credentialed** and **connected** as distinct states — only `connectedAt` advances the final step (see the Slack guide's comments).84- Server state through TanStack Query; after a mutation, invalidate `getAgentIntegrationsQueryKey(currentEnvironment?._id, agent.identifier)`.85- Escape any value injected into a manifest/snippet (e.g. Slack YAML double-quoted strings).86- Reset local state when `integrationId` changes so switching integrations doesn't leak progress.87- 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.8889## Build & verify90911. Don't start the dashboard — check types via Cursor diagnostics.922. 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.