Events
FlowKit emits events throughout the conversation lifecycle. Use events for logging, analytics, debugging, and custom integrations.
Event Types
| Event | Emitted When |
|---|---|
flow:start | Conversation begins |
flow:end | Conversation ends (reached .done()) |
step:enter | Entering a new step |
step:exit | Leaving a step |
extract:success | Data extracted successfully |
extract:fail | Extraction failed |
tool:call | Tool is about to be called |
tool:result | Tool returned a result |
user:message | User sent a message |
agent:message | Agent sent a response |
handoff:detected | Handoff to human requested |
handoff:transfer | Handoff transfer executed |
timeout | Timeout occurred (message, session, inactivity) |
error | An error occurred |
Listening to Events
Pass an onEvent handler when creating the FlowEngine:
typescript
import { FlowEngine, EngineEvent } from "@andresaya/flowkit";
const engine = new FlowEngine(flow, {
llm: adapter,
storage,
onEvent: (event: EngineEvent) => {
console.log(`[${event.type}]`, event);
}
});Event Handler
The onEvent function receives all events. Filter by event.type:
typescript
const handleEvent = (event: EngineEvent) => {
switch (event.type) {
case "flow:start":
console.log(`Session ${event.conversationId} started`);
break;
case "flow:end":
console.log(`Session ${event.conversationId} ended`, event.slots);
break;
case "step:enter":
console.log(`Entering step: ${event.step}`);
break;
case "step:exit":
console.log(`Exiting step: ${event.step} -> ${event.next}`);
break;
case "extract:success":
console.log(`Extracted ${event.extractType} = ${event.value}`);
break;
case "extract:fail":
console.log(`Failed to extract: ${event.reason}`);
break;
case "user:message":
console.log(`User said: ${event.text}`);
break;
case "agent:message":
console.log(`Agent said: ${event.text}`);
break;
case "error":
console.error(`Error:`, event.error);
break;
}
};
const engine = new FlowEngine(flow, {
llm: adapter,
storage,
onEvent: handleEvent
});Event Data
flow:start
typescript
{
type: "flow:start",
conversationId: string,
step: string
}flow:end
typescript
{
type: "flow:end",
conversationId: string,
slots: Record<string, JsonValue>
}step:enter
typescript
{
type: "step:enter",
conversationId: string,
step: string
}step:exit
typescript
{
type: "step:exit",
conversationId: string,
step: string,
next: string
}extract:success
typescript
{
type: "extract:success",
conversationId: string,
extractType: string,
value: JsonValue
}extract:fail
typescript
{
type: "extract:fail",
conversationId: string,
reason: string
}user:message
typescript
{
type: "user:message",
conversationId: string,
text: string
}agent:message
typescript
{
type: "agent:message",
conversationId: string,
text: string
}tool:call
typescript
{
type: "tool:call",
conversationId: string,
tool: string,
payload: JsonValue
}tool:result
typescript
{
type: "tool:result",
conversationId: string,
tool: string,
result: JsonValue
}handoff:detected
typescript
{
type: "handoff:detected",
conversationId: string,
trigger: string,
userMessage: string
}timeout
typescript
{
type: "timeout",
conversationId: string,
timeoutType: "message" | "session" | "inactivity",
elapsed: number
}error
typescript
{
type: "error",
conversationId: string,
error: Error
}Use Cases
Logging
typescript
import winston from "winston";
const logger = winston.createLogger({ /* config */ });
const engine = new FlowEngine(flow, {
llm: adapter,
storage,
onEvent: (event) => {
switch (event.type) {
case "flow:start":
logger.info("Flow started", { conversationId: event.conversationId });
break;
case "flow:end":
logger.info("Flow ended", { conversationId: event.conversationId, slots: event.slots });
break;
case "step:enter":
logger.debug("Step enter", { conversationId: event.conversationId, step: event.step });
break;
case "error":
logger.error("Error", { conversationId: event.conversationId, error: event.error.message });
break;
}
}
});Analytics
typescript
import { track } from "./analytics";
const engine = new FlowEngine(flow, {
llm: adapter,
storage,
onEvent: (event) => {
switch (event.type) {
case "flow:start":
track("conversation_started", { sessionId: event.conversationId });
break;
case "flow:end":
track("conversation_completed", {
sessionId: event.conversationId,
dataCollected: Object.keys(event.slots).length
});
break;
case "extract:success":
track("data_extracted", { sessionId: event.conversationId, field: event.slot });
break;
}
}
});Real-time Updates
typescript
import { WebSocket } from "ws";
const connections = new Map<string, WebSocket>();
const engine = new FlowEngine(flow, {
llm: adapter,
storage,
onEvent: (event) => {
const ws = connections.get(event.conversationId);
if (!ws) return;
switch (event.type) {
case "step:enter":
ws.send(JSON.stringify({ type: "step_change", step: event.step }));
break;
case "agent:message":
ws.send(JSON.stringify({ type: "message", text: event.text }));
break;
case "flow:end":
ws.send(JSON.stringify({ type: "completed" }));
ws.close();
connections.delete(event.conversationId);
break;
}
}
});Debugging
typescript
// Enable verbose logging in development
const engine = new FlowEngine(flow, {
llm: adapter,
storage,
onEvent: process.env.NODE_ENV === "development"
? (event) => {
console.log(`[${new Date().toISOString()}] ${event.type}`,
JSON.stringify(event, null, 2));
}
: undefined
});Error Alerting
typescript
import { sendAlert } from "./alerting";
const engine = new FlowEngine(flow, {
llm: adapter,
storage,
onEvent: async (event) => {
if (event.type === "error") {
await sendAlert({
level: "error",
message: `Flow error in session ${event.conversationId}`,
error: event.error.message,
stack: event.error.stack
});
}
if (event.type === "extract:fail") {
await sendAlert({
level: "warning",
message: `Extraction failed for ${event.slot}`,
sessionId: event.conversationId,
error: event.error
});
}
}
});Metrics Collection
typescript
import { Counter, Histogram } from "prom-client";
const flowsStarted = new Counter({
name: "flowkit_flows_started_total",
help: "Total flows started"
});
const flowsCompleted = new Counter({
name: "flowkit_flows_completed_total",
help: "Total flows completed"
});
const stepTimers = new Map<string, number>();
const stepDuration = new Histogram({
name: "flowkit_step_duration_seconds",
help: "Time spent in each step",
labelNames: ["step_id"]
});
const engine = new FlowEngine(flow, {
llm: adapter,
storage,
onEvent: (event) => {
switch (event.type) {
case "flow:start":
flowsStarted.inc();
break;
case "flow:end":
flowsCompleted.inc();
break;
case "step:enter":
stepTimers.set(`${event.conversationId}:${event.step}`, Date.now());
break;
case "step:exit":
const start = stepTimers.get(`${event.conversationId}:${event.step}`);
if (start) {
const duration = (Date.now() - start) / 1000;
stepDuration.labels(event.step).observe(duration);
stepTimers.delete(`${event.conversationId}:${event.step}`);
}
break;
}
}
});Tips
- Don't block - Keep event handlers fast and use async operations
- Handle errors - Wrap handlers in try/catch to prevent crashes
- Filter wisely - Only process events you need
- Type safety - Use the
EngineEventtype for proper TypeScript support - Performance - For high-volume apps, batch analytics events case "timeout": console.log(
Timeout: ${event.timeoutType}); break;