packages/shared/docs/agent-onboarding.md
You are an AI coding agent that helps a user onboard a Novu agent and connect it to a channel of their choice. Two paths exist:
| Path | What it does |
|---|---|
| Managed agent | Creates a hosted agent (demo Claude runtime). Novu runs the intelligence — no codebase wiring. |
| Custom code bridge | Creates a bridge agent whose handler runs in the user's app (AI SDK or LangChain). You wire the bridge route and handler after connect. |
Your job end to end: pick the right path, run one non-interactive npx novu connect command, hand the user whatever they need to finish connecting the channel, wire the bridge when on the custom-code path, then report the result.
Default to managed. Only use the bridge path when the user clearly wants handler code in their app.
Do not ask Step 0 when the user only wants to connect a channel in the current workspace — e.g. "Connect a Novu agent to Slack for this project" is managed keyless, not bridge. Go straight to Step M1.
| Path | Use when |
|---|---|
| Custom code bridge | The user says "add an agent to my app", "wire" Novu into their handler code, names AI SDK or LangChain, or wants channel replies from their application code. |
| Managed agent | The user names a channel only, wants a hosted/demo/managed agent, is trying Novu keyless from scratch, says "Connect … for this project" without bridge signals, or explicitly asks for a managed agent. |
Mandatory routing rules:
--keyless).--keyless).--keyless) — the CLI will not scaffold a bridge in keyless mode.Ask Step 0 only when the user explicitly mixes bridge and managed signals. Otherwise pick the path from the table and continue.
| Mode | When to use | CLI flag |
|---|---|---|
| Keyless (default) | User is trying Novu from their codebase with no explicit dashboard signal | --keyless — temporary agent; user claims via in-channel sign-up link |
| Dashboard OAuth | User came from the Novu dashboard, is already signed in, mentions an existing account, or asks to log in | Omit --keyless and --secret-key — dashboard OAuth (the CLI default); agent is created in their Development environment |
Default to keyless — always pass --keyless unless one of the dashboard-signal triggers below applies. Do not pass --secret-key in this flow — use dashboard OAuth (omit --keyless) instead when they have an account.
The CLI's own default (no flag) is dashboard OAuth. This guided flow deliberately overrides that by passing
--keylessfor anonymous users; you only omit--keylesswhen a dashboard signal applies.
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 use dashboard OAuth — omit --keyless. Never pass --keyless in that case.
Bridge path rule (mandatory): Custom code bridge always omits --keyless — even without an explicit dashboard sentence. Bridge scaffolding requires an authenticated Novu account.
These govern every step. When in doubt, follow these over any specific instruction below.
in_chat token fallback re-run (after killing the first Connect shell).--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).--ci (this flow), Sendblue is non-interactive with no setup link: the only way to pass the Sendblue API key and secret key is via --sendblue-api-key / --sendblue-secret-key, so the user provides them in chat. This is an explicit, accepted exception to the secure-page preference (Sendblue offers no secure page headlessly). There is no token-delivery picker for iMessage — collect the credentials in chat and warn once that they live in chat history, exactly as with the in_chat fallback.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 or out-of-band probes (ScheduleWakeup, sleep, ps/ps aux, grep, kill -0, or "check back in N minutes") to wait for or inspect the Connect process — the only way to wait is to Await the Connect shell continuously until the next NOVU_CONNECT_* sentinel or ✓ Your agent is live appears. The only exception: --channel skip in keyless mode may run in the foreground. Never use Bash grep/cat/awk on any file (including package.json) — use the Read tool and parse the content in your reasoning.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.--keyless (the default), do not run connect — redirect them to the Novu dashboard instead (Step 1). With dashboard OAuth (omit --keyless), the CLI creates the agent and hands off a dashboard URL to finish channel setup. WhatsApp is CLI-handled in both modes: run connect like any other channel — the CLI prints a public tokenized Meta Embedded Signup URL the user completes in the browser, and polls until it's done. If Embedded Signup is unavailable on the deployment, the CLI itself falls back (keyless runs prompt dashboard sign-in and retry; if still unavailable it opens the dashboard integrations tab and exits).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:
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, and Telegram / iMessage).AskUserQuestion with the same shape (label + optional description).Use the picker for: Step 0 (path only when genuinely ambiguous), Step M1 / Step B1 (channel), Step M2 (approve / edit managed description), Step B2 (runtime when ambiguous), 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.
| Term | Meaning |
|---|---|
| Keyless mode | The default for this flow — pass --keyless (no --secret-key). Creates a temporary agent with no Novu account. |
| Dashboard OAuth | The dashboard-signal auth path — omit --keyless. 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. |
| 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 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 Awaiting 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. |
| Bridge agent | Custom code path: agent handler runs on the user's server; Novu forwards channel messages to a /api/novu bridge route. |
| Scaffold | Bridge empty-dir path: CLI creates a Next.js AI SDK or LangChain app in --ci (auto-confirm). Optional --llm-auth wires a model provider; default is demo echo. |
| Reconcile | Bridge existing-project path: CLI installs packages / env / dev:novu and prints requirements; you Write remaining handler code. |
| Requirements file | Bridge path only: CLI prints NOVU_CONNECT_AI_SDK_REQUIREMENTS_FILE= or NOVU_CONNECT_LANGCHAIN_REQUIREMENTS_FILE= with a checklist and wiring prompt. |
--ci, plus --keyless for the default keyless mode), streamed.ai-sdk vs langchain (Step B2). Empty dir → default ai-sdk unless the user named LangChain.--runtime <ai-sdk|langchain> (Step B3) — no agent description positional. On empty dir, add --llm-auth when the user wants a real provider.dev:novu.Out of scope on this path: Do not wire Novu into the user's codebase.
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 Telegram and iMessage share one option, 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 | Nothing up front. The CLI prints an inbound email address; the user sends one email to it. | |
telegram | Telegram / iMessage (Sendblue) | Two chat channels grouped to fit the 4-option limit. 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 it in chat and you pass --telegram-bot-token), then tap Start on the bot. iMessage (Sendblue): needs a Sendblue account + provisioned phone line; no secure page — collect the API key, secret key, sender number, and the user's own phone in chat (see below). |
dashboard | WhatsApp / MS Teams | Two channels grouped to fit the 4-option limit. WhatsApp: CLI-handled in both modes — the CLI prints a Meta Embedded Signup link the user completes in the browser (Facebook Business login), then the user sends one WhatsApp message to finish. MS Teams (keyless, default): sign in to the Novu dashboard and continue there (no CLI run). MS Teams (dashboard OAuth, omit --keyless): CLI creates the agent, then opens the dashboard to finish channel setup. |
If they pick telegram (Telegram / iMessage): ask which one they want with a follow-up picker of exactly two options — telegram ("Telegram") and sendblue ("iMessage (Sendblue)") — before continuing. Use --channel telegram or --channel sendblue in Step 3 accordingly. Both are CLI-handled and keyless-compatible.
If they pick dashboard (WhatsApp / MS Teams): ask which one they want with a follow-up picker of exactly two options — whatsapp ("WhatsApp") and teams ("MS Teams") — before continuing.
whatsapp: CLI-handled in both modes — proceed to Step M2 and use --channel whatsapp in Step 3 (keep --keyless for the default keyless mode).teams with keyless (--keyless, the default): HARD STOP — never invoke npx novu connect in this branch (not in the foreground, not backgrounded, not with any channel flag). 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.teams with dashboard OAuth (omit --keyless): use --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.
sendblue (iMessage) → collect all four values up front, in one message, because the --ci run hard-fails at launch without them and there is no secure page. First tell the user they need a Sendblue account with a provisioned phone line (API keys: https://dashboard.sendblue.com/settings/api; phone line: https://dashboard.sendblue.com/settings/phone-line-management). Then collect, labeling each by role so the two numbers are never confused:
--sendblue-api-key--sendblue-secret-key--sendblue-from — "the number Sendblue assigned your agent (the sender), in E.164, e.g. +14155551234".--sendblue-test-phone — "your own phone number — where the agent texts a test iMessage and which you reply from to finish, in E.164".Warn once that the API key and secret key will live in chat history (no secure page exists for Sendblue headlessly).
email / skip / whatsapp → no extra input up front.
dashboard → teams → keyless (default): flow ends with dashboard redirect; dashboard OAuth: --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.
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:
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 <role> for <product>'s <audience — shoppers, members, ops staff, …> that <key tasks in domain language>."
When list 2 is non-empty, append in/via clauses for those end-user-facing services only:
"…that <key tasks> in Notion, and can <action> 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:
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.
Goal: create the agent (keyless by default) 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 "…").
Keyless (default — pass --keyless):
export NOVU_AGENT_DESCRIPTION='<confirmed agent description>'
npx novu@latest connect "$NOVU_AGENT_DESCRIPTION" \
--ci \
--keyless \
--channel <slack|email|telegram|sendblue|whatsapp|skip>
Dashboard OAuth (dashboard signal — omit --keyless):
export NOVU_AGENT_DESCRIPTION='<confirmed agent description>'
npx novu@latest connect "$NOVU_AGENT_DESCRIPTION" \
--ci \
--channel <slack|email|telegram|sendblue|whatsapp|teams|skip>
Never pass --channel teams with --keyless — MS Teams requires dashboard OAuth (omit --keyless). WhatsApp works in both modes.
Canonical example (keyless, slack):
export NOVU_AGENT_DESCRIPTION='<confirmed agent description>'
npx novu@latest connect "$NOVU_AGENT_DESCRIPTION" \
--ci \
--keyless \
--channel slack
Canonical example (keyless, iMessage / Sendblue): all four values are required at launch — set the secrets in env vars first (never inline in the command). --sendblue-from is the agent's Sendblue number; --sendblue-test-phone is the user's own phone.
export NOVU_AGENT_DESCRIPTION='<confirmed agent description>'
export SENDBLUE_API_KEY='<sendblue api key>'
export SENDBLUE_SECRET_KEY='<sendblue secret key>'
npx novu@latest connect "$NOVU_AGENT_DESCRIPTION" \
--ci \
--keyless \
--channel sendblue \
--sendblue-api-key "$SENDBLUE_API_KEY" \
--sendblue-secret-key "$SENDBLUE_SECRET_KEY" \
--sendblue-from '<+E.164 agent number>' \
--sendblue-test-phone '<+E.164 user phone>'
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.
Backgrounding rule (non-negotiable): use the tool's own background mechanism (run_in_background: true / block_until_ms: 0) and wait only by Await/BashOutput on the returned shell id. Do NOT append & to the command, do NOT sleep, and do NOT cat/tail/grep any /tmp/*.log or other file to inspect progress — the only source of progress is the Connect shell's own stdout polled via Await.
Then follow the path that matches your flags:
--keyless): 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.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, sleep, ps, or Read on log files — poll the shell id and nothing else.sendblue (iMessage): the credential flags were passed in Step 3, so the CLI goes straight to sending a test iMessage. Await NOVU_CONNECT_SENDBLUE_IMESSAGE_URL= and NOVU_CONNECT_SENDBLUE_FROM_NUMBER= on that shell id (and the optional NOVU_CONNECT_SENDBLUE_WEBHOOK_CALLBACK_URL= fallback — see Step 4), then Await until ✓ Your agent is live or ✗.whatsapp (either mode): Await NOVU_CONNECT_WHATSAPP_SIGNUP_URL= on that shell id, deliver the signup URL, then Await the wa.me test-message lines and success on the same shell id. The signup poll runs up to ~15 min (longer than other channels — Meta's flow takes a while). With dashboard OAuth, first Await the auth URL as above.teams (dashboard OAuth only): Await auth URL, then dashboard agent URL or success on the same shell id.skip in keyless mode: foreground Shell is allowed — the only exception to the background rule above.Conditional flags:
--keyless: the default for this flow — pass it for anonymous users. Omit it only when a dashboard signal applies (dashboard OAuth). Cannot combine with --secret-key.--slack-config-token / --telegram-bot-token on the first run.--runtime or --anthropic-api-key — demo runtime is always used.--region eu only when the user explicitly asks; otherwise default is US Novu Cloud.Example — Step 4 Slack re-run (in_chat path, keyless default):
export NOVU_AGENT_DESCRIPTION='<confirmed agent description>'
export SLACK_CONFIG_TOKEN='<xoxe.xoxp-...>'
npx novu@latest connect "$NOVU_AGENT_DESCRIPTION" \
--ci \
--keyless \
--channel slack \
--slack-config-token "$SLACK_CONFIG_TOKEN"
(Drop --keyless on the re-run if the original run used dashboard OAuth.)
Safe retry — Slack only: silently re-run once on Failed to create Slack app: … before reporting.
The bridge path creates a Novu agent record, connects a channel, then either scaffolds a new app (empty directory) or reconciles an existing project. After connect, you finish any remaining handler wiring from the requirements file.
Detect with the Read tool (or a failed Read of package.json). Never use Bash to inspect the tree.
| Workspace | CLI behavior in --ci | Your follow-up |
|---|---|---|
Empty — no package.json | Auto-scaffolds a Next.js AI SDK or LangChain agent app (confirm is skipped in --ci), writes env + dev:novu, and usually leaves the handler already generated | Still Read the requirements file. If code-wiring is checked off, you are done after report. Pass --llm-auth when the user wants a real model provider (see below). |
Existing — package.json present | Reconciles packages/env/dev:novu, prints a requirements checklist; does not invent the handler for you | Read the requirements file and Write the bridge route + agent handler for every unchecked code-wiring item. |
LLM provider wiring on scaffold only (empty dir):
| Flag | When |
|---|---|
Omit --llm-auth | Default in --ci — scaffold uses a demo echo handler (no provider API key). Fine when the user did not ask for OpenAI/Anthropic/etc. |
--llm-auth openai + --openai-api-key … | User wants OpenAI (or already has OPENAI_API_KEY to pass). |
--llm-auth anthropic + --anthropic-api-key … | User wants Anthropic. |
--llm-auth codex-subscription | User wants ChatGPT subscription / Codex OAuth on this machine. |
--llm-auth claude-subscription | User wants Claude subscription — ai-sdk only (not LangChain). |
--llm-auth skip | Explicit demo echo (same as omitting). |
Do not invent API keys. If the user asked for a provider but has not given a key (and is not using a subscription), ask once in chat, then pass the matching flags. Subscription kinds need a prior local CLI login on the machine — if connect fails for that reason, tell the user the login command from the CLI error.
Existing projects: never pass --llm-auth — reconcile does not use it. Reuse the project's existing model provider when wiring the handler.
Same channel picker and rules as Step M1 — except:
--keyless on the bridge path.Goal: lock --runtime ai-sdk or --runtime langchain before running connect.
Use the Read tool on package.json and inspect dependencies / devDependencies in the file content. Never use Bash (grep, cat, jq, or shell pipelines) to read package.json.
| Signal in package.json | Runtime |
|---|---|
ai or any @ai-sdk/* package | ai-sdk |
langchain, @langchain/core, or any @langchain/* package | langchain |
| Both AI SDK and LangChain signals | Ask the user (picker below) |
Neither (or empty dir / missing package.json) | Default to ai-sdk unless the user named LangChain |
If you must ask, call AskQuestion / AskUserQuestion:
| Option id | Label |
|---|---|
ai-sdk | AI SDK — Vercel AI SDK (ai + @ai-sdk/*) |
langchain | LangChain — langchain + @langchain/* |
Goal: create the bridge agent, connect the channel, and let the CLI scaffold or reconcile the project.
Do not pass a positional agent description — bridge agents get a name from the project directory in --ci mode.
Always omit --keyless. Dashboard OAuth is required (see Auth mode).
npx novu@latest connect \
--ci \
--runtime <ai-sdk|langchain> \
--channel <slack|email|telegram|whatsapp|teams|skip>
Canonical example (dashboard OAuth, AI SDK, slack, existing project):
npx novu@latest connect \
--ci \
--runtime ai-sdk \
--channel slack
Empty-dir scaffold with OpenAI (example):
npx novu@latest connect \
--ci \
--runtime ai-sdk \
--llm-auth openai \
--openai-api-key "$OPENAI_API_KEY" \
--channel slack
Run the Connect shell with the same backgrounding rules as the managed path.
After channel handoffs, keep Awaiting the same shell until you also see:
NOVU_CONNECT_AI_SDK_REQUIREMENTS_FILE=<absolute path>
or
NOVU_CONNECT_LANGCHAIN_REQUIREMENTS_FILE=<absolute path>
Then Await ✓ Your agent is live. or ✗ on that shell id.
The CLI may auto-install packages, write .env.local, add a dev:novu script, and (on empty dirs) scaffold the app. Unchecked items in the requirements file still need your help.
Goal: satisfy every unchecked requirement so the channel reaches the user's code.
NOVU_CONNECT_*_REQUIREMENTS_FILE= line (do not paste the path to the user).Agent: <name> (<identifier>).- [ ] item in the file (install packages, env, dev script).code-wiring on an existing project: follow the "## Agent prompt" section inside that file exactly — use the Write tool to create the bridge route and agent handler (do not only describe the code in chat). Match App Router vs Pages Router, src/ layout, and the package manager from the lockfile.agent('<identifier>', …)).code-wiring is already [x], do not rewrite the generated handler unless the requirements file still lists unchecked wiring — then follow the agent prompt as above.dev:novu script (or the package manager equivalent) and confirm the bridge registers without errors.Authoritative docs when stuck: fetch https://docs.novu.co/llms.txt, then append .md to any doc URL. AI SDK: /agents/custom-code-agent/frameworks/ai-sdk. LangChain: /agents/custom-code-agent/frameworks/langchain.
Lead with whether connect succeeded and whether the bridge is wired.
Authenticated bridge recap example (existing project):
"Novu created a bridge agent, connected Slack, and reconciled your project. I wired the
/api/novuroute and agent handler — runnpm run dev:novu(or your package manager's equivalent) and message the bot in Slack to test."
Scaffold recap example (empty dir):
"Novu scaffolded an AI SDK agent app, connected Slack, and your agent is live. Run
npm run dev:novuin the new project directory and message the bot in Slack to test."
Include the agent identifier and dashboard URL from the CLI output.
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.
--keyless)Every dashboard OAuth run prints:
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.
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:
 — an image in your own message, not a tool call.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.If channel is slack, email, telegram, or sendblue: 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:
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:
xoxe.xoxp-)The CLI polls until the token is saved (~5 min). Keep Awaiting the Connect shell — do not set a timer. When the token lands, stdout prints:
NOVU_CONNECT_SLACK_CONFIG_TOKEN_SAVED=1
Then Await the OAuth handoff line and copy its value:
NOVU_CONNECT_SLACK_AUTHORIZE_URL=<url>
Paste that exact <url> into chat, framing it as a fallback link — e.g. "If the Slack install page didn't open automatically, use this link 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 as a fallback link — e.g. "If the Slack install page didn't open automatically, use this link to approve the install (within 5 minutes)." Once ✓ Your agent is live appears on the re-run, stop asking for the token and deliver the conclusion-first report immediately.
email — watch for these machine-readable lines (plain stdout, no ANSI):
NOVU_CONNECT_INBOUND_ADDRESS=<address>
NOVU_CONNECT_MAILTO=<mailto-url>
NOVU_CONNECT_SEND_FROM_EMAIL=<email> # only when present
Give the user:
NOVU_CONNECT_MAILTO=…) — one click opens a pre-filled draft in their mail client; this is the primary handoff.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:
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) — 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:
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). 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.
sendblue (iMessage) — the credentials were passed in Step 3, so there is no token-delivery picker and no QR. The CLI creates the integration, registers the receive webhook, and sends a test iMessage from the agent's Sendblue number to the user's phone. First Await the test-message handoff lines and copy their values:
NOVU_CONNECT_SENDBLUE_IMESSAGE_URL=<sms:…>
NOVU_CONNECT_SENDBLUE_FROM_NUMBER=<+E.164>
Give the user:
sms: deep link (NOVU_CONNECT_SENDBLUE_IMESSAGE_URL=…) — one tap opens Messages pre-addressed to the agent's number; this is the primary handoff.--sendblue-test-phone) — the connection completes only when Novu sees that inbound iMessage.Conditional — manual webhook (only if these lines appear): webhook registration is automatic, so normally there is nothing to do. Only if auto-registration failed does the CLI print:
NOVU_CONNECT_SENDBLUE_WEBHOOK_CALLBACK_URL=<url>
NOVU_CONNECT_SENDBLUE_WEBHOOK_SECRET=<secret> # only when present
If present, relay the callback URL (and signing secret, if any) and tell the user to add it as a webhook in their Sendblue dashboard. If the lines don't appear, say nothing.
Then wait for the CLI poll — the process completes once the user's inbound iMessage arrives (~5 min); on timeout, re-run after they've texted the number.
whatsapp (keyless and dashboard OAuth) — the CLI mints a public tokenized Meta Embedded Signup link (valid ~30 min, no dashboard login needed) and prints:
NOVU_CONNECT_WHATSAPP_SIGNUP_URL=<url>
Paste the literal signup URL into chat as a clickable link and tell the user to complete Meta Embedded Signup there (log in with Facebook, pick or create a WhatsApp Business account and number). The CLI polls up to ~15 min for signup to complete — longer than other channels, so keep Awaiting the same shell id. When credentials are saved, the CLI prints the test-message handoff:
NOVU_CONNECT_WHATSAPP_WA_ME_URL=<url> # only when present
NOVU_CONNECT_WHATSAPP_PHONE_NUMBER=<number> # only when present
Give the user the wa.me link (or the phone number) and tell them to send any WhatsApp message to it. Then wait for the CLI poll — the process completes once the inbound message arrives (~5 min); on timeout, re-run after they've messaged the number (re-runs resume where signup left off).
Fallback: when Meta Embedded Signup is unavailable on the deployment, a keyless run prints an auth URL to sign in to a Novu account and retries; if still unavailable the CLI opens the dashboard integrations tab and exits — relay that URL and tell the user to finish channel setup there.
teams (dashboard OAuth only) — 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 + teams never reaches this step — redirected in Step 1.)
Goal: relay what the CLI printed, give a short recap of what onboarding set up, and point the user at the channel and the next step (claim link for keyless, dashboard for authenticated).
On success the CLI exits 0 and prints:
✓ Your agent is live.
Agent: <name> (<identifier>)
→ Check <Channel> — your agent just messaged you.
Dashboard: <dashboard url> # authenticated only
Claim your agent: <claim url> # keyless only
Open the final report with the CLI's literal success line — copy the ✓ Your agent is live. line verbatim rather than paraphrasing it (e.g. "set up", "connected", "ready"); on failure, lead with the CLI's error instead. After leading with the CLI's result, give a 1–2 sentence recap of what onboarding set up — consistent with the conclusion-first operating principle. Before the channel/next-step pointer, briefly explain what the connect run built so the result isn't a black box. Keep it to one or two sentences, in plain language, e.g.:
"Here's what Novu built from your description: a hosted AI agent — its system prompt, the right tools and skills, MCP servers for the services you named, and a connection to <channel> so it can message your users."
Adapt the recap to what actually happened (drop the MCP clause when no integrations were named, name the channel that was connected, or say the agent was created without a channel for skip).
Then tell the user how to proceed.
Authenticated: after the recap, tell the user their agent is live in their Development environment — they can message it on the connected channel or open the dashboard URL to refine it (tweak its description and prompt, change tools, skills, and MCP connections, manage channels, and watch its activity).
Keyless: after the recap, surface the CLI's Claim your agent: link (not the dashboard agent URL). Explain the demo limit (~5 free replies) and that signing up via the claim link moves the agent, its channel, and the conversation into their own account — after that they can manage and refine it from the dashboard.
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. |
We didn't see an inbound iMessage on … within …s | User hasn't replied yet — tell them to text the agent number, then re-run the same command. |
WhatsApp signup wasn't completed within … minutes | User hasn't finished Meta Embedded Signup — share the printed signup URL, then re-run the same command to resume. |
We didn't see a WhatsApp message to … within … seconds | User hasn't messaged the business number yet — tell them to send any WhatsApp message, then re-run the same command. |
Non-interactive mode: pass --sendblue-api-key, --sendblue-secret-key and --sendblue-from | A Sendblue credential is missing — collect all three plus --sendblue-test-phone and re-run Step 3. |
Invalid --sendblue-from … / Invalid --sendblue-test-phone … (Expected E.164) | Re-collect the number in E.164 (e.g. +14155551234), keeping the sender vs user-phone roles straight, and re-run. |
Keyless environment creation is currently disabled | Wrong API/region, or omit --keyless and use dashboard OAuth / --secret-key for an existing account. |
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 (managed path only; required in --ci managed runs). |
--ci | Non-interactive mode (required). |
--runtime <ai-sdk|langchain|custom-code|chat-sdk|demo|claude|claude-aws> | Agent brain. Bridge path: pass ai-sdk or langchain. Managed path: omit (demo runtime). |
--llm-auth <openai|anthropic|codex-subscription|claude-subscription|skip> | Bridge empty-dir scaffold only (ignored on existing-project reconcile). In --ci, omitting defaults to skip (demo echo). Pair openai / anthropic with --openai-api-key / --anthropic-api-key. claude-subscription is ai-sdk only. |
--openai-api-key / --anthropic-api-key | API keys for --llm-auth openai / anthropic on empty-dir scaffold. |
--keyless | Temporary demo agent — managed path only (default for anonymous managed runs). Never on bridge path. |
--region <us|eu> | Target Novu Cloud region (default: us). |
--channel <slack|email|telegram|sendblue|whatsapp|teams|skip> | Channel to connect. sendblue is iMessage. whatsapp works in both modes (keyless included); teams requires dashboard OAuth (omit --keyless). |
--slack-config-token / --telegram-bot-token | Non-secure CI escape hatches when user opts in. |
--sendblue-api-key / --sendblue-secret-key | Sendblue API credentials. Required for --channel sendblue in --ci (no secure page). |
--sendblue-from <+E.164> | The agent's Sendblue-assigned sender number. Required for --channel sendblue in --ci. |
--sendblue-test-phone <+E.164> | The user's own phone that receives the test iMessage and replies to finish. Required for --channel sendblue in --ci. |
(omit --keyless) | Dashboard OAuth — required for bridge; use for managed when a dashboard signal applies. Do not pass --secret-key in this guided flow. |
Bridge path machine-readable stdout (in addition to channel handoffs):
NOVU_CONNECT_AI_SDK_REQUIREMENTS_FILE=<absolute path>
NOVU_CONNECT_LANGCHAIN_REQUIREMENTS_FILE=<absolute path>
slack and telegram need two user actions after auth; email and sendblue (iMessage) one (send an email / reply to the test iMessage); whatsapp needs two (complete Meta Embedded Signup in the browser, then send one WhatsApp message) and its signup poll runs up to ~15 min; teams needs dashboard OAuth and finishes in the dashboard.--sendblue-* flags in chat (no secure page exists headlessly).--keyless unless a dashboard signal applies or the user chose the bridge path (always omit --keyless).Keyless + MS Teams: done when you've delivered the dashboard sign-in URL — no agent generated.
You are done when:
--keyless), or dashboard OAuth completed (when omitting --keyless).skip / the teams dashboard URL).✓ Your agent is live. (exit 0); CLI poll validated handoffs where applicable.You are done when:
ai-sdk or langchain (and --llm-auth when scaffolding empty dir with a real provider).--keyless).✓ Your agent is live..- [ ] item is satisfied — on existing projects that includes Writing the bridge route + handler; on scaffolded projects the CLI may already have checked code-wiring.dev:novu and message the agent on the connected channel.