Voxli Voxli

Tools, Events, and Actions

Record what your agent does during a conversation so Voxli can score it, and register the affordances your UI shows so the tester can use them.

There are three things you can attach to a chatbot turn:

  • Tool calls (type: "tool") - a function call your agent made, such as a database lookup or an API request. Used to score tool-use assertions.
  • Events (type: "internal-event" or type: "public-event") - anything else you want to record. Internal events are only visible to the scoring agent. Public events are also visible in the conversation history (for example a form or widget that appeared in the chat).
  • Actions (registered via metadata.actions on an event) - invokable affordances like buttons or forms that the tester can pick instead of typing a reply.

All three are recorded on the same endpoint: POST /test-results/{id}/conversation. Actions are different only in that they also change what Voxli returns from next-message.

What tool calls are

Tool calls represent actions your agent takes during a conversation, like searching a database, calling an API, or retrieving documents. If you register tool calls, assertions can judge whether they were called correctly and in the expected order.

If your bot executes tools, record each call using its real execution hooks or traces. Do not infer calls from reply text. For new bridges, use metadata.id, metadata.arguments, and metadata.response. Use the runtime’s call ID, or assign one when capturing the execution if the runtime has none. Existing bridges can preserve other metadata fields, such as input and output.

Hallucination detection also reads tool metadata as evidence for the bot’s claims. Post each tool entry before the reply that uses its output. If the platform exposes only replies or observed actions, report that limit and do not claim internal tool verification is complete.

If a hosted integration already records tools in Voxli, verify its session is linked to the test and omit those same calls from your bridge’s entries.

Registering a tool call or event

# Use values captured from an actual tool execution.
response = requests.post(
f"{base_url}/test-results/{test_result_id}/conversation",
headers={"Authorization": f"Bearer {api_key}"},
json={
"type": "tool",
"name": tool_name,
"metadata": {
"id": tool_call_id,
"arguments": tool_arguments,
"response": tool_response,
},
},
timeout=30,
)
response.raise_for_status()

Request fields

  • type (required) - "tool" for tool calls, "internal-event" or "public-event" for events.
  • name (required) - identifier for the tool or event (for example "search_database").
  • metadata (optional) - any JSON payload up to 100KB - tool arguments, results, timing data, etc.

When to register

Register tool calls after your agent executes them but before posting the bot’s reply to Voxli. The bridge in Local posts entries in the order returned by your adapter.

When data arrives after the chat

Set completionMode to manual with PATCH /test-results/{id} before the conversation ends. After end_chat: true, attach late tools and events through POST /test-results/{id}/conversation. Use before or after with an existing message ID to put them in the correct order. Use PATCH /test-results/{id}/conversation/{message_id} to merge message metadata.

Finish these writes before calling POST /test-results/{id}/finish to queue scoring. Inserting or updating entries is rejected once processing starts. Posting late entries alone does not delay automatic scoring.

Registering actions

An action is an affordance your UI presents alongside a chatbot turn - a button, a quick-reply chip, a small form. Instead of typing a free-text reply, the tester can invoke one of the actions you register. This lets you exercise non-text flows (picking a menu option, submitting a form) the same way you exercise a text conversation.

Register actions by attaching a list to metadata.actions on a public-event or internal-event. The registry is scoped to the current chatbot turn only - every tester reply (text or action) clears it. Re-register the same actions on the next turn if they should remain available.

"""
Register invokable actions on the chatbot side and handle the tester's
response.
The tester may either type a free-text reply or invoke one of the actions
you registered this turn. `next-message` returns exactly one of `message`
or `action` - never both.
"""
import os
import requests
base_url = os.getenv("VOXLI_API_URL", "https://api.voxli.io")
headers = {"Authorization": f"Bearer {os.getenv('VOXLI_API_KEY')}"}
test_result_id = os.getenv("VOXLI_TEST_RESULT_ID")
conversation_endpoint = f"{base_url}/test-results/{test_result_id}/conversation"
next_message_endpoint = f"{base_url}/test-results/{test_result_id}/next-message"
# 1. Record the chatbot's reply as a message.
requests.post(
conversation_endpoint,
headers=headers,
json={
"type": "message",
"content": "Would you like to reschedule or cancel this appointment?",
},
)
# 2. Register the buttons your UI shows alongside that reply as a
# public-event. The `metadata.actions` list scopes to this turn only.
requests.post(
conversation_endpoint,
headers=headers,
json={
"type": "public-event",
"name": "appointment_choice",
"metadata": {
"actions": [
{
"name": "reschedule",
"description": "Reschedule the appointment to a new time.",
"properties": {
"new_time": {
"type": "string",
"description": "ISO 8601 timestamp for the new slot.",
},
},
"required": ["new_time"],
},
{
"name": "cancel",
"description": "Cancel the appointment.",
},
],
},
},
)
# 3. Ask Voxli for the tester's next move. The response contains either
# `message` (free text) or `action` (an ActionInvocation), never both.
response = requests.post(next_message_endpoint, headers=headers).json()
if response.get("end_chat"):
print("Conversation ended.")
elif response.get("action"):
invocation = response["action"]
name = invocation["name"]
arguments = invocation.get("arguments", {})
if name == "reschedule":
new_time = arguments["new_time"]
# Apply the action in your system, then tell the chatbot what
# happened so its next reply is grounded.
confirmation = your_agent.reschedule(new_time)
requests.post(
conversation_endpoint,
headers=headers,
json={"type": "message", "content": confirmation},
)
elif name == "cancel":
confirmation = your_agent.cancel()
requests.post(
conversation_endpoint,
headers=headers,
json={"type": "message", "content": confirmation},
)
else:
tester_message = response["message"]
# Feed the tester's text into your agent as usual.
agent_reply = your_agent.process(tester_message)
requests.post(
conversation_endpoint,
headers=headers,
json={"type": "message", "content": agent_reply},
)

Action definition fields

Each entry in metadata.actions must include:

  • name (required) - unique identifier within the turn. Must be snake_case: start with a letter or underscore, then letters, digits, or underscores.
  • description (required) - short human-readable text. The tester LLM sees this when choosing whether to invoke the action.
  • properties (optional) - map of argument name to a JSON Schema primitive (string, number, integer, or boolean) with an optional description. Nested objects, arrays, enums, and formats are not supported.
  • required (optional) - list of argument names that must be supplied. Each name must appear in properties.

Duplicate names within a single event, or re-registering a name already registered earlier in the same turn, both return 400.

Handling action invocations

POST /test-results/{id}/next-message returns exactly one of message or action each turn. When the tester picks an action, the response looks like:

{
  "message": null,
  "action": {
    "name": "reschedule",
    "arguments": { "new_time": "2026-05-02T14:00:00Z" }
  },
  "end_chat": false,
  "ready": true
}

Apply the action in your system, then record your chatbot’s follow-up reply on the conversation endpoint as usual. The tester’s invocation is already recorded by Voxli - you do not need to post it back.

If the tester supplies arguments that do not match the declared schema (unknown action, missing required argument, wrong type), the test is aborted and marked failed. Treat a schema error as a signal that either the registered definition or the tester prompt needs fixing.

Example: checking a destructive tool call

Test whether a cancellation question triggers a destructive tool call shows a conversation where asking about cancellation must not trigger the tool. Record the tool calls your agent makes so tool-call assertions can check that behavior.

Test your Agent

External Links