Back to Novu

Novu Agent Onboarding — instructions for an AI agent

packages/shared/docs/agent-onboarding.md

3.19.054.2 KB
Original Source

Novu Agent Onboarding — instructions for an AI agent

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:

PathWhat it does
Managed agentCreates a hosted agent (demo Claude runtime). Novu runs the intelligence — no codebase wiring.
Custom code bridgeCreates 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.


Step 0 — Choose path

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.

PathUse when
Custom code bridgeThe 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 agentThe 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:

  • "Add an agent to my app" (or equivalent) → custom code bridge. Never use the managed path.
  • "Connect a Novu agent to <channel> for this project" (or similar channel-only wording) → managed. Never bridge. "For this project" means the current workspace directory, not "wire my handler."
  • "in my repo" / "in my codebase" alone is not a bridge signal unless they also want AI SDK / LangChain / handler wiring.
  • "I'm signed in to the Novu dashboard" + add-to-my-app / wire / AI SDK / LangChain signal → custom code bridge with dashboard OAuth (omit --keyless).
  • "I'm signed in to the Novu dashboard" + channel-only connect (no bridge signals) → managed with dashboard OAuth (omit --keyless).
  • Keyless is only valid on the managed path. Bridge agents always require dashboard OAuth (omit --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.


Auth mode — pick one before Step 3

ModeWhen to useCLI 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 OAuthUser came from the Novu dashboard, is already signed in, mentions an existing account, or asks to log inOmit --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 --keyless for anonymous users; you only omit --keyless when 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.


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. Managed path: channel (Step M1) and purpose confirmation (Step M2) require the user. Bridge path: channel (Step B1) and runtime (Step B2) when detection is ambiguous. Default everything else 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).
  • iMessage (Sendblue) has no secure setup page — its secrets go in chat. In --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.
  • 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 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.
  • The CLI validates handoffs. For dashboard OAuth, 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.
  • MS Teams in keyless mode never reaches the CLI. If the user picks MS Teams and you are using --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).
  • 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)

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, and Telegram / iMessage).
  • Claude Code: 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.


Glossary (shared language — use these terms)

TermMeaning
Keyless modeThe default for this flow — pass --keyless (no --secret-key). Creates a temporary agent with no Novu account.
Dashboard OAuthThe 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 runtimeDefault — shared Claude runtime. In keyless mode, limited to ~5 free replies.
HandoffThe channel-specific user action (authorize link, send email, or dashboard URL) that finishes connecting the channel.
Dashboard redirectKeyless-only MS Teams path: no agent is created in the CLI — user continues in the dashboard.
Connect shellThe 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 pollWhile 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.
ClaimKeyless only: user signs up via the in-channel link, migrating the temporary agent into their workspace.
Bridge agentCustom code path: agent handler runs on the user's server; Novu forwards channel messages to a /api/novu bridge route.
ScaffoldBridge 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.
ReconcileBridge existing-project path: CLI installs packages / env / dev:novu and prints requirements; you Write remaining handler code.
Requirements fileBridge path only: CLI prints NOVU_CONNECT_AI_SDK_REQUIREMENTS_FILE= or NOVU_CONNECT_LANGCHAIN_REQUIREMENTS_FILE= with a checklist and wiring prompt.

Flow overview

Managed agent

  1. Channel — ask which channel. Keyless + MS Teams → dashboard redirect only (Steps M2–M5 skipped). Slack, Email, Telegram, iMessage (Sendblue), and WhatsApp are CLI-handled in keyless. Dashboard OAuth 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 M3 (--ci, plus --keyless for the default keyless mode), streamed.
  4. HandoffShared channel handoffs. Let the CLI poll.
  5. Report — relay success, recap, next step (claim link or dashboard).

Custom code bridge (AI SDK / LangChain)

  1. Channel — same picker as managed (Step B1).
  2. Runtime — detect or ask ai-sdk vs langchain (Step B2). Empty dir → default ai-sdk unless the user named LangChain.
  3. Run — connect with --runtime <ai-sdk|langchain> (Step B3) — no agent description positional. On empty dir, add --llm-auth when the user wants a real provider.
  4. HandoffShared channel handoffs. Also Await the bridge requirements file sentinel on the same shell.
  5. Wire — read the requirements file; on existing projects Write the bridge route/handler; on scaffold only if items remain unchecked (Step B5).
  6. Report — agent + channel live, bridge ready, run dev:novu.

Managed agent path

Out of scope on this path: Do not wire Novu into the user's codebase.

Step M1 — 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 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 idLabelWhat the user must do
slackSlackRecommended (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.
emailEmailNothing up front. The CLI prints an inbound email address; the user sends one email to it.
telegramTelegram / 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).
dashboardWhatsApp / MS TeamsTwo 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:

    1. API key--sendblue-api-key
    2. Secret key--sendblue-secret-key
    3. Sender number--sendblue-from — "the number Sendblue assigned your agent (the sender), in E.164, e.g. +14155551234".
    4. Your own phone--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.


Step M2 — 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 <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:

  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 idLabel
approveLooks good — run connect
editI 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 M3 — Run connect (managed, non-interactive)

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):

bash
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):

bash
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):

bash
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.

bash
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:

  • If using dashboard OAuth (omitting --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.
  • 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, sleep, ps, or Read on log files — poll the shell id and nothing else.
  • If channel is 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 .
  • If channel is 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.
  • If channel is teams (dashboard OAuth only): Await auth URL, then dashboard agent URL or success on the same shell id.
  • If channel is 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.
  • 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, keyless default):

bash
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.


Custom code bridge path (AI SDK / LangChain)

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.

Empty directory vs existing project

Detect with the Read tool (or a failed Read of package.json). Never use Bash to inspect the tree.

WorkspaceCLI behavior in --ciYour follow-up
Empty — no package.jsonAuto-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 generatedStill 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).
Existingpackage.json presentReconciles packages/env/dev:novu, prints a requirements checklist; does not invent the handler for youRead the requirements file and Write the bridge route + agent handler for every unchecked code-wiring item.

LLM provider wiring on scaffold only (empty dir):

FlagWhen
Omit --llm-authDefault 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-subscriptionUser wants ChatGPT subscription / Codex OAuth on this machine.
--llm-auth claude-subscriptionUser wants Claude subscription — ai-sdk only (not LangChain).
--llm-auth skipExplicit 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.

Step B1 — Choose channel and collect inputs

Same channel picker and rules as Step M1except:

  • Never pass --keyless on the bridge path.
  • Do not infer or confirm a managed-agent description (skip Step M2 entirely).
  • Runtime is chosen in Step B2, not here.

Step B2 — Pick bridge runtime

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.jsonRuntime
ai or any @ai-sdk/* packageai-sdk
langchain, @langchain/core, or any @langchain/* packagelangchain
Both AI SDK and LangChain signalsAsk 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 idLabel
ai-sdkAI SDK — Vercel AI SDK (ai + @ai-sdk/*)
langchainLangChain — langchain + @langchain/*

Step B3 — Run connect (bridge, non-interactive)

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).

bash
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):

bash
npx novu@latest connect \
  --ci \
  --runtime ai-sdk \
  --channel slack

Empty-dir scaffold with OpenAI (example):

bash
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:

text
NOVU_CONNECT_AI_SDK_REQUIREMENTS_FILE=<absolute path>

or

text
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.

Step B5 — Wire the bridge

Goal: satisfy every unchecked requirement so the channel reaches the user's code.

  1. Read the requirements file from the NOVU_CONNECT_*_REQUIREMENTS_FILE= line (do not paste the path to the user).
  2. Note the agent identifier from the connect success output: Agent: <name> (<identifier>).
  3. Complete every - [ ] item in the file (install packages, env, dev script).
  4. For 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.
  5. Use the agent identifier from step 2 in the handler (agent('<identifier>', …)).
  6. Scaffolded (empty-dir) projects: if 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.
  7. Verify: run the project's 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.

Step B6 — Report the result (bridge)

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/novu route and agent handler — run npm 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:novu in the new project directory and message the bot in Slack to test."

Include the agent identifier and dashboard URL from the CLI output.


Shared — Channel handoffs

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 omitting --keyless)

Every dashboard OAuth 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, 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:

    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:

    • secureSecure setup page (recommended) — paste the token on the page the CLI printed; it never enters chat.
    • in_chatPaste 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). Keep Awaiting the Connect shell — do not set a timer. When the token lands, stdout prints:

    text
    NOVU_CONNECT_SLACK_CONFIG_TOKEN_SAVED=1
    

    Then Await the OAuth handoff line and copy its value:

    text
    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):

    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:

    • secureSecure setup page (recommended) — paste the token on the page/QR the CLI printed; it never enters chat.
    • in_chatPaste 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:

    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). 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:

    text
    NOVU_CONNECT_SENDBLUE_IMESSAGE_URL=<sms:…>
    NOVU_CONNECT_SENDBLUE_FROM_NUMBER=<+E.164>
    

    Give the user:

    1. The sms: deep link (NOVU_CONNECT_SENDBLUE_IMESSAGE_URL=…) — one tap opens Messages pre-addressed to the agent's number; this is the primary handoff.
    2. Tell them to reply from the phone they gave you (their --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:

    text
    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:

    text
    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:

    text
    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.)


Step M5 — Report the result (managed)

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:

text
✓ 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:

SymptomFix
…requires --prompt "<agent description>"Re-run Step 3 with the positional description.
Authorization timed out / Authorization session expiredUser didn't approve in time — re-run for a fresh auth link.
This environment doesn't have a Novu demo Claude integrationDemo runtime not enabled — enable in dashboard or use BYOK runtime flags.
The Slack App Configuration Token wasn't saved within … secondsRe-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 … secondsRe-run the same command.
We didn't see your email at … within …sRe-run after they send the email.
Telegram token / /start timeoutsRe-run the same command.
We didn't see an inbound iMessage on … within …sUser hasn't replied yet — tell them to text the agent number, then re-run the same command.
WhatsApp signup wasn't completed within … minutesUser 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 … secondsUser 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-fromA 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 disabledWrong API/region, or omit --keyless and use dashboard OAuth / --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.

FlagPurpose
connect "<description>"Positional agent description (managed path only; required in --ci managed runs).
--ciNon-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-keyAPI keys for --llm-auth openai / anthropic on empty-dir scaffold.
--keylessTemporary 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-tokenNon-secure CI escape hatches when user opts in.
--sendblue-api-key / --sendblue-secret-keySendblue 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):

text
NOVU_CONNECT_AI_SDK_REQUIREMENTS_FILE=<absolute path>
NOVU_CONNECT_LANGCHAIN_REQUIREMENTS_FILE=<absolute path>

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 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.
  • Prefer secure setup pages for Slack/Telegram tokens.
  • iMessage (Sendblue) needs a Sendblue account with a provisioned phone line, and its API key + secret key are passed via --sendblue-* flags in chat (no secure page exists headlessly).
  • Keyless is managed-only — bridge agents require dashboard OAuth.
  • Keyless managed data is temporary until claimed via the in-channel sign-up link.
  • Keyless is the default for managed-only flows — pass --keyless unless a dashboard signal applies or the user chose the bridge path (always omit --keyless).

Definition of done

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

Managed agent

You are done when:

  1. The user picked a channel and confirmed the agent description.
  2. Keyless bootstrap succeeded (default, with --keyless), or dashboard OAuth completed (when omitting --keyless).
  3. You delivered channel handoffs (or noted skip / the teams dashboard URL).
  4. Connect shell printed ✓ Your agent is live. (exit 0); CLI poll validated handoffs where applicable.
  5. You reported agent identifier + next step (claim link for keyless, dashboard URL for authenticated), gave a brief recap, and explained how the user can keep going.

Custom code bridge

You are done when:

  1. The user picked a channel and you locked ai-sdk or langchain (and --llm-auth when scaffolding empty dir with a real provider).
  2. Dashboard OAuth completed (never --keyless).
  3. You delivered channel handoffs and the connect shell printed the requirements file path and ✓ Your agent is live..
  4. You Read the requirements file; every - [ ] item is satisfied — on existing projects that includes Writing the bridge route + handler; on scaffolded projects the CLI may already have checked code-wiring.
  5. You told the user to run dev:novu and message the agent on the connected channel.