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.jsonvalue). 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
uivalues. 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.mdURL — 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 a200HTML shell rather than a404, 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 thehidden/finishmessage 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
listingsarray), 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