Skip to main content
By the end of this page, you have a travel concierge running on Vercel. It asks travelers questions with buttons, reads their home airport from the page, and drops destination cards into the conversation. It takes about 30 minutes (packing not included). Before you start, you need:
  • A Cadenya account and the CLI, logged in to your Development workspace. The introduction covers both.
  • An enabled model that supports tool calling. Run cadenya models list to see yours.
  • Node.js 22.12 or later.
  • A Vercel account and the Vercel CLI: npm install -g vercel.
Want to see where you’re headed first? The travel concierge demo is a bigger version of what you build here.

What a Widget is

A Widget puts a Cadenya Agent inside your web app. It binds one Agent to its own host (something like adbtaawrmh4h.widgets.cadenya.com) and to a list of web origins allowed to talk to it. Your backend has one job: mint a Widget Session. It calls Cadenya with your private API key and hands the browser a short-lived token and the Widget’s host. From then on, the browser and the Widget host talk to each other. Messages, tool calls, and the live event stream never pass through your server. Your Cadenya API key never leaves your server. The interesting part is the tools. A Bare tool has no HTTP endpoint and no MCP server behind it. It waits for your application to supply the result. In a Widget, that application is the browser, so the page itself can answer the agent: a button the traveler clicks, a value sitting in React state, a card that renders and says “done”. That’s the whole trick of this guide. The Widgets guide goes deeper on sessions, lifetimes, and revocation.

The project

Create a Next.js app and install three packages: the server SDK, the React UI kit, and Radix Themes (the kit builds on it).
By the end, the project looks like this:
The cadenya/ folder isn’t part of the app. It holds the JSON you feed the CLI, so the agent’s configuration lives in Git next to the code that depends on it.

Deploy to Vercel

Deploy now, before there’s anything to deploy. Why so early? Because a Widget only answers requests from origins on its allowlist, and the allowlist takes exact origins, not wildcards. You need your production URL before you create the Widget.
Accept the defaults. When the deploy finishes, Vercel prints the production URL, something like https://travel-concierge.vercel.app. Copy it. You use it in the next section.
Every Vercel preview deployment gets its own URL, and each one is a separate origin. Add the ones you want to test against to the Widget’s allowlist, or test previews against localhost and keep the allowlist to production.

Create the agent and Widget

The concierge needs four things in Cadenya: a Tool Set the page can answer, an Agent, a variation, and a Widget.

Add the Widgets Tools template

You don’t have to write the tools. The Widgets Tools template is a Bare Tool Set built for Widget pages, and the React UI kit already knows how to render it. Open it in your workspace: app.cadenya.com/w/default/toolSets/templates/widgets-tools That default in the URL isn’t a typo. Cadenya swaps it for your default workspace (pick yours under Preferences on your profile page), so one link in the docs opens the page in your workspace. Keep the defaults and click Add to workspace. You get a Tool Set with the external ID widget-tools and three tools, one for each way a page can answer an agent:
  1. cdy_widget_ask_user asks the traveler up to four questions and waits. Each question is radio buttons, checkboxes, or a text field, and the answers go back as {"answers": {"<question id>": ...}}.
  2. cdy_widget_display_details shows a card: a title, fields, markdown, an image, and link buttons. Nobody needs to answer a card, so the tool answers the agent the moment it’s called.
  3. cdy_widget_get_page_context reads whatever the page chooses to share. The traveler doesn’t click anything: the page answers on its own.
The template also turns on widget argument exposure for the whole Tool Set. Without it, the browser never sees the arguments the agent passes to a tool, and the question card has nothing to render. (The cdy_widget_ prefix keeps the names from colliding with your own tools.)

Create the Agent and the rest

The Agent is the role, and its variation carries the prompt. Put both in the cadenya/ folder:
cadenya/agent.json
cadenya/variation.json
Create everything in order. Every command refers to resources by their external IDs, including the widget-tools Tool Set from the template, so you never copy a generated ID around. Set MODEL_ID to a model from cadenya models list, and put your Vercel URL in the last command:
A Widget only binds to a published Agent, which is why agents publish comes before widgets create.

Mint a Widget Session

The browser can’t hold your API key, so it asks your server for a session instead. First, give the server its credentials. Create an API key in your Development workspace with the widget_sessions:manage scope, then fill in .env.local:
.env.local
cadenya auth status prints your workspace ID. The SDK reads all three variables on its own. Now the route. It mints a session for the Widget and returns the two values the browser needs:
app/api/session/route.ts
This route mints a session for anyone who asks, and everyone is the same traveler, so everyone shares one conversation history. That’s fine for a quickstart. In your real app, check who’s asking first and use their IDs for the tenant and subject. Start the dev server and mint one:
You should see a token and a host (trimmed here):
Always use the host from the response. Don’t build it from the Widget ID. Then give Vercel the same three variables, so production can mint sessions too:

Give the session to the Widget

CadenyaWidgetProvider takes the host and token and builds the browser client. ConversationsPanel is the whole chat: the conversation list, the message thread, the composer, and the live event stream.
components/concierge.tsx
The Widget host has no refresh endpoint. getToken goes back to your server, which is the point: your app decides every time whether this visitor still gets a token. Replace app/page.tsx with a page that gives the panel room. The panel fills its container’s height, so the container needs a definite one. Without it, the panel grows with the conversation and pushes the composer off the screen.
app/page.tsx
Open http://localhost:3000 and say hi. The agent answers. Now ask it to plan a weekend away. It calls cdy_widget_get_page_context, and then… nothing. A Bare tool waits until something supplies its result, and nothing on the page does yet. Time to fix that.

Register frontend tool handlers

The UI kit gives you two ways to answer a tool from the browser:
  1. Tool components render a tool call as a React component. The component gets the call’s arguments and a submit function. Use them when the traveler answers.
  2. Page tools are plain handlers. The panel runs them the moment the agent calls the tool and sends back whatever they return. Use them when the page answers.
The Widgets Tools template needs both, and <WidgetTools> registers all three in one element: the question card and the details card as tool components, and cdy_widget_get_page_context as a page tool. It lives in its own entry point, so apps that don’t use the template don’t bundle it. Here’s the finished components/concierge.tsx, with a home airport picker whose value becomes the page context:
components/concierge.tsx
<WidgetTools> and the panel have to share one PageToolsProvider. The provider is how the panel finds the renderers and handlers the element registers. pageContext takes anything. A string goes to the agent as-is, and anything else gets JSON-encoded. Leave it out and the tool still answers, saying the page shares nothing, so the agent never waits on it. toolPlacement="inline" keeps each tool call in the thread where it happened. The default, "activity", shows a call above the composer and clears it when the agent replies, which suits a progress chip but not a card the traveler wants to scroll back to. Inline calls also come back when a conversation reopens. They don’t run again: page tools only fire for calls that are still running.

Bring your own tools

The template covers questions, cards, and page context. When you need something else, add your own Bare tool to a Tool Set and answer it with the same two hooks <WidgetTools> uses. Say the agent can save a destination to the traveler’s shortlist with a save_destination tool:
components/shortlist-tool.tsx
Render it next to <WidgetTools>, inside the same PageToolsProvider. Whatever the handler returns becomes the tool’s result, and a thrown error reaches the agent as {"error": "..."}. For a tool the traveler answers, register a component with useToolComponent (or the panel’s toolComponents prop) and call its submit prop with the answer. To restyle the template’s own cards, pass your components to <WidgetTools components={{ askUser, displayDetails }} />.

Watch it plan a trip

Pick Tokyo (NRT), then ask:
The agent reads your airport from the page context, asks a question or two (budget, pace, beach or city), and then shows destination cards within a short flight of Tokyo. Change the airport to London (LHR) and ask again. The suggestions follow you. When it works locally, ship it:

Styling

Styling comes in three layers. Start at the top and stop when it looks right.

The Radix Theme

The surrounding <Theme> is the main styling API. The kit’s CSS only references Radix tokens, so these five props restyle everything at once:
appearance="inherit" follows a light or dark class on a parent element, which is what you want if your site already has a theme toggle. The Radix Themes docs list every accent and gray.

Panel props

ConversationsPanel takes two props for its own look. composer picks the input style, and bubbleColors takes any CSS background, gradients included:
"pill" fuses the input and send button into one rounded capsule. "floating" lifts that capsule onto a shadowed card and turns the agent’s bubbles into cards.

CSS

Everything else is CSS. The kit’s class names all start with cdny- (.cdny-panel, .cdny-bubble-user, .cdny-composer, .cdny-ask-user, .cdny-details, and friends), and they’re a stable contract. Bubble colors also exist as CSS variables:
app/globals.css
Hand the class to the panel with className="concierge-panel". The same prop is where sizing and placement go.

Where to go next

Your concierge hands a session to anyone who asks for one. The Widgets guide shows how to pass trusted context from your own auth into every conversation: tenants, subjects, pinned parameters, and per-visitor secrets.