Use execution hooks
Observe the AI loop, modify tool inputs and outputs, and control execution with hooks.
Hooks run your code at specific points during execute(). Use them to log tool calls, validate inputs, change generated code, or stop further iterations.
Add hooks
Pass a hooks object to execute() in your conversation handler:
import { Conversation } from '@botpress/runtime'
export default new Conversation({
channel: 'webchat.channel',
handler: async ({ execute }) => {
await execute({
instructions: 'You are a helpful support assistant.',
hooks: {
onBeforeTool: async ({ tool }) => {
console.log(`Calling tool: ${tool.name}`)
},
onIterationEnd: async (iteration) => {
console.log(`Iteration finished: ${iteration.status.type}`)
},
},
})
},
})
Hooks apply to that execute() call. If your handler calls execute() again, pass the hooks again to use the same behavior.
When hooks run
An execution can contain several iterations. This diagram shows the main execution path and where each hook runs:
flowchart TD
accTitle: Execution hook order
accDescr: Each iteration starts with onIterationStart, followed by model generation and onBeforeExecution. Generated code can call tools, with onBeforeTool and onAfterTool surrounding each call. A valid exit invokes onExit before onIterationEnd. The loop can then finish or start another iteration. onTrace observes traces throughout the iteration.
start[onIterationStart] --> model[Model generates code]
model --> before[onBeforeExecution]
before --> code[Run generated code]
code -->|Tool call| beforeTool[onBeforeTool]
beforeTool --> tool[Tool handler]
tool --> afterTool[onAfterTool]
afterTool --> code
code -->|Valid exit| exit[onExit]
code -->|Iteration ends without a valid exit| endIteration[onIterationEnd]
exit --> endIteration
endIteration --> next{Another iteration?}
next -->|Yes| start
next -->|No| done[Execution finishes]
model -. Traces .-> trace[onTrace]
code -. Traces .-> trace
onTrace receives traces as the iteration runs. If the loop needs another iteration, it starts again at onIterationStart. Errors and cancellation can skip later stages, so use try / finally around execute() for cleanup that must run when the call finishes.
Execution hooks run inside the AI loop. For incoming messages, idle nudges, and session expiration, use your conversation handler and lifecycle configuration.
Hook reference
The iteration argument is an Autonomous.Iteration, which includes generated code, execution status, and traces. In onTrace, iteration is a number instead.
| Hook | Arguments | When it runs | Return value |
|---|---|---|---|
onIterationStart | (iteration, controller, context) | Before the model call for an iteration. | A partial iteration object to update the current iteration, or nothing. |
onBeforeExecution | (iteration, controller) | After the model generates code, before that code runs. | { code } to replace the generated code, or nothing. |
onBeforeTool | ({ iteration, tool, input, controller }) | Before a tool handler runs. | { input } to replace the tool input, or nothing. |
onAfterTool | ({ iteration, tool, input, output, controller }) | After a tool returns, before its output goes back to the generated code. | { output } to replace the tool output, or nothing. |
onTrace | ({ trace, iteration }) | As execution traces are produced. | Nothing. |
onExit | ({ exit, result }) | When generated code reaches a valid exit, before the iteration finishes. | Nothing. |
onIterationEnd | (iteration, controller) | After an iteration finishes, before the loop decides what to do next. | Nothing. |
Except for onTrace, hooks can be asynchronous and execution waits for them. The tool hooks and onBeforeExecution expect asynchronous callbacks. Keep awaited work short, since it adds to response time.
The controller is an AbortController for the execution. Calling controller.abort() requests cancellation; it does not undo completed tool calls or messages. To reject a particular tool call before its handler runs, throw from onBeforeTool.
Validate or change tool input
Use onBeforeTool to inspect a tool call and optionally return a replacement input. The following hook expects a tool named searchProducts with a string query input:
import { Autonomous, z } from '@botpress/runtime'
const searchInput = z.object({ query: z.string() })
const hooks: Autonomous.Hooks = {
onBeforeTool: async ({ tool, input }) => {
if (tool.name !== 'searchProducts') return
const parsed = searchInput.parse(input)
const query = parsed.query.trim()
if (!query) {
throw new Error('Provide a non-empty search query.')
}
return { input: { ...parsed, query } }
},
}
Pass hooks alongside your tools in execute(). The tool receives the replacement input. Throwing prevents that call’s handler from running and exposes the error to the AI execution, which may retry. Check permissions in the tool handler as well if the operation can be called from other parts of your application.
Tool inputs and outputs have type unknown in Autonomous.Hooks. Check the tool name and validate the value against its schema before reading or changing fields. See Define tools.
Change tool output
Use onAfterTool to transform a result before the generated code receives it. For example, a lookupOrder tool might return an internal note that the model does not need:
import { Autonomous, z } from '@botpress/runtime'
const orderOutput = z.object({
orderId: z.string(),
status: z.string(),
internalNote: z.string(),
})
const hooks: Autonomous.Hooks = {
onAfterTool: async ({ tool, output }) => {
if (tool.name !== 'lookupOrder') return
const order = orderOutput.parse(output)
return { output: { ...order, internalNote: '' } }
},
}
This changes the returned value; it does not reverse anything the tool already did. onAfterTool is not a general error or cleanup hook. Put cleanup inside the tool handler’s finally block when it must also run on failure.
Inspect generated code
onBeforeExecution runs after generation, including iterations where the model does not call a tool. Read iteration.code to inspect the generated code, or return { code: replacementCode } to replace it with a non-empty string.
const hooks: Autonomous.Hooks = {
onBeforeExecution: async (iteration) => {
console.log(`Generated ${iteration.code?.length ?? 0} characters of code`)
},
}
If you need to run a check before the model is called, use onIterationStart instead.
Stop further iterations
Use onIterationEnd to inspect the iteration’s status and request cancellation before another iteration starts:
const hooks: Autonomous.Hooks = {
onIterationEnd: async (iteration, controller) => {
if (iteration.status.type === 'execution_error') {
controller.abort('Stopping after an execution error')
}
},
}
Returning a value from this hook does not change the iteration’s result. To set a fixed iteration limit, use the iterations option.
Observe traces and exits
Use onTrace for logging and monitoring. It receives events such as model calls, tool calls, and code execution. Its return value is ignored, and execution does not wait for asynchronous work started inside it.
const hooks: Autonomous.Hooks = {
onTrace: ({ trace, iteration }) => {
console.log(`Iteration ${iteration}: ${trace.type}`)
},
onExit: async ({ exit }) => {
console.log(`Reached exit: ${exit.name}`)
},
}
onExit receives the exit definition and its validated result. It runs for built-in and custom exits. It is not a callback for every way execution can stop: cancellation, a model failure, or reaching the iteration limit may end execution without it. If it throws, the exit fails and the loop can continue with the error as context.
You can also inspect execution in the Dev Console’s logs and traces.
Reuse hooks
Define shared hooks with the Autonomous.Hooks type and pass them to each execution that needs them:
import { Autonomous } from '@botpress/runtime'
const hooks: Autonomous.Hooks = {
onBeforeTool: async ({ tool }) => {
console.log(`Calling tool: ${tool.name}`)
},
}
// Inside your conversation handler:
await execute({
instructions: 'You are a helpful support assistant.',
hooks,
})
For counters or other data that should reset on each handler invocation, create the hooks inside the handler.
Conversation lifecycle
To run code when a message arrives, put it in your conversation handler before execute(). To react to inactivity, configure lifecycle and handle props.type === 'nudge' or props.type === 'expire' in that handler.
See Manage conversation lifecycle for a complete example with timers and session management.