Cookbooks · Agents that do things

An agent that books appointments

An agent that finds a free appointment time and books it while the caller is still on the line. It has two tools: one reads your calendar, one writes to it. Here the calendar is three slots in memory, so there is nothing else to set up.

Before you start

  • An agent, and its ID. The Quickstart makes one.
  • A workspace API key in the CRACKLE_API_KEY environment variable. See Authentication.
  • Node.js 18 or later, with "type": "module" in package.json, and npm install express.
  • A token of your own choosing in TOOL_TOKEN. Your endpoints use it to check a request really comes from the agent.
  1. Write the two endpoints

    These are ordinary HTTP endpoints on your server. The agent never sees their addresses or the token; it only knows each tool’s name, what it is for and what it returns.

    tools-server.js
    import express from 'express';
    
    const app = express();
    app.use(express.json());
    
    const slots = [
      { id: 'wed-10', label: 'Wednesday at 10 am', bookedBy: null },
      { id: 'wed-1130', label: 'Wednesday at 11:30 am', bookedBy: null },
      { id: 'thu-16', label: 'Thursday at 4 pm', bookedBy: null },
    ];
    
    // Only Crackle knows this token: it is each tool's authToken.
    app.use('/tools', (req, res, next) => {
      if (req.get('authorization') !== `Bearer ${process.env.TOOL_TOKEN}`) return res.sendStatus(401);
      next();
    });
    
    app.get('/tools/slots', (req, res) => {
      const free = slots.filter((slot) => !slot.bookedBy).map(({ id, label }) => ({ id, label }));
      res.json({ free });
    });
    
    app.post('/tools/book', (req, res) => {
      const { slot: id, name, callId } = req.body;
      const slot = slots.find((item) => item.id === id);
    
      // Answer 200 with the reason, so the agent can tell the caller what went wrong.
      if (!slot) return res.json({ booked: false, reason: 'There is no such time.' });
      if (slot.bookedBy) return res.json({ booked: false, reason: 'That time has just been taken.' });
    
      // A test call from the studio is not a real caller: say yes, save nothing.
      if (req.get('x-goblin-test-call') !== 'true') slot.bookedBy = { name, callId };
      res.json({ booked: true, label: slot.label });
    });
    
    app.listen(3000);

    Notice what the endpoints send back. Times carry a label written the way it should be said aloud, and a booking that fails answers 200 with a reason. A 4xx or 5xx tells the agent little more than that the tool failed.

  2. Create the agent and its tools

    One script makes the agent and gives it both tools. The instructions mention each tool as @name, which is what switches a tool on for the agent and tells the model when to reach for it.

    setup.js
    const API = 'https://goblins-call-api.onrender.com';
    const headers = {
      Authorization: `Bearer ${process.env.CRACKLE_API_KEY}`,
      'Content-Type': 'application/json',
    };
    
    async function api(method, path, body) {
      const response = await fetch(`${API}${path}`, { method, headers, body: body && JSON.stringify(body) });
      const data = await response.json();
      if (!response.ok) throw new Error(`${method} ${path}: ${data.error}`);
      return data;
    }
    
    const { agent } = await api('POST', '/v1/agents', {
      name: 'Sunrise Dental front desk',
      systemInstruction: [
        'You answer the phone for Sunrise Dental and book appointments.',
        'When a caller wants an appointment, use @check_slots and offer at most two of the free times.',
        'Once they pick one and you have their name, use @book_slot.',
        'Tell them it is booked only after @book_slot says so. If the time was taken, apologise and offer another.',
        'Keep every reply to one or two short sentences.',
      ].join('\n'),
      greetingInstruction: 'Greet the caller and ask how you can help, in one short sentence.',
      voice: { language: 'en' },
      systemTools: { endCall: true },
    });
    
    const shared = { agentId: agent.id, auth: 'bearer', authToken: process.env.TOOL_TOKEN, timeoutMs: 5000 };
    
    await api('POST', '/v1/tools', {
      ...shared,
      name: 'check_slots',
      description: 'Lists the appointment times that are still free. Use it before offering any time.',
      method: 'GET',
      url: `${process.env.PUBLIC_URL}/tools/slots`,
      sampleOutput: '{"free":[{"id":"wed-10","label":"Wednesday at 10 am"}]}',
    });
    
    await api('POST', '/v1/tools', {
      ...shared,
      name: 'book_slot',
      description: "Books one free time for the caller. Needs the id of a time from check_slots and the caller's name.",
      method: 'POST',
      url: `${process.env.PUBLIC_URL}/tools/book`,
      params: [
        { key: 'slot', value: 'wed-10' },
        { key: 'name', value: 'Divya Rao' },
      ],
      // call_id comes from the call itself, not from the model.
      body: '{"slot": "{{slot}}", "name": "{{name}}", "callId": "{{call_id}}"}',
      sampleOutput: '{"booked":true,"label":"Wednesday at 10 am"}',
    });
    
    console.log('Agent ready:', agent.id);
    Terminal
    export CRACKLE_API_KEY="ck_your_key_here"
    export TOOL_TOKEN="a-long-random-string"
    export PUBLIC_URL="https://your-tunnel.example"
    node tools-server.js &
    node setup.js
  3. Check each tool on its own

    Before talking to the agent, run each tool once. Test a tool calls your endpoint exactly as a call would and shows what came back, which separates “my endpoint is wrong” from “the agent used it wrongly”.

    curl -X POST "https://goblins-call-api.onrender.com/v1/tools/YOUR_TOOL_ID/test" \
      -H "Authorization: Bearer $CRACKLE_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
      "params": {
        "slot": "wed-10",
        "name": "Divya Rao"
      }
    }'

Try it

Open the agent in the studio and press Test agent, or call it from the page in A talk button for your website. Ask for an appointment. You should hear a short holding line while it checks, then an offer of free times, and a confirmation once you pick one. On a call started through the API, ask for that same time again on a second call and it has gone.

A test call from the studio carries X-Goblin-Test-Call: true, so the endpoint above answers “booked” without saving anything. A call started through the API saves for real.

Making it real

  • Replace the array with your calendar or booking system. Keep the answers small: the agent reads about the first 1,800 characters of a response.
  • Have /tools/book be safe to call twice with the same callId and slot, in case a caller repeats themselves.
  • Start each call with what you already know as variables, such as the caller’s name, so the agent does not have to ask. They can be used in a tool’s body the same way {{call_id}} is.
  • Keep endpoints fast. The caller is waiting in silence after the holding line, and a tool is cut off at 30 seconds at the very most.