> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kugelaudio.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Recipes for hosted agents

> Recipes for common hosted-agent integrations. A call link for chats and emails, the event a call comes from, what you already know about a logged-in visitor, one widget per channel, a recording notice, a feedback question, your own API with a secret, and a test copy before a change goes live.

Each recipe puts together what the other hosted-agent pages describe, for one common
integration. They are written so that your [coding agent](/hosted-agents/coding-agent)
can follow them: ask it to apply one to your project.

## Send a call link in a chat or an email

A link cannot start a call by itself: a browser lets a page use the microphone only after
the visitor clicks. So the link opens a page of yours with the widget on it, and one click
starts the call.

1. Make a page on a website your widget accepts calls from, for example
   `https://example.com/call` (see [which sites can start calls](/hosted-agents/web-widget#which-sites-can-start-calls)).
2. Put the widget on it inline, so that its button sits in the page. Start from your
   widget's code in the dashboard, which carries every attribute your widget needs, and
   add the inline position, the label and the title:

   ```html
   <!doctype html>
   <html lang="en">
     <head>
       <meta charset="utf-8">
       <meta name="viewport" content="width=device-width, initial-scale=1">
       <title>Talk to our assistant</title>
     </head>
     <body>
       <main>
         <h1>Talk to our assistant</h1>
         <p>Press the button and allow your microphone.</p>
         <div data-kugelaudio-widget></div>
       </main>

       <script src="https://cdn.kugelaudio.com/widget/v1/widget.js"
         data-agent="YOUR_WIDGET_ID"
         data-position="inline"
         data-size="lg"
         data-label="Start the call"
         data-title="Our assistant"
         async></script>
     </body>
   </html>
   ```

3. Send the page's address in your chat or email.

`data-label` is the button's text and `data-title` the heading of the call panel that
opens from it. The visitor's click on that button starts the call. A button of your own
design can start it too, by calling the widget's `start()`; see
[Start a call from your own button](/hosted-agents/web-widget#start-a-call-from-your-own-button).

## Tell the agent which event a call comes from

Attendees of a webinar click a call link, and the agent should know which webinar they come
from without asking. Name the event in the link, let the widget pass it on, and let the agent
look it up.

1. Make the call page from [the call link recipe](#send-a-call-link-in-a-chat-or-an-email)
   and add `data-context-params="event"` to its widget tag. The widget then copies the page
   address's `event` parameter into the call's context (see
   [Tell the agent where a call comes from](/hosted-agents/web-widget#tell-the-agent-where-a-call-comes-from)):

   ```html
   <script src="https://cdn.kugelaudio.com/widget/v1/widget.js"
     data-agent="YOUR_WIDGET_ID"
     data-position="inline"
     data-size="lg"
     data-label="Start the call"
     data-context-params="event"
     async></script>
   ```

2. Send each event's attendees their own link, such as
   `https://example.com/call?event=webinar-11-04` for one webinar and
   `https://example.com/call?event=webinar-11-17` for the next. Keep email addresses, names
   and anything else personal out of the link: whoever has the link can read it, and the
   context is stored with the call. To tell attendees apart, add a token that means something
   only to your own systems, such as `&t=8f3k2q`, and name it too:
   `data-context-params="event,t"`.

3. In the agent, map the event ids of your links to your own words, and render the sentence
   for the one the call came from. `useClient()` reads the call's context; its `event` is the
   link's `event`:

   ```tsx
   import { Text, useClient } from "curb";

   // Your own words for the events your links name. Any other value renders nothing.
   const WEBINARS: Record<string, string> = {
     "webinar-11-04": "our webinar on getting started",
     "webinar-11-17": "our webinar on advanced setups",
   };

   export function WebinarContext() {
     const { event } = useClient();
     const webinar = WEBINARS[event ?? ""];
     return webinar ? (
       <Text>The caller comes from {webinar}. Mention it in your greeting and answer questions about it.</Text>
     ) : null;
   }
   ```

   Render `<WebinarContext />` inside `<Agent>` in `src/agent/agent.tsx`, after
   `<SystemPrompt>`.

The agent knows the event from its first sentence. Anyone can edit a link, so the event is a
hint and never a reason to unlock anything: a value your table does not know renders nothing,
and the agent talks as it would without one.

## Pass what you already know (a logged-in user)

A visitor who is logged in on your website should not have to spell their email address
for the agent. Give the widget what your page already knows, and let the agent use it
instead of asking.

1. On the page, set the widget's `context` property once you know who is logged in, and set
   it to `null` when they log out:

   ```js
   customElements.whenDefined('kugelaudio-agent').then(() => {
     document.querySelector('kugelaudio-agent').context = { email: user.email, name: user.name }
   })
   ```

   The next call to start carries it (see
   [Tell the agent where a call comes from](/hosted-agents/web-widget#tell-the-agent-where-a-call-comes-from)).
   The property stays in the page's memory and is never written into its HTML, so a session
   recording, which captures the HTML, does not capture the address. For the same reason, keep
   personal details out of the tag's `data-context`, even on a page rendered on the server.

2. In the agent, `useClient()` reads the call's context, each key without the `client.`
   prefix. Tell the model the address so it does not ask:

   ```tsx
   import { Instruction, useClient } from "curb";

   export function KnownCaller() {
     const { email } = useClient();
     // Text from the page: only something shaped like an address gets in.
     if (!email || !/^[^\s@"]+@[^\s@"]+$/.test(email)) return null;
     return <Instruction>{`The caller is logged in with the email address "${email}". Use it and do not ask for it.`}</Instruction>;
   }
   ```

   Render `<KnownCaller />` inside `<Agent>` in `src/agent/agent.tsx`, after
   `<SystemPrompt>`. Without an address it renders nothing, and the agent works as before.

3. Try it locally. `--client` gives the conversation the context a page would send:

   ```bash
   npx curb say --new --client email=max@example.com "Hi"
   ```

   Continue with a request that needs the address: the agent uses max@example.com instead of
   asking for it.

The page sets the context, and anyone who controls the page can change it. Use it to prefill
and to skip questions, never as proof of who the caller is or to unlock their account's data;
to act on an account, check the caller in a tool against your own system. Send only what the
agent needs: the context is stored with the call.

## Compare channels with one widget per channel

Give each channel its own widget: one for the button on your website, one for the link
in your emails, one for the link in your chats. Every call records the id of the widget
it came through, so you can tell the channels apart.

1. Create one widget per channel. The dashboard's **Add to your website** names a widget
   after its website, and an agent's widgets need different names, so let your coding
   agent create the second and later widgets for the same website: `add_agent_website`
   takes a name, such as `Email link` or `Chat link`.
2. Put each widget's code only where its channel leads: the website widget on your site,
   the email widget on the page your emails link to (`https://example.com/call/email`),
   and so on.
3. To see where a call came from, open it on the agent's **Calls** page and choose the
   **IDs** button. **Endpoint** is the id of the widget the call came through, the same id
   as in that widget's `data-agent`. `list_agent_websites` lists every widget's id with its
   name.

## Show a recording notice before the call

The widget has no consent step: a click on its button starts the call. Put your notice
on the page above the button, so that visitors read it before they click. No call starts
and no microphone opens before that click.

Whether a call is recorded is decided when the call starts, by the agent's
**Settings** > **Recording and retention**. A call that starts while the agent keeps
nothing has no transcript, audio or activity afterwards. Write the notice to match what
that section says.

```html
<p>
  This call is recorded and transcribed so that we can help you. Our
  <a href="https://example.com/privacy">privacy policy</a> says how long we keep it.
</p>
<div data-kugelaudio-widget></div>
```

To ask for an explicit yes, show the button only once the visitor agrees. Keep the
widget's script tag with `data-position="inline"` on the page: the widget also fills a
`data-kugelaudio-widget` element that is added later.

```html
<label><input type="checkbox" id="agree"> I agree that this call is recorded.</label>
<div id="call"></div>

<script>
  document.getElementById('agree').addEventListener('change', (event) => {
    const call = document.getElementById('call')
    if (!event.target.checked || call.firstChild) return
    const spot = document.createElement('div')
    spot.setAttribute('data-kugelaudio-widget', '')
    call.append(spot)
  })
</script>
```

A phone call has no page. Let the agent speak first on a call (`firstTurn: "agent"` in
`src/agent/agent.tsx`, as the template has it) and tell it in its `<SystemPrompt>` to say
that the call is recorded before anything else. The model says it in its own words.

## Ask for feedback at the end of a call

Make the question a tool. When the caller answers, the agent calls the tool, and the
tool's arguments are stored with the call.

In `src/agent/agent.tsx`, put the tool in `<GlobalTools>`, beside `<Skills>`, so that it
exists wherever the conversation ends. `GlobalTools`, `Tool` and `z` come from `"curb"`.

```tsx
<GlobalTools>
  <Tool
    name="record_feedback"
    description="Record the caller's answer to the feedback question at the end of the call."
    schema={z.object({
      rating: z
        .number()
        .int()
        .min(1, "Ask for a number from 1 to 5.")
        .max(5, "Ask for a number from 1 to 5.")
        .describe("The caller's rating, from 1 (poor) to 5 (excellent)."),
      comment: z
        .string()
        .max(500)
        .optional()
        .describe("Anything else the caller said about the call, in their words."),
    })}
    success="Feedback recorded."
    onSubmit={(input) => ({ ok: true, data: input })}
  />
</GlobalTools>
```

Then tell the agent when to ask, in its `<SystemPrompt>`:

```text
When the caller has what they called for, ask: "Before you go, how would you rate this
call from 1 to 5?" Call record_feedback with the answer, then say goodbye and end the
call. If the caller does not want to answer, end the call without asking again.
```

Open a call on the agent's **Calls** page: its **Activity** tab shows `record_feedback`
with the rating and the comment. A call that starts while the agent keeps nothing stores
no activity (see [the recording notice](#show-a-recording-notice-before-the-call)). To
keep every answer in a system of your own, send it there from `onSubmit`, as in the next
recipe.

## Call an HTTPS API with a secret

A tool can call any HTTPS API on the internet (see
[what your agent can reach](/hosted-agents/deploy#what-your-agent-can-reach)). Keep the
API's credential in a variable on the agent, and read it only inside the tool. This
example asks a calendar API for free slots with an app-only credential, one that belongs
to your application rather than to a person.

1. Put the credential in `.env`, which `npm run dev` reads:

   ```bash
   CALENDAR_CLIENT_ID=...
   CALENDAR_CLIENT_SECRET=...
   ```

   Send it to the agent with `npx curb secrets push`, or set it on the agent's
   **Variables** page.
2. Add the names to `curb.requiredRuntimeSecrets` in `package.json`. A version whose
   required variables are not set on the agent never goes live, so a forgotten secret
   fails the deploy rather than a call.

   ```json
   {
     "curb": {
       "requiredRuntimeSecrets": [
         "ANTHROPIC_API_KEY",
         "KUGELAUDIO_API_KEY",
         "CALENDAR_CLIENT_ID",
         "CALENDAR_CLIENT_SECRET"
       ]
     }
   }
   ```

3. Read the variables in the tool, each time it runs:

   ```tsx
   import { defineTool, z } from "curb";

   // An app-only token from the calendar's sign-in server (OAuth client credentials).
   async function calendarToken(): Promise<string> {
     const clientId = process.env.CALENDAR_CLIENT_ID;
     const clientSecret = process.env.CALENDAR_CLIENT_SECRET;
     if (!clientId || !clientSecret) throw new Error("CALENDAR_CLIENT_ID or CALENDAR_CLIENT_SECRET is not set");
     const response = await fetch("https://login.example.com/oauth2/token", {
       method: "POST",
       body: new URLSearchParams({
         grant_type: "client_credentials",
         client_id: clientId,
         client_secret: clientSecret,
       }),
     });
     if (!response.ok) throw new Error(`Calendar token request failed with HTTP ${response.status}`);
     const { access_token } = (await response.json()) as { access_token: string };
     return access_token;
   }

   export const findFreeSlots = defineTool({
     name: "find_free_slots",
     description: "Find the free appointment slots on one day.",
     schema: z.object({
       date: z
         .string()
         .regex(/^\d{4}-\d{2}-\d{2}$/, "Ask which day, and pass it as YYYY-MM-DD.")
         .describe("The day, as YYYY-MM-DD."),
     }),
     onSubmit: async ({ date }) => {
       const response = await fetch(`https://calendar.example.com/v1/free-slots?date=${date}`, {
         headers: { Authorization: `Bearer ${await calendarToken()}` },
       });
       if (!response.ok) return { ok: false, message: "The calendar is not answering. Offer to call back later." };
       const { slots } = (await response.json()) as { slots: string[] };
       return { ok: true, data: { slots } };
     },
   });
   ```

   Render it in the skill that books appointments with `<Tool def={findFreeSlots} />`.

Keep the secret out of everything the model reads:

- Never put a secret in the system prompt, an instruction, a skill's text, or a tool's
  result or message. The model reads all of them, and it may say aloud anything it reads.
- Keep it out of error messages and logs: what your agent prints goes to its logs. An
  error the tool throws reaches the model only as a generic failure.
- Read it from `process.env` inside the tool, as above, never when the file loads. A
  hosted agent may load your files before its variables are set.

## Test a change on a test copy first

Keep a second agent as a test copy. Deploy each change to it first, call it, and deploy
the same project to your live agent only once it works. Until then your live agent keeps
answering its numbers and widgets with the version it has.

**With your coding agent:** ask it to create the test copy (`create_agent`), give it the
same variables (`set_agent_secrets`), deploy the project to it and give you its
Playground link. Once your call works, ask it to deploy the same project to the live agent.

**From a terminal:**

```bash
cat .curb/link.json      # "agentId" is your live agent's id: note it
rm .curb/link.json
npx curb secrets push    # choose "Create a new agent" and name it, for example my-agent-test
npx curb deploy          # deploys the test copy and ends with its Playground link
```

`curb secrets push` prints the test copy's id when it creates it. Call the test copy in
its Playground. When it works, deploy the same project to the live agent:

```bash
npx curb deploy --agent <live-agent-id>
```

`--agent` also links the project to the agent you name, so name one on every `deploy`
and `secrets push` from then on: `--agent <test-agent-id>` first, then
`--agent <live-agent-id>`.

The test copy needs the same variables as the live agent. Its calls are billed like any
other (see [pricing](https://kugelaudio.com/pricing)). To call it from your phone, add
your phone to its [test line](/hosted-agents/phone-numbers#call-your-agent-from-your-own-phone).

## Next steps

<CardGroup cols={2}>
  <Card title="Web widget" icon="globe" href="/hosted-agents/web-widget">
    Every widget option and where it can start calls
  </Card>
  <Card title="Deploy from your terminal" icon="terminal" href="/hosted-agents/deploy">
    Every option of `curb login`, `curb secrets push` and `curb deploy`
  </Card>
</CardGroup>
