Scaffold a custom experience with an AI app builder

Hand an AI app builder (Lovable, v0, Bolt, Cursor) a specific brief + a constraints contract and have it generate a real Landbot experience with @landbot/core — worked end-to-end as a real-estate lead-gen app.

AI app builders are great at React + Tailwind UI and terrible at APIs they've never seen — they'll wire up a slider in seconds and then invent a customer.ui field that doesn't exist. Two things flip that: a specific brief (so it builds your experience, not a generic skeleton) and a constraints contract (so the Landbot wiring is actually correct). This recipe gives you both, worked end-to-end as a concrete example — an engaging lead-gen experience for a real estate company — that you can paste as-is or adapt by swapping the brief.

The example mirrors the live property-search demo: a calm two-pane app where the chat drives a purpose-built widget per step (a budget slider, a property-card grid, a contact form) instead of a wall of text bubbles.

Note Division of labour. The AI builds the front-end. You still build the bot flow in the Landbot dashboard (see Build a conversational app for the block setup) and supply its config URL (Share → Embed → the …/index.json value). No tool can do the bot-builder part for you.

The prompt

Paste this into Lovable / v0 / Bolt / Cursor and fill in your config URL. It's tool-agnostic. Notice the shape: a specific brief (the experience + a widget per step + the vibe), then the contract (the rules that make the Landbot wiring correct). To build something else, keep the contract and rewrite the brief.

Build an engaging lead-generation experience for a real-estate company — a polished
React + Tailwind single-page app powered by the Landbot Core SDK (@landbot/core). It
should feel like a product, not a chat widget: a calm two-pane layout with a slim
conversation transcript on the left and a large "canvas" on the right that renders a
purpose-built UI for each step. Warm, trustworthy real-estate aesthetic; generous
whitespace; smooth transitions between steps; fully responsive.

A bot I built in Landbot drives the flow and puts a `[[ui:VALUE]]` marker at the start
of each step's message. Render this widget per value, and make each one feel alive:

  location      → search input for a city or neighbourhood (with a subtle map vibe)
  property-type → large tappable image cards: Apartment, House, Studio
  bedrooms      → a segmented control: Studio, 1, 2, 3+
  budget        → a dual-handle € range slider with live min–max labels
  timeline      → choice chips: ASAP, 1–3 months, Just browsing
  results       → a responsive grid of property cards (photo, price, beds, location)
  contact       → a form: name, email, phone, with light validation
  goodbye       → a warm confirmation that recaps the choices they made

Bot config URL (fetch it for the config): <PASTE YOUR …/index.json URL HERE>
Read first: https://dev.landbot.io/guides/cookbook/conversational-app.md
and https://dev.landbot.io/llms-full.txt
Visual target to match in polish: https://landbot-realtor-demo.netlify.app

Honor these constraints EXACTLY — they are non-obvious and the usual cause of broken
Landbot integrations:
1. Named import only: `import { Core } from "@landbot/core"`. A default import fails.
2. Subscribe to `core.pipelines.$readableSequence` BEFORE calling `core.init()`.
3. The stream carries BOTH bot and user messages — discriminate on `msg.author_type`
   ("bot" | "user" | "agent" | "sys").
4. Dedupe on `msg.key`; the stream can replay a message.
5. There is NO structured customer field on the stream. The per-step signal is the
   `[[ui:VALUE]]` marker at the START of the bot message body — read it with a regex
   and strip it before displaying the text.
6. Render a step's input widget ONLY when the bot is waiting for input:
   `msg.type === "dialog" || msg.type === "multi_question" || msg.extra?.textarea`.
   Every other message is informational — show it in the transcript, the bot keeps going.
7. Send replies with `core.sendMessage({ message })` for text, or
   `core.sendMessage({ type: "button", message, payload })` for a choice
   (payload comes from the matching entry in `msg.payloads`).
8. NEVER eval message content. Ignore `type:"hidden"` messages; a `hidden` message with
   `action:"finish"` means the conversation ended — render the goodbye step.

Each widget collects its answer and calls send(...) to advance the flow. Keep the
transcript and the canvas in sync.

Finally, also output a BOT FLOW SPEC I can build in Landbot so the bot emits exactly the
markers this UI expects. For each step give: the block type (Ask a Question / Buttons /
Multi-Question / Send a Message), the exact message text INCLUDING the `[[ui:VALUE]]`
marker at the start, the field to save the answer to, and any button labels. Use the same
`ui` values and field names as the widgets above — the UI and the bot must agree.

The first half is the brief — it's what makes the output a real estate experience rather than a generic shell, so be opinionated here (steps, widgets, aesthetic). The numbered half is the contract — eight rules, each a behaviour verified against the live SDK (see Build a conversational app). Drop the contract and the tool gets rules 2, 5, and 8 wrong almost every time; drop the brief and you get a skeleton.

Note The bot has to emit those ui values. The widget names in the brief (location, budget, results, …) must match the [[ui:…]] markers your bot puts in each step's prompt. Build the flow to match — the property-search demo ships a block-by-block guide (realtor/BOT_SETUP.md) for this exact eight-step flow, and Build a conversational app covers the marker mechanic.

Tip Feed it the right URLs. Point the tool at llms-full.txt (the whole docs corpus as one Markdown file) or a specific recipe's .md URL — both return real Markdown. Don't hand it arbitrary page URLs: the docs are a single-page app, so an unknown or mistyped path returns a 200 HTML shell rather than a 404, and the tool would ingest the shell as if it were content.

The matching bot flow

A front-end is only half of it — ship the bot too, or the dispatch silently misses. Here's the complete flow for the real-estate example; build it once in the dashboard (~15 min) and the front-end above works against its config URL. Each input block's prompt starts with the literal [[ui:…]] marker — no Code block needed (see Build a conversational app).

# Block Message text (marker + prompt) Save to Buttons
1 Ask a Question (text) [[ui:location]] Welcome 👋 Where are you looking — city or neighbourhood? location
2 Buttons [[ui:property-type]] Great, @{location}. What type of place? property_type Apartment · House · Condo · Townhouse
3 Buttons [[ui:bedrooms]] How many bedrooms? bedrooms Studio · 1 · 2 · 3 · 4+
4 Ask a Question (number) [[ui:budget]] What's your maximum budget (€)? budget
5 Buttons [[ui:timeline]] When are you hoping to move? timeline ASAP · 1–3 months · Just browsing
6 Buttons [[ui:results]] Here are some matches — want the details? interested Yes please · Not yet
7 Multi-Question [[ui:contact]] Where should we send them? name, email, phone
8 Send a Message → Close chat Thanks @{name}! We'll send @{property_type} options in @{location} up to €@{budget} shortly. 👋
  • Step 8 (goodbye) is a plain Send Message that recaps the answers via @{field} interpolation, then Close chat — which emits the hidden/finish message the front-end shows as its goodbye screen. No [[ui:goodbye]] marker is needed; the finish event drives that view.
  • Step 6 (results) only signals the grid — the listings themselves come from your own data on the front-end (the realtor demo keeps a mock listings array), filtered by the answers so far. The bot just marks the step and waits on a yes/no.
  • The widget keys in the brief (location, property-type, budget, …) match these markers and fields exactly. Change one, change both — that's the whole contract between the two halves.

For the full eight-step production version (dual-handle budget, richer copy, every field), the property-search demo ships its complete build guide in realtor/BOT_SETUP.md.

A reference implementation it can lean on

If your tool accepts a code seed, give it this. It's the conversational-app recipe in React: all the Landbot-specific wiring lives in one hook, and the rest is ordinary components the builder can restyle freely.

import { useEffect, useRef, useState } from "react";
import { Core } from "@landbot/core";

const CONFIG_URL = "PASTE_YOUR_BOT_CONFIG_URL"; // the …/index.json from Share → Embed

const UI = /^\s*\[\[ui:([^\]]+)\]\]\s*/;
const readUi  = (t) => (("" + (t || "")).match(UI) || [])[1] || null;
const stripUi = (t) => ("" + (t || "")).replace(UI, "");
const isWaiting = (m) => m.type === "dialog" || m.type === "multi_question" || !!m.extra?.textarea;

// Every Landbot-specific rule lives in this hook. The components below are just React.
function useLandbot(configUrl) {
  const [log, setLog] = useState([]);
  const [step, setStep] = useState({ ui: "default", msg: null });
  const core = useRef(null);
  const seen = useRef(new Set());

  useEffect(() => {
    let instance;
    (async () => {
      const config = await fetch(configUrl).then((r) => r.json());
      instance = new Core(config);
      core.current = instance;
      // SUBSCRIBE before init() — the welcome messages fire during init().
      instance.pipelines.$readableSequence.subscribe((m) => {
        if (seen.current.has(m.key)) return;
        seen.current.add(m.key);
        if (m.author_type === "user") return setLog((l) => [...l, { who: "user", text: m.message }]);
        if (m.author_type === "sys") return;
        if (m.type === "hidden") { if (m.action === "finish") setStep({ ui: "finish", msg: null }); return; }
        setLog((l) => [...l, { who: "bot", text: stripUi(m.message) }]);
        if (isWaiting(m)) setStep({ ui: readUi(m.message) || "default", msg: m });
      });
      await instance.init();
    })();
    return () => instance?.destroy?.();
  }, [configUrl]);

  const send = (m) => core.current?.sendMessage(typeof m === "string" ? { message: m } : m);
  return { log, step, send };
}

export default function App() {
  const { log, step, send } = useLandbot(CONFIG_URL);
  return (
    <div className="flex h-screen">
      <aside className="w-1/3 overflow-y-auto p-4 space-y-2">
        {log.map((m, i) => <div key={i} className={m.who === "user" ? "text-right" : ""}>{m.text}</div>)}
      </aside>
      <main className="flex-1 grid place-items-center p-8">
        <Canvas step={step} send={send} />
      </main>
    </div>
  );
}

function Canvas({ step, send }) {
  if (step.ui === "finish") return <p>All done 🎉</p>;
  if (step.ui === "budget") return <BudgetSlider onSubmit={(v) => send(String(v))} />;
  const m = step.msg;
  if (m?.type === "dialog")
    return (
      <div className="flex gap-2">
        {m.buttons.map((b, i) => (
          <button key={i} onClick={() => send({ type: "button", message: b, payload: m.payloads?.[i] ?? `$${i}` })}>{b}</button>
        ))}
      </div>
    );
  return <TextReply onSubmit={send} />;
}

function BudgetSlider({ onSubmit }) {
  const [v, setV] = useState(1500);
  return (
    <div className="grid gap-3 w-72">
      <input type="range" min={500} max={5000} step={100} value={v} onChange={(e) => setV(+e.target.value)} />
      <output>€{v}</output>
      <button onClick={() => onSubmit(v)}>Set budget</button>
    </div>
  );
}

function TextReply({ onSubmit }) {
  const [v, setV] = useState("");
  return (
    <input
      value={v}
      placeholder="Type a reply…"
      onChange={(e) => setV(e.target.value)}
      onKeyDown={(e) => e.key === "Enter" && v.trim() && onSubmit(v.trim())}
    />
  );
}

For a full-scale, production-grade version of exactly this pattern — eight steps, a bespoke component each, real styling — point the tool at the property-search demo (React + Tailwind + shadcn/ui; source in realtor/). It's the closest reference to what these builders output, so it adapts cleanly.

Why this beats "just share the docs"

Two ways this goes wrong on its own: hand a builder the whole site and it skims, misses the load-bearing rules, and ships something that renders but doesn't talk to the bot; hand it a vague prompt ("build a Landbot experience") and it ships a generic skeleton. The prompt above fixes both — a specific brief (the real-estate experience, a widget per step, the vibe) so the output is yours, plus the eight-rule contract so the wiring is correct — and the seed gives it working wiring to adapt rather than invent. The bot half stays with you; the UI half becomes a few minutes of generation plus restyling.

Reference: Build a conversational app · Build a custom chat UI · For AI agents · Core overview