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.

Registering a tool call or event

# After your agent makes a tool call
response = requests.post(
f"{base_url}/test-results/{test_result_id}/conversation",
headers={"Authorization": f"Bearer {api_key}"},
json={
"type": "tool",
"name": "search_knowledge_base",
"metadata": {
"query": "API authentication",
"results_count": 5,
"execution_time_ms": 234
}
}
)

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 sending the next message to Voxli:

# 1. Get message from Voxli
voxli_message = response.json()["message"]
# 2. Send to your agent
agent_response = your_agent.process(voxli_message)
# 3. Record any tool calls your agent made
requests.post(
f"https://api.voxli.io/test-results/{test_result_id}/conversation",
json={"type": "tool", "name": "cancel-order", "metadata": {...}}
)
# 4. Record your agent's response
requests.post(
f"https://api.voxli.io/test-results/{test_result_id}/conversation",
json={"type": "message", "content": agent_response.text}
)
# 5. Get next message from Voxli

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.

Test your Agent

External Links