- Direct send - your handler explicitly sends the component
- LLM-driven - the LLM decides when to render the component during
execute()
Creating a component
A custom component has two files: a React component (.bp.tsx) and an ADK wrapper (.ts).
Step 1: Write the React component
Create a.bp.tsx file in src/components/:
.css files directly. CSS imports are bundled and injected as <style> tags at runtime. React 18 is available with hooks like useState, useMemo, and useEffect.
Step 2: Create the ADK wrapper
Create a.ts file next to the .bp.tsx:
adk dev and adk deploy. The component name is derived from the React function name (TicketCard in this case).
Sending components manually
Send a custom component like any other message:props object is type-checked against the React component’s Props type.
LLM-driven components
To let the LLM decide when to render your component, add LLM metadata and list it in the conversation’scomponents array.
Add LLM metadata
Thedescription field tells the LLM what the component does:
TicketCard.ts
| Field | Required | Description |
|---|---|---|
description | Yes | Tells the LLM when to use this component. Be specific. |
props | Yes | Zod schema defining the component’s props. Use .describe() on each field. |
exampleValues | Yes | Array of example prop objects. Converted to JSX examples in the LLM prompt. |
Provide the component to the LLM
To provide the component to your agent’s LLM, pass it into theexecute() function’s components field:
<TicketCard> and will render it when appropriate during execute().
If you list a component in
components that was created without LLM metadata, the Conversation constructor throws immediately.Combining both approaches
You can use direct send and LLM-driven components in the same conversation:WelcomeBannerComponent doesn’t need LLM metadata since it’s only sent directly. It’s not in the components array.
Build pipeline
Duringadk dev and adk deploy, custom components go through the following pipeline:
- Discover - scans
src/for.tsfiles that exportCustomComponentinstances - Resolve - finds the
.bp.tsximport in each wrapper - Bundle -
esbuildbundles the.bp.tsxinto standalone ESM (reactandreact-domare externalized) - Upload - the bundle is uploaded to Botpress as a public file
- Wire - the component URL is injected so
conversation.send()works
adk dev, the file watcher rebuilds only changed components incrementally.
Tips
- Keep components focused. One component, one purpose. A ticket card, a status badge, a product listing.
- Use
useMemofor expensive computations or random values. React may re-render multiple times. - Name your component function. The name is derived from the function name. Anonymous exports become
"UnnamedComponent". - Write good descriptions. The LLM reads the
descriptionfield to decide when to use the component. - Provide realistic examples. Use values that represent real usage, not placeholders like
"string". - Avoid reserved prop names. The webchat renderer reserves
status. Use more specific names liketicketStatus.
Limitations
- Webchat only. Custom components only render in Webchat. Other channels don’t support them.
- No global stylesheets. You can import
.cssfiles in your component, but your project’s global styles are not available. - React 18. React 19 features are not available.
- No server-side rendering. Components are client-rendered in the browser.
Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
Component "X" not deployed. Run "adk deploy". | Component URL not set | Re-run adk dev to build and upload |
| Component doesn’t appear in Webchat | Wrapper doesn’t export a CustomComponent | Check the .ts file imports the .bp.tsx and exports new CustomComponent(...) |
| LLM never uses the component | Missing metadata or not in components array | Add { description, props, exampleValues } and list in components |
| Styles look wrong | Using CSS classes | Switch to inline styles |
A prop is undefined | Reserved prop name (e.g., status) | Rename to something specific (e.g., ticketStatus) |
TypeScript errors on .bp.tsx | Missing tsconfig settings | Add "jsx": "react", "allowImportingTsExtensions": true, and "noEmit": true |