pinecallDocs
Key concepts / Hooks & events

Hooks & events

What runs when a call opens and ends, and facts your backend sends in mid-call.

Hooks are methods Pinecall calls at fixed moments: when a call starts, when it ends, and when a fact your agent declared arrives from outside the conversation. Like tools, they may write state.

What is a hook?#

A method of the class with a reserved name. Override the ones you need; the rest do nothing.

hook runs it gets
onCall(call) when the call starts, before the model reads anything id, contact, from, channel, medium
onEnd(call) when the call ends the same
onEvent(name, data, meta) when an event the class declares arrives, from a sender it allows the payload, and meta: source, identity, seq

A transfer, a hold or a keypress is not a hook. It is a verb of this.call that your tool calls.

Opening the call#

onCall is where a call is prepared: the caller's file looked up, a returning caller recognised. What it writes is in the first prompt the model reads.

agents/clinica-norte/agent.tsx
override async onCall(call: Call) {
  this.patient = await this.agenda().byPhone(call.from ?? "");
  if (this.patient) this.stage = "choose";
}
agents/clinica-norte/agent.py
def on_call(self, call: CallWorld) -> None:
    self.patient = self.agenda.by_phone(call.from_ or "")
    if self.patient:
        self.stage = "choose"

The hooks are on_call(self, call), on_end(self, call), on_event(self, name, data, meta) and on_memory(self, ops, call), each a def or an async def.

agents/clinica-norte/agent.rb
def on_call(call)
  self.patient = agenda.by_phone(call.from)
  self.stage = :choose if patient
end

The hooks are on_call(call), on_end(call), on_event(name, data, meta) and on_memory(ops, call).

An opening that depends on who calls is say or reply inside onCall, with no greeting in the settings:

TypeScript
override async onCall(call: Call) {
  this.patient = await this.agenda().byPhone(call.from ?? "");
  void this.say(this.patient ? `Hi ${this.patient.name}, how can I help?` : "Clínica Norte, good morning.");
}

Do not await the say here. It resolves when the sentence has been spoken, and nothing onCall wrote reaches the prompt until the hook returns.

If a greeting is also set, the caller hears both: the greeting first, then the hook's sentence. pinecall agent clear greeting makes the hook the only opening.

onCall runs once per call. When a call moves to another process mid-conversation, the new process restores the state instead and does not run onCall again; onEnd runs wherever the call ends.

Events from outside the call#

While a call is going on, your backend can tell the agent something: a payment cleared, a cart changed, a slot freed. The class declares each event it accepts, and who may send it:

import { Agent, type EventMeta } from "@pinecall/agents";
export default class Shop extends Agent {
  static events = { "cart.changed": { from: ["app"] } };
  cart: Cart | undefined;
  override onEvent(name: string, data: Record<string, unknown>, meta: EventMeta) {
    if (name === "cart.changed") this.cart = data["cart"] as Cart;
  }
}
from pinecall import Agent, EventMeta
class Shop(Agent):
    accepts = {"cart.changed": ["app"]}
    cart: dict | None = None
    def on_event(self, name: str, data: dict, meta: EventMeta) -> None:
        if name == "cart.changed":
            self.cart = data["cart"]
class Shop < Pinecall::Agent
  accepts "cart.changed", from: %i[app]
  state :cart
  def on_event(name, data, meta)
    self.cart = data[:cart] if name == "cart.changed"
  end
end
sender from sends it with
your backend "app" the call.event command
a participant's browser "participant" pinecall.event, from the page in the call

The pair is the gate. An event declared from: ["app"] that arrives from a browser is somebody else's event with your name on it: it never reaches onEvent, and your process logs once that it was dropped. Events run one at a time, in the order they arrived.

Who writes what#

Writes inside a hook are authored by it: hook:onCall, hook:onEnd, event:cart.changed. A change made by an event is logged with the event that caused it. Anywhere else, an assignment to state throws UnauthoredWrite (see State & visibility).

Watching in process#

agent.on("state", …) hears every change, and agent.on("event", …) every event after its hook ran. Each returns a function that unsubscribes. They are for code in your own process, such as a test.

Next steps#