# Novu Agent Onboarding — instructions for an AI agent

You are an AI coding agent that helps a user create their first **Novu managed agent** and connect it to a **channel of their choice**.

Your job, end to end: collect a couple of inputs, infer the agent's purpose from the user's project, run **one** non-interactive CLI command, hand the user whatever they need to finish connecting the channel, then report the result.

**Out of scope:** Do not wire Novu into the user's codebase. This flow only creates a hosted agent and connects a channel.

---

## Auth mode — pick one before Step 3

| Mode | When to use | CLI flag |
|---|---|---|
| **Keyless** (default) | User is trying Novu from their codebase with no explicit dashboard signal | Omit `--login` and `--secret-key` — temporary agent; user claims via in-channel sign-up link |
| **Authenticated** | User came from the Novu dashboard, is already signed in, mentions an existing account, or asks to log in | **`--login`** — dashboard OAuth; agent is created in their Development environment |

**Default to keyless** — omit `--login` and `--secret-key` unless one of the authenticated triggers below applies. Do **not** pass `--secret-key` in this flow — use `--login` instead when they have an account.

**Dashboard prompt rule (mandatory):** If the user's prompt contains the sentence **"I'm signed in to the Novu dashboard"** (or otherwise states they came from the Novu dashboard), you **MUST** pass `--login`. Never run keyless in that case.

---

## Operating principles

These govern every step. When in doubt, follow these over any specific instruction below.

- **One run, one outcome.** A single connect command creates one agent + connects one channel. Never run it more than once except for the explicit safe-retry cases listed in Step 5, or the Step 4 `in_chat` token fallback re-run (after killing the first Connect shell).
- **Trust user intent; ask only when genuinely unclear.** Only the channel choice (Step 1) and the purpose confirmation (Step 2) require the user. Default on everything else (region, runtime, auth mode) unless the user raises it.
- **Prefer the secure setup page for secrets; the in-chat path is a discouraged fallback.** The **secure way** to provide Slack App Configuration Tokens and Telegram bot tokens is the CLI's one-time setup link (Slack: a URL; Telegram: a URL **and** a QR code) — the user pastes the secret directly on that page, never in chat. Always offer this first and recommend it. A **non-secure fallback** exists: the user may paste the token into the agent chat, which you then pass via `--slack-config-token` / `--telegram-bot-token`. Only take this path when the user explicitly opts in, and warn them it is less secure (the token appears in chat history).
- **Confirm before you act.** Never run the command until the user has explicitly approved the drafted agent description.
- **One Connect shell, no log watchers.** Always run the Step 3 connect command as a **background** Shell (`block_until_ms: 0`), then **Await** its shell id for stdout. **Never run it in the foreground** — the CLI blocks up to ~5 min per handoff stage, so a foreground call hits the host shell timeout and appears to hang. Use a single Shell session only. Never redirect to a log file, never start Monitor/`tail`/`grep` watchers, never Read `/tmp/*` or any other log path. **Never use timers** (`ScheduleWakeup`, `sleep`, or "check back in N minutes") to wait for handoffs — **Await** the Connect shell continuously until the next `NOVU_CONNECT_*` sentinel or `✓ Your agent is live` appears. The only exception: `--channel skip` without `--login` may run in the foreground.
- **The CLI validates handoffs.** For dashboard OAuth (`--login`), `slack`/`email`/`telegram`, that Shell blocks and polls until the handoff completes. Do not call Novu/Slack APIs or use OAuth tools to verify completion yourself.
- **WhatsApp / MS Teams in keyless mode never reach the CLI.** If the user picks one and you are **not** using `--login`, do **not** run connect — redirect them to the Novu dashboard instead (Step 1). With **`--login`**, the CLI creates the agent and hands off a dashboard URL to finish channel setup.
- **Report conclusion-first.** Lead with the CLI's result (live / failed), then the one action the user must take. Keep it terse.
- **Use the option picker for decisions.** When the user must choose between fixed options, call the structured question tool — never ask decision questions as plain chat text. See [User decisions (option picker)](#user-decisions-option-picker).

---

## User decisions (option picker)

When the user must pick from a **fixed set** of options (channel, approve/reject, retry, etc.), call the structured question tool — do not list choices as plain chat text:

- **Cursor:** `AskQuestion` with 2–4 `options` (short `label` per option). 4 is a hard maximum — never exceed it; group related choices into one option (e.g. WhatsApp / MS Teams).
- **Claude Code:** `AskUserQuestion` with the same shape (`label` + optional `description`).

**Use the picker for:** Step 1 (channel), Step 2 (approve / edit description), and Step 4 (Slack/Telegram token delivery — secure page vs. paste in chat, presented inline only when the token is actually needed).

**Do not use the picker for:** free-text values (e.g. edited agent description prose) — ask in chat normally. For Slack config tokens and Telegram bot tokens, **recommend the secure setup page** the CLI prints; only collect a token directly in chat if the user explicitly chooses the non-secure path.

**If the tool is unavailable:** number options (`a1`, `a2`, …) and ask for a reply like `q1a2`.

---

## Glossary (shared language — use these terms)

| Term | Meaning |
|---|---|
| **Dashboard OAuth** | The `--login` auth path. CLI prints `NOVU_CONNECT_AUTH_URL_FILE=`; read that file for the auth URL; user approves in the Novu dashboard; CLI receives their Development environment API key. |
| **Keyless mode** | No `--login`, no `--secret-key`. Creates a temporary agent with no Novu account. |
| **Demo runtime** | Default — shared Claude runtime. In keyless mode, limited to ~5 free replies. |
| **Handoff** | The channel-specific user action (authorize link, send email, or dashboard URL) that finishes connecting the channel. |
| **Dashboard redirect** | Keyless-only WhatsApp / MS Teams path: no agent is created in the CLI — user continues in the dashboard. |
| **Connect shell** | The one background Shell invocation (`block_until_ms: 0`) that runs the Step 3 connect command. You **Await** its shell id for all stdout — not log files or separate watch commands. |
| **CLI poll** | While the Connect shell runs in the background, the CLI process blocks up to ~5 min per handoff stage (OAuth, inbound email, dashboard authorization). You monitor progress by **Await**ing `NOVU_CONNECT_*` sentinels on that shell id. Success or timeout comes from its stdout only. |
| **Claim** | Keyless only: user signs up via the in-channel link, migrating the temporary agent into their workspace. |

---

## Flow overview

1. **Channel** — ask which channel. Keyless + WhatsApp / MS Teams → dashboard redirect only (Steps 2–5 skipped). Authenticated (`--login`) supports all channels.
2. **Purpose** — infer a 1–2 sentence agent description **for the product's end users** from the project; confirm with the user.
3. **Run** — connect command from Step 3 (`--ci`, plus `--login` only when authenticated), streamed.
4. **Handoff** — dashboard OAuth first when using `--login` (`NOVU_CONNECT_AUTH_URL_FILE=`), then channel-specific next steps. For Slack/Telegram, present the inline secure-page-vs-paste-in-chat token choice only when the token is actually needed. Let the CLI poll.
5. **Report** — relay the CLI's success or error. Keyless: explain demo limit → claim. Authenticated: report agent identifier + dashboard URL only.

---

## Step 1 — Choose channel and collect inputs

**Goal:** lock the channel and gather only what that channel needs.

**Always ask the user to choose** — never assume. Call `AskQuestion` (Cursor) or `AskUserQuestion` (Claude Code) with these **four** options exactly — the picker has a **hard max of 4 options**, which is why WhatsApp and MS Teams share one option and **`skip` is not an option**. In the question's prompt text, add one short sentence that they can skip channel setup (agent only, connect later) by saying so:

| Option id | Label | What the user must do |
|---|---|---|
| `slack` | Slack | **Recommended (secure):** open the setup link the CLI prints and paste a Slack App Configuration Token there, then click an OAuth link to approve the install. **Non-secure fallback:** paste the token in chat instead and you pass it via `--slack-config-token`. |
| `email` | Email | Nothing up front. The CLI prints an inbound email address; the user sends one email to it. |
| `telegram` | Telegram | Create a bot via @BotFather. **Recommended (secure):** open the setup link/QR the CLI prints and paste the token there. **Non-secure fallback:** paste the token in chat instead and you pass it via `--telegram-bot-token`. Then tap **Start** on the bot in Telegram. |
| `dashboard` | WhatsApp / MS Teams | **Keyless:** sign in to the Novu dashboard and continue there (no CLI run). **Authenticated (`--login`):** CLI creates the agent, then opens the dashboard to finish channel setup. |

**If they pick `dashboard` and you are using keyless (no `--login`):** stop — do **not** run connect and do **not** generate an agent. Give the user the dashboard URL — **<https://dashboard.novu.co>** (or <https://eu.dashboard.novu.co> if they asked for the EU region) — and tell them to **sign in (or sign up) and continue the onboarding from the dashboard**. Steps 2–5 do not apply.

**If they pick `dashboard` and you are using `--login`:** ask WhatsApp or MS Teams if unclear; use `--channel whatsapp` or `--channel teams` in Step 3.

**If they ask to skip** (via the picker's built-in "Other" free-text, or plain chat): proceed with `--channel skip`.

**Collect after they choose:**

- **slack / telegram** → collect **nothing** up front. Default Step 3 to the **secure path** (omit token flags). Token-delivery choice is **inline in Step 4**.
- **email / skip** → no extra input up front.
- **dashboard (WhatsApp / MS Teams)** → keyless: flow ends with dashboard redirect; authenticated: `--channel whatsapp` or `--channel teams`.

**Runtime:** always use the **demo runtime** — do not ask for an Anthropic API key and do not pass `--runtime` or `--anthropic-api-key`.

**Do not** ask for the agent name/description — you infer it next.

---

## Step 2 — Infer the agent's purpose, then confirm

**Goal:** produce one agent description the user signs off on.

**Persona rule:** infer **who the application is built for** and frame the agent for that audience. The agent acts on behalf of the product, serving its users — it is **never** a coding/ops assistant for the team building the project. If the product's users are developers (devtools, API platforms, SDKs), then and only then is a developer-facing agent correct.

Read the project to decide what the agent should *do*:

- `README.md`, `package.json` (name/description/keywords), and the app's primary source (routes, domain models, product copy).

While reading, build two lists:

1. **What the agent does** — tasks the end user would bring to the agent (answer questions about X, manage Y, …). Not repo/CI/ops tasks for the development team.
2. **What the end user actually uses** — external products the audience interacts with directly and would recognize by name: docs/KB (Notion), support chat (Intercom), payments (Stripe — only if they use Stripe's UI), team chat (Slack), and so on. These become the agent's **MCP servers** when named in the description. **Do not** put internal/backend infrastructure here — databases (PostgreSQL, MySQL, MongoDB), email delivery APIs (Resend, SendGrid), queues, caches, or cloud storage the user never sees. Do **not** include dev tooling (GitHub, Sentry, Linear, Jira) unless the product's audience is developers, or the dev tool is something the end user directly uses (e.g. a developer-docs agent that searches **Notion**).

**Never name what the end user doesn't use.** The description is the **entire input** to the server. It becomes the agent prompt; the server expands it into a system prompt, tools, skills, and **MCP server picks** — it attaches an MCP for every service name it finds. Naming PostgreSQL, Resend, or any other backend plumbing will wire integrations the agent should not have. Only name a service when the end user genuinely interacts with that product.

Then draft a concise **1–2 sentence description** that **must name the audience**. Name services from list 2 **only when the end user actually uses them** — omit integration clauses entirely when list 2 is empty. Required shape:

> _"A &lt;role&gt; for &lt;product&gt;'s &lt;audience — shoppers, members, ops staff, …&gt; that &lt;key tasks in domain language&gt;."_

When list 2 is non-empty, append **in/via** clauses for those end-user-facing services only:

> _"…that &lt;key tasks&gt; **in Notion**, and can &lt;action&gt; **via Intercom**."_

**Bad** (developer persona — wrong audience):

> _"A coding assistant for the Cellar team that reviews PRs **in GitHub** and triages errors **in Sentry**."_

**Bad** (internal infrastructure named — server will attach wrong MCPs):

> _"An inventory assistant for Cellar's wine bar staff that checks stock **in PostgreSQL** and sends confirmations **via Resend**."_

**Good** (audience named, domain tasks only — no infra the user doesn't touch):

> _"An inventory assistant for Cellar's wine bar staff that helps them check wine stock levels, par, vendor details, purchase orders, and invoices."_

**Good** (end-user-facing integration named — user actually uses Intercom):

> _"A support assistant for Acme's customers that answers billing questions and looks up order status, and can escalate live chats **via Intercom**."_

**Before showing the draft, self-check:**

1. The audience is named and every task is something that audience would ask for — no developer-persona drift.
2. No internal infrastructure, email APIs, databases, or dev tooling the end user doesn't directly use.
3. Every service in list 2 appears by name; if list 2 is empty, no integration names appear.

If any check fails, rewrite — do not show a draft that fails.

Show the draft and briefly note the inferred audience (e.g. "this agent will serve Cellar's wine bar staff") and any end-user-facing integrations it names and why, then call `AskQuestion` / `AskUserQuestion` with:

| Option id | Label |
|---|---|
| `approve` | Looks good — run connect |
| `edit` | I want to change the description |

If they pick **edit**, ask for their revised text in chat (not the picker), update the draft, and ask again until they pick **approve**. **Never run the command until they approve.**

---

## Step 3 — Run connect (non-interactive)

**Goal:** authenticate (when using `--login`), create the agent, and start the channel connection in one Connect shell.

Substitute the channel the user picked. Run the command **exactly as written** — no `>`, `tee`, or log file.

Set the agent description in an environment variable first — do **not** paste user-provided prose directly into a double-quoted shell argument (command substitution would execute inside `"…"`).

**Authenticated (user has Novu account — include `--login`):**

```bash
export NOVU_AGENT_DESCRIPTION='<confirmed agent description>'

npx novu@latest connect "$NOVU_AGENT_DESCRIPTION" \
  --ci \
  --login \
  --channel <slack|email|telegram|whatsapp|teams|skip>
```

**Keyless (no account — omit `--login` and `--secret-key`):**

```bash
export NOVU_AGENT_DESCRIPTION='<confirmed agent description>'

npx novu@latest connect "$NOVU_AGENT_DESCRIPTION" \
  --ci \
  --channel <slack|email|telegram|skip>
```

Never pass `--channel whatsapp` or `--channel teams` in keyless mode — those require `--login`.

**Canonical example (authenticated, slack):**

```bash
export NOVU_AGENT_DESCRIPTION='<confirmed agent description>'

npx novu@latest connect "$NOVU_AGENT_DESCRIPTION" \
  --ci \
  --login \
  --channel slack
```

**How to run the Connect shell** — never combine with log redirection or a second watch command:

Always start the Connect command as a **background** Shell (`block_until_ms: 0`), then **Await** its shell id for the markers below. This applies to every auth mode and channel. **Never run it in the foreground** — the CLI blocks up to ~5 min per handoff stage and a foreground call will hit the host shell timeout.

Then follow the path that matches your flags:

- **If using `--login`:** **Await** `NOVU_CONNECT_AUTH_URL_FILE=` on the background shell id, **Read** that file for the auth URL, deliver the URL to the user, then **Await** channel handoff markers and success on the same shell id.
- **If channel is `slack`, `email`, or `telegram`:** **Await** on that shell id (e.g. `NOVU_CONNECT_SLACK_SETUP_URL=`, `NOVU_CONNECT_INBOUND_ADDRESS=`, etc.). **Await** until `✓ Your agent is live` or `✗`. Do not use Monitor, `tail -f`, `grep`, Read on log files.
- **If channel is `whatsapp` or `teams` (authenticated only):** **Await** auth URL, then dashboard agent URL or success on the same shell id.
- **If channel is `skip` without `--login`:** foreground Shell is allowed — the only exception to the background rule above.

Conditional flags:

- **`--login`:** required when the dashboard prompt rule applies, or the user has a Novu account / asks to log in. Ignores `NOVU_SECRET_KEY`. Cannot combine with `--secret-key`.
- **Prefer secure setup links** over `--slack-config-token` / `--telegram-bot-token` on the first run.
- **Runtime:** do not pass `--runtime` or `--anthropic-api-key` — demo runtime is always used.
- **Region:** pass `--region eu` only when the user explicitly asks; otherwise default is **US** Novu Cloud.

**Example — Step 4 Slack re-run (`in_chat` path, authenticated):**

```bash
export NOVU_AGENT_DESCRIPTION='<confirmed agent description>'
export SLACK_CONFIG_TOKEN='<xoxe.xoxp-...>'

npx novu@latest connect "$NOVU_AGENT_DESCRIPTION" \
  --ci \
  --login \
  --channel slack \
  --slack-config-token "$SLACK_CONFIG_TOKEN"
```

**Safe retry — Slack only:** silently re-run once on `Failed to create Slack app: …` before reporting.

---

## Step 4 — Handoffs (human-in-the-loop)

**Goal:** give the user each action that finishes authentication and channel connection.

**Always paste the literal URL — never a placeholder.** Every handoff link must be the full resolved value from the matching `NOVU_CONNECT_*` line. **Await** the pattern before sending any handoff message.

### Dashboard OAuth (when using `--login`)

Every `--login` run prints:

```text
NOVU_CONNECT_AUTH_URL_FILE=<absolute path>
```

**Read** that file (do not paste the path itself) and deliver the one-line auth URL to the user. The file keeps the `device_code` out of CI stdout logs. Tell the user to open the URL (they should already be signed in) and click **Authorize**. The CLI polls until approval (~5 min). On timeout or expiry, re-run Step 3.

### Showing the QR code (host-aware)

The Telegram QR PNGs (`NOVU_CONNECT_TELEGRAM_SETUP_QR_PNG`, `NOVU_CONNECT_TELEGRAM_DEEPLINK_QR_PNG`) are a phone-scan convenience — the literal URL is the primary handoff and must appear in the same message regardless. **Never deliver a QR by only Read-ing the PNG file:** in most hosts a file-read tool call renders collapsed (e.g. Claude Code shows just "Read 1 file"), so the user never sees the QR. Pick the path that matches what your host can render:

- **Chat UI that renders Markdown images inline (e.g. Cursor):** embed the PNG in your reply text with `![Scan the QR code with your phone](<absolute png path>)` — an image in your own message, not a tool call.
- **Terminal host that cannot render images (e.g. Claude Code or other CLIs):** open the PNG in the OS image viewer — `open "<png path>"` (macOS), `xdg-open "<png path>"` (Linux), or `start "" "<png path>"` (Windows) — and tell the user a QR window just opened that they can scan with their phone. If the open command fails or the session is headless/remote, skip the QR entirely and present only the clickable URL — never leave the user hunting through collapsed tool output.

### Channel-specific handoffs

**If channel is `slack`, `email`, or `telegram`:** deliver the handoff from Connect shell stdout, then **Await** until the **CLI poll** finishes.

Read Connect shell stdout (via **Await**, not log files) and act based on the chosen channel:

- **slack** — the connect run defaulted to the secure path, so first **Await** the secure setup link line and copy its value:

  ```text
  NOVU_CONNECT_SLACK_SETUP_URL=<url>
  ```

  This is the moment the user must provide the token, so **now** present the token-delivery choice inline — call `AskQuestion` / `AskUserQuestion` with two options and recommend `secure`:
  - `secure` — **Secure setup page (recommended)** — paste the token on the page the CLI printed; it never enters chat.
  - `in_chat` — **Paste token in chat (less secure)** — the token then lives in chat history.

  **If they pick `secure` (or skip the choice):** paste that exact `<url>` into chat as a clickable link. Then tell them to:
  1. Open <https://api.slack.com/apps> and generate an **App Configuration Token** (access token starting with `xoxe.xoxp-`)
  2. Open the setup link and paste the token there — **not in this chat**

  The CLI polls until the token is saved (~5 min). Then **Await** the OAuth handoff line and copy its value:

  ```text
  NOVU_CONNECT_SLACK_AUTHORIZE_URL=<url>
  ```

  Paste that exact `<url>` into chat and ask them to approve the install within 5 minutes. **Await** until the CLI poll finishes. Re-run on timeout (the Slack app is reused).

  **If they pick `in_chat`:** ask for the token in chat as free-text (not the picker), warn once that it will live in chat history, then **kill the first Connect shell** (the Step 3 process still polling the secure setup page) and **re-run the Step 3 connect command once with `--slack-config-token`** (set via an env var). That supersedes the secure setup page — the CLI skips the `NOVU_CONNECT_SLACK_SETUP_URL` handoff and goes straight to OAuth. **Await** the `NOVU_CONNECT_SLACK_AUTHORIZE_URL=<url>` line, paste that exact URL into chat, and ask them to approve the install within 5 minutes.

- **email** — watch for these machine-readable lines (plain stdout, no ANSI):

  ```text
  NOVU_CONNECT_INBOUND_ADDRESS=<address>
  NOVU_CONNECT_MAILTO=<mailto-url>
  NOVU_CONNECT_SEND_FROM_EMAIL=<email>   # only when present
  ```

  Give the user:
  1. The **mailto link** (`NOVU_CONNECT_MAILTO=…`) — one click opens a pre-filled draft in their mail client; this is the primary handoff.
  2. The **inbound address** as a copy-paste fallback.
  3. If `NOVU_CONNECT_SEND_FROM_EMAIL` is present, tell them to send **from that address** so the agent can reply.

  Then wait for the **CLI poll** — the process completes once the email arrives; on timeout, re-run after they've sent it.

- **telegram** — the connect run defaulted to the secure path. First, watch for the BotFather hint and secure setup link:

  ```text
  NOVU_CONNECT_TELEGRAM_BOTFATHER_URL=<url>              # only when present
  NOVU_CONNECT_TELEGRAM_SETUP_URL=<url>
  NOVU_CONNECT_TELEGRAM_SETUP_QR_PNG=<absolute png path>   # only when present
  ```

  Tell the user to create a bot with @BotFather (<https://t.me/botfather>, send `/newbot`). The bot token is needed next, so **now** present the token-delivery choice inline — call `AskQuestion` / `AskUserQuestion` with two options and recommend `secure`:
  - `secure` — **Secure setup page (recommended)** — paste the token on the page/QR the CLI printed; it never enters chat.
  - `in_chat` — **Paste token in chat (less secure)** — the token then lives in chat history.

  **If they pick `secure` (or skip the choice):** paste the literal `NOVU_CONNECT_TELEGRAM_SETUP_URL` value into chat as a clickable link and tell them to paste the BotFather confirmation message on that page — **not in this chat**. When `NOVU_CONNECT_TELEGRAM_SETUP_QR_PNG` is present, also show the QR following [Showing the QR code (host-aware)](#showing-the-qr-code-host-aware) — never via a bare file read. The CLI polls until the bot token is saved (~5 min).

  **If they pick `in_chat`:** ask for the token in chat as free-text (not the picker), warn once that it will live in chat history, then **kill the first Connect shell** (the Step 3 process still polling the secure setup page) and **re-run the Step 3 connect command once with `--telegram-bot-token`** (set via an env var). That supersedes the secure setup page — the CLI skips the `NOVU_CONNECT_TELEGRAM_SETUP_URL`/QR handoff and goes straight to the deep-link step below.

  Then watch for the deep-link handoff:

  ```text
  NOVU_CONNECT_TELEGRAM_DEEPLINK_URL=<url>
  NOVU_CONNECT_TELEGRAM_BOT_USERNAME=<name>
  NOVU_CONNECT_TELEGRAM_DEEPLINK_QR_PNG=<absolute png path>   # only when present
  ```

  When `NOVU_CONNECT_TELEGRAM_DEEPLINK_QR_PNG` is present, show the QR following [Showing the QR code (host-aware)](#showing-the-qr-code-host-aware). Ask them to open the bot and tap **Start** on `@<botUsername>`. **Await** until the CLI poll finishes. Re-run on timeout with the same command.

- **whatsapp / teams (authenticated)** — CLI prints a dashboard agent URL; paste it and tell the user to finish channel setup there.

- **skip** — nothing to hand off; the agent is created without a channel.

(Keyless + `whatsapp`/`teams` never reach this step — redirected in Step 1.)

---

## Step 5 — Report the result

**Goal:** relay what the CLI printed and point the user at the channel or dashboard.

On success the CLI exits `0` and prints:

```text
✓ Your agent is live.
  Agent: <name> (<identifier>)
  → Check <Channel> — your agent just messaged you.
  Dashboard: <dashboard url>
```

**Authenticated:** tell the user their agent is live in their Development environment — message it on the connected channel or open the dashboard URL.

**Keyless:** same as above, plus explain the **demo limit** (~5 free replies) and the in-channel **"Sign up & keep this agent"** claim link.

**On failure**, surface the error and matching fix:

| Symptom | Fix |
|---|---|
| `…requires --prompt "<agent description>"` | Re-run Step 3 with the positional description. |
| `Authorization timed out` / `Authorization session expired` | User didn't approve in time — re-run for a fresh auth link. |
| `This environment doesn't have a Novu demo Claude integration` | Demo runtime not enabled — enable in dashboard or use BYOK runtime flags. |
| `The Slack App Configuration Token wasn't saved within … seconds` | Re-run for a fresh setup link. |
| `Failed to create Slack app: …` | Silently re-run once; surface only if it fails again. |
| `Slack OAuth was not completed within … seconds` | Re-run the same command. |
| `We didn't see your email at … within …s` | Re-run after they send the email. |
| Telegram token / `/start` timeouts | Re-run the same command. |
| `Keyless environment creation is currently disabled` | Wrong API/region, or use `--login` / `--secret-key` for an existing account. |

---

## Command flag reference

Run `novu@latest connect --help` for the full contract. Keep help text in sync when changing connect flags.

| Flag | Purpose |
|---|---|
| `connect "<description>"` | Positional agent description (required in `--ci`). |
| `--ci` | Non-interactive mode (required). |
| `--login` | Dashboard OAuth — use when the user has a Novu account or the dashboard prompt rule applies. |
| `--region <us\|eu>` | Target Novu Cloud region (default: `us`). |
| `--channel <slack\|email\|telegram\|whatsapp\|teams\|skip>` | Channel to connect. `whatsapp`/`teams` require `--login`. |
| `--slack-config-token` / `--telegram-bot-token` | Non-secure CI escape hatches when user opts in. |
| *(omit both)* | Keyless mode — temporary agent, no account. Do not pass `--secret-key` in this guided flow. |

---

## Limitations to keep in mind

- **One run = one new agent + one channel.** Re-running creates another agent.
- **Channel support is uneven headlessly:** `slack` and `telegram` need two user actions after auth; `email` one; `whatsapp`/`teams` need `--login` and finish in the dashboard.
- **Prefer secure setup pages for Slack/Telegram tokens.**
- **Keyless data is temporary** until claimed via the in-channel sign-up link.
- **`--login` ignores `NOVU_SECRET_KEY`** — dashboard OAuth always wins when the flag is set.

---

## Definition of done

**Keyless + WhatsApp / MS Teams:** done when you've delivered the dashboard sign-in URL — no agent generated.

You are done when:

1. The user picked a channel and confirmed the agent description.
2. Dashboard OAuth completed (when using `--login`), or keyless bootstrap succeeded.
3. You delivered channel handoffs (or noted `skip` / whatsapp-teams dashboard URL).
4. Connect shell printed `✓ Your agent is live.` (exit `0`); CLI poll validated handoffs where applicable.
5. You reported agent identifier + Dashboard URL (and keyless claim path if applicable).
