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.
override async onCall(call: Call) {
this.patient = await this.agenda().byPhone(call.from ?? "");
if (this.patient) this.stage = "choose";
}
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.
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:
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
awaitthesayhere. It resolves when the sentence has been spoken, and nothingonCallwrote 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.