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.

Hook reference

The iteration argument is an Autonomous.Iteration, which includes generated code, execution status, and traces. In onTrace, iteration is a number instead.

HookArgumentsWhen it runsReturn 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.

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.