After-call code

After-call code

afterCall and afterEvaluation are two functions in your agent’s code that run once a call is over. Use them to update a CRM, send a summary or file a ticket. They live in your project, deploy with your agent and read the same variables.

Add a hook

Add one or both to defineAgent in src/agent/agent.tsx:

import { defineAgent } from "curb";

export const agent = defineAgent({
  app: BookingAgent,
  goals: {
    booked: { description: "The caller booked a slot", tool: "book_slot" },
  },

  // Right after the call: the transcript and the call data are final.
  afterCall: async (call) => {
    console.log(`Call ${call.id} took ${call.duration_seconds} s`);
  },

  // Once the goals are judged: the same call, with the results.
  afterEvaluation: async (call) => {
    const booked = call.goals?.find((goal) => goal.name === "booked");
    console.log(`Booked: ${booked?.status}`);
  },
});
HookRunscall.goals
afterCallRight after the call, on call.endednull
afterEvaluationOnce the goals are judged, on call.evaluatedAn array, [] when the agent has no goals

Both run for every call, including one that failed, so check call.status when that matters. A hook runs apart from the call: nothing it does reaches the caller, and a hook that fails does not affect the call.

A hook is a plain async function, not a React component, so it cannot use useVariant or any other React hook.

What a hook receives

A hook is called with (call, event). event is the whole event, the same JSON a webhook receives, and call is its data.object. Field names are snake_case and times are Unix seconds.

FieldMeaning
event.idStays the same every time this event is run. Use it to do the work once
event.typecall.ended for afterCall, call.evaluated for afterEvaluation
call.idThe call
call.statuscompleted or failed
call.livemodefalse for a test call
call.direction, call.channelHow the call came in, such as inbound and sip
call.started_at, call.ended_at, call.duration_secondsWhen it ran, null for a call that never started
call.clientWhat the caller’s page passed to the call, {} when nothing
call.experimentsThe variant of each experiment, {} when none
call.transcript{ complete, turns }, where each turn has role, text and created
call.call_dataWhat the agent sent with send.toHost, as { name, payload }. payload is null for a message too large to keep
call.goalsEach goal’s name, status and detail. status is reached, not_reached, not_judged or not_evaluable

call.transcript is null when the call stores nothing, and then call.content is not_stored and call.call_data is []. It is also null for a transcript too long to send, and then call.transcript_truncated is true.

The types come from curb, for a hook in a file of its own:

import type { CallHook } from "curb";

export const logCall: CallHook = async (call, event) => {
  console.log(event.type, call.id);
};

Do the work once

A hook can run more than once for the same event: a run that failed is run again, and in rare cases a run that worked is too. Key the work on event.id, so the second run changes nothing:

afterCall: async (call, event) => {
  const response = await fetch(`${process.env.CRM_URL}/calls`, {
    method: "POST",
    headers: { "idempotency-key": event.id, "content-type": "application/json" },
    body: JSON.stringify({ id: call.id, status: call.status }),
  });
  if (!response.ok) throw new Error(`The CRM answered HTTP ${response.status}`);
},

Throw when the work failed, so the run counts as failed and can be tried again.

Time, retries and order

A run has 120 seconds. Past that it fails with timeout. A throw fails it too, with the thrown error’s message, which holds no stack trace and no value of the runner’s environment.

On KugelAudio a run gets 3 attempts, the second after 30 seconds and the third 5 minutes later, and a call’s afterEvaluation starts after its afterCall has finished. There is no order between different calls. After-call code needs a plan that includes it: on any other plan a hook does not run, and its error says so.

On your own servers, your host decides when a hook runs and how often it is retried: see Hook runner.

Read your variables

A hook reads the agent’s variables from process.env, as a tool does. Add the ones it needs to curb.requiredRuntimeSecrets in package.json, so a forgotten one fails the deploy rather than a run.

Try it locally

curb dev and curb say never fire a hook, so a test conversation cannot write to your real CRM. Run one yourself on a call you describe:

npx curb hook afterCall --call fixtures/call.json

The fixture can be a whole event, or only the fields your hook reads, and Curb fills in the rest:

{ "status": "failed" }

The hook gets your .env, so point it at a test system. The last line says how the run ended, and the exit code is 0 when it succeeded and 1 when it threw. To check a hook in a test of your own, use runHook.

Next steps