Krispy AI
guides

lead forms + connectors

define data-driven forms the bot can raise, and wire where captured leads go.

lead forms + connectors

a form is a data-driven card the bot can raise with [!FORM:<id>] (booking, quote, demo). connectors decide where a captured lead goes. both live in your tenant config in KV โ€” no code, no redeploy.

define a form

a form is a FormSpec: an id (what [!FORM:<id>] references), a title, an ordered list of fields, and optionally connectorIds to scope which connectors receive its leads (default = all configured).

{
  "forms": [
    {
      "id": "book",
      "title": "book a call",
      "fields": [
        { "name": "name", "label": "your name", "type": "text", "required": true },
        { "name": "email", "label": "email", "type": "email", "required": true },
        {
          "name": "budget",
          "label": "rough budget",
          "type": "select",
          "options": ["<5k", "5โ€“20k", "20k+"]
        },
        { "name": "notes", "label": "anything else", "type": "textarea" }
      ],
      "connectorIds": ["tg-main", "sales-email"]
    }
  ]
}

field type is one of text | email | tel | textarea | select (select uses options). the widget renders the form entirely with createElement/textContent (never innerHTML), so field labels/options are XSS-safe.

the form renders as a card inside the message log โ€” it scrolls with the conversation like a bubble (chat continues past it) instead of pinning between the log and the composer. on successful delivery it collapses in place to a compact one-line record: successText if set, else a built-in "Thanks โ€” we'll be in touch." If email delivery fails, the form keeps the visitor's values and offers a retry. Up to the ten most recent chat messages accompany the lead email, so the visitor need not repeat the question.

teach the bot to raise it

the bot only offers a form if it knows the form exists. the server injects the list of your forms into the system prompt automatically (<id> (<title>)), instructing the model to append [!FORM:<id>] when a visitor is ready for a concrete next step it can't complete in chat. you don't wire this by hand โ€” configuring a form is enough.

connectors โ€” where a lead goes

{
  "connectors": [
    { "id": "tg-main", "type": "telegram" },
    { "id": "sales-email", "type": "email", "toAddress": "sales@you.com" },
    { "id": "wa", "type": "whatsapp", "phone": "15551234567" },
    { "id": "ig", "type": "instagram", "profileUrl": "https://instagram.com/you" }
  ]
}
typefieldsbehavior
telegramโ€” (uses the top-level bot token)the lead is posted into the visitor's topic
emailtoAddressrendered + sent via Resend (silent no-op without RESEND_API_KEY)
whatsappphone (E.164 digits, no +)a wa.me CTA link in the widget; also a "Reply on WhatsApp" button in the lead email
instagramprofileUrla profile-link CTA with the filled Instagram glyph and a comfortable mobile tap target

telegram and email are delivery channels โ€” the lead is sent for you. whatsapp and instagram are CTA-only: the widget renders them as links for the visitor to tap; nothing is delivered server-side for them, and they never leave the server as data.

An operator can send an existing form or Instagram CTA while talking with a visitor. The app lists the forms and visitor-facing Instagram connectors configured for that conversation's site. Selecting one places a card in the chat transcript. The form uses the same /api/lead delivery path as a bot-offered form, and the Instagram button uses the connector's configured HTTPS profile URL. Cards survive reconnects.

Lead emails show labeled fields and the recent conversation. To include Buttr beside the Krispy wordmark, set the edge Worker's EMAIL_ASSET_ORIGIN to a public HTTPS host serving /brand/buttr-chill.png. Email clients that block images still show the text wordmark; unset or non-HTTPS origins omit the image.

While a conversation waits for a team member, the widget offers a compact "Talk to the team" card using only the site's configured forms and visitor-facing CTAs. A form choice expands the existing form in the chat transcript; /api/lead still receives the recent conversation. A visitor can also request an audio call when the site's call settings allow it and the call status confirms availability. Unconfigured choices stay hidden. The pending choice card closes when a team member takes over or the AI resumes; an expanded form keeps the visitor's input.

the two capture paths

  • [!FORM:<id>] โ†’ the widget renders your FormSpec; on submit it POSTs to /api/lead, which fans the values out to the scoped connectors.
  • [!HANDOFF] with no form โ†’ the widget offers configured contact choices while the team is pending. It does not create a default contact form or delivery route. /api/contact remains as a back-compat shim for already-deployed widgets.

write the config

push it with the CLI (it merges โ€” unset fields are preserved):

KRISPY_API=https://krispy-edge.YOU.workers.dev TENANT_SYNC_SECRET=... \
  bun packages/cli/src/index.ts set-kbase ./kbase.md

set-kbase writes the systemPrompt; for forms/connectors/theme you POST the same shape to /api/tenant/config (see reference โ†’ edge routes).

On this page