Tools & Execution Logs
The three tool types a customer can create, where each one is configured, when each one runs, and which Logs tab shows its executions.
Last verified: 2026-08-24
Why This Doc Exists
Tool configuration used to be spread across four pages (/webhooks, /mcp-integrations, /tool-library, and the Agent Editor), each with its own list UI and its own copy of the delivery-log table. The copies drifted: date filters behaved differently, status pills were styled differently, and two of the four tool types had no log view at all.
Everything is now consolidated behind /tools (configuration) and /logs (execution history). This doc is the single source of truth for which surface owns what. The per-system docs WEBHOOKS.md, DYNAMIC-TOOL-CALLING.md, MCP-INTEGRATION.md, API-ACTIONS.md remain authoritative for their own internals.
Architecture Overview
The Three Tool Types
| Webhook | Client tool | Integration (MCP) | |
|---|---|---|---|
| Create at | /tools → Add webhook | /tools → Add client tool | /tools → Add integration |
| Wizard | WebhookWizard | ClientToolWizard | IntegrationToolForm |
| Collection | WebhookEndpoint | SharedDynamicTool | McpIntegration |
| Reaches an agent via | agentIds[] on the endpoint (empty = all agents) | linkedToolIds[] on the Agent | scope: global or agentId |
| When it runs | After the call, on lifecycle events | During the call, LLM-invoked | During the call, LLM-invoked |
| Who decides | Event subscription (deterministic) | The LLM | The LLM |
| Feature flag | webhooks | dynamic_tools | mcp_integrations |
| Logs tab | Webhook Logs | Client Tool Logs | Integration Logs |
The split that matters: webhooks are push, tools are pull. A webhook fires on a call lifecycle event and delivers call data outward. A client tool or integration is called by the model mid-conversation and its response re-enters the conversation. Use a webhook for guaranteed delivery; a tool call only happens if the model decides to make it.
Which one for webhook delivery
Use the Webhook tool. Then pick a payload mode:
- Standard fixed versioned payload, HMAC-SHA256 signed (
X-Oktivo-Signature). Use when you own the receiver and want to verify authenticity. - Custom your own method, headers, body template and auth. Use when posting into a third-party CRM that expects a specific shape.
Subscribe to call.completed for end-of-call delivery; it fires only once call status, transcription, analysis and recording are all present. call.ended is earlier but has no analysis or recording yet. Full detail in WEBHOOKS.md.
Do not use client tools for this they are in-call and model-driven. Do not use API Actions deprecated, see below.
Configuration Surfaces
/tools canonical
ToolsPage renders ToolCards (create) and ToolList (manage). Both are driven by registries so adding a tool type is one entry, not three branches:
config.ts→TOOL_TYPE_CONFIGS: card title, icon, feature flag, wizard component.tool-registry.ts→TOOL_ADAPTERS:list/get/remove/setActiveper type, pluslistAllTools()which loads all types in parallel and skips a failing type rather than blanking the list.
setActive is null for types without an activation toggle, so ToolList hides the switch instead of calling a mismatched endpoint. An earlier version branched on tool.type in three separate places, which is how "toggle a client tool" ended up hitting the webhook endpoint.
Editing deep-links as /tools/edit/:type?id=<toolId>. The id is in the URL as well as router state so ToolCreatorPage can re-fetch via TOOL_ADAPTERS[type].get(id) after a refresh, instead of silently falling back to a blank create form.
Agent Editor overlapping, still live
The Agent Editor's Tools sub-tab (DynamicToolsSection) and MCP sub-tab (McpTabContent) still work and overlap with /tools:
- Tools lets you link a library tool to the agent, or define an agent-scoped inline tool (
DynamicToolcollection, unique on(agentId, toolName)). - MCP duplicates the integration form.
/tools now covers create-and-attach for client tools via the wizard's Agents step (sharedToolService.setAssignments), so the Agent Editor tab is redundant except for inline tools, which exist nowhere else. It is deliberately kept until those are migrated hiding it would leave existing inline tools executing but unreachable in the UI.
At call time getAgentToolsInternal merges inline and linked tools; on a name collision inline wins.
Execution Logs /logs
LogsPage renders LOG_TAB_CONFIGS from config.ts, filtered by feature flag, in the same order as the tool cards so a tool and its logs sit in the same position.
| Tab | Collection | Flag | Written by |
|---|---|---|---|
| Webhook Logs | WebhookDeliveryLog | webhooks | BullMQ worker, every attempt |
| Client Tool Logs | DynamicToolLog | dynamic_tools | Agent Server → POST /internal/dynamic-tool-logs |
| Integration Logs | McpToolCallLog | mcp_integrations | Agent Server → POST /internal/mcp-tool-call-logs |
| API Action Logs | ApiActionLog | dynamic_tools | Legacy worker (historical only) |
All four collections carry a 30-day TTL on createdAt.
One table, four configurations
Each tab is a declarative config passed to a single LogsExplorer component. The tab files contain only columns, filters and the expanded-row body.
app/src/components/features/tools/logs/
├── LogsExplorer.tsx # filter bar + expandable table + pagination
├── useLogFeed.ts # filters, paging, 30s polling, debounced refresh
├── LogPrimitives.tsx # LogStatusBadge, LogPayload, LogDetailPanel, MonoCell
├── logFormat.ts # formatMaybeJson / formatLatency / formatTimestamp
├── types.ts # LogColumnDef, LogFilterDef, STATUS_FILTER, DATE_RANGE_FILTERS
├── WebhookLogsSection.tsx
├── ClientToolLogsSection.tsx
├── IntegrationLogsSection.tsx
└── ApiActionLogsSection.tsxBehaviour that is now identical everywhere because it lives in one place: 30-second background polling, a 2-second debounce on manual refresh, filter changes resetting to page 1, stale-response discarding via a request-id guard (a slow earlier page can't overwrite a newer one), and only the first load showing a skeleton so filtering doesn't flash an empty state.
logFormat.ts is split out from LogPrimitives.tsx because that file must export only components for React Fast Refresh to work on it.
Shared server-side query helpers
utils/logQuery.utils.ts is used by every log endpoint:
parseLogPagination(page, limit)clamps toLOG_MAX_LIMIT(100), default 20.applyCreatedAtRange(filter, from, to)inclusive range. A date-onlytosuch as2026-08-24parses to midnight UTC, so an$lteused to exclude the whole selected day; it is now widened to23:59:59.999. Invalid dates are ignored rather than producing anInvalid Datethat matches nothing.buildLogPaginationMeta(total, pagination)emits bothpagesandtotalPages, because existing clients read different keys.
Client tool log enrichment
DynamicToolLog stores only toolId, and that id may point at either DynamicTool (inline) or SharedDynamicTool (library). enrichToolLogs in dynamic-tool.controller.ts resolves display fields in two batched queries regardless of page size, adding:
toolNamefrom whichever collection owns the id (nullif hard-deleted)agentNamefromAgent.AgentNametoolSource"shared"or"agent", rendered as theSharedbadge
Without this the tab would show opaque dtl_… ids.
Why MCP logging is shaped differently
MCP tool calls travel straight from the Agent Server to the external MCP server over the MCP protocol, so the backend never observes the HTTP exchange. There is no responseStatus or header detail to record. Instead Agent/src/core/mcp_tool_logging.py listens for LiveKit's function_tools_executed event and reports the tool name, the arguments the LLM supplied, the result returned, and the latency.
Only MCP-owned tools are reported: toolset ids are set to the McpIntegration id in mcp_servers.create_mcp_toolsets, which is what lets the backend resolve the catalog entry for display. Client tools already log themselves with full HTTP detail, and built-in transfer/end-call tools are not integrations. Logging is fire-and-forget a logging failure must never affect the conversation.
API Endpoints
| Method | Path | Purpose |
|---|---|---|
GET | /webhooks/delivery-logs | Webhook deliveries. Filters: webhookId, eventType, status, from, to |
GET | /webhooks/delivery-logs/:deliveryId | Single delivery |
GET | /dynamic-tool-logs | Client tool calls. Filters: agentId, toolId, status, from, to |
GET | /dynamic-tool-logs/:logId | Single call |
GET | /api/v2/mcp/tool-call-logs | MCP tool calls. Filters: integrationId, agentId, toolName, status, from, to |
GET | /api/v2/mcp/tool-call-logs/:logId | Single call |
GET | /api-action-logs | Legacy. Filters: actionId, status, triggerType, from, to |
POST | /internal/dynamic-tool-logs | Agent ingest (x-api-secret) |
POST | /internal/mcp-tool-call-logs | Agent ingest (x-api-secret) |
All public routes require JWT plus the tab's feature flag, and are scoped to customerUID. Every list endpoint accepts page and limit and returns { success, logs, pagination }.
Tool CRUD endpoints live in the per-system docs: WEBHOOKS.md, DYNAMIC-TOOL-CALLING.md, MCP-INTEGRATION.md.
Deprecated: API Actions
Superseded by webhook Custom payload mode. No creation UI remains the only surviving reference is a read-only active toggle on CampaignDetailPage. The API Action Logs tab is kept so historical runs stay auditable, and the worker still runs for actions configured before the migration. Migrate with POST /migration/api-actions. See API-ACTIONS.md.
Removed Pages
These were lazily imported in routes.tsx but never mounted their paths already redirected to /tools so they were unreachable UI that could still drift out of sync. Deleted 2026-08:
| File | Replaced by |
|---|---|
pages/customer/WebhooksPage.tsx | /tools + /logs. Held its own duplicate webhook and API-action log tables. |
pages/customer/McpIntegrationsPage.tsx | IntegrationToolForm on /tools |
pages/customer/ToolLibraryPage.tsx | ClientToolWizard on /tools |
components/features/tools/WebhookToolForm.tsx | WebhookWizard |
components/features/tools/ConfigurationPanel.tsx | Nothing ToolsPage navigates to /tools/new/:type |
Redirects retained: /webhooks, /mcp-integrations, /tool-library → /tools.
How to Test (End-to-End)
Webhook delivery
/tools→ Add webhook. Enter an HTTPS URL, selectcall.completed, save. Copy the secret (shown once).- Use Send Test on the review step. In Custom mode this replays the real request including auth a bearer-protected endpoint answering 401 means the token is wrong, not that the test is unauthenticated.
- Run a real call.
/logs→ Webhook Logs. Expand the row to see request headers, request body, response status and response body per attempt.
Client tool
/tools→ Add client tool. Complete the wizard and attach at least one agent on the Agents step an unattached tool is invisible to every agent and reads as "the tool silently does nothing".- Run a call with that agent and prompt the behaviour that should trigger the tool.
/logs→ Client Tool Logs. Confirm the row shows the tool name (not a raw id) and aSharedbadge.
Integration
/tools→ Add integration, pick a catalog entry, supply credentials, verify.- Run a call and prompt a task the MCP server handles.
/logs→ Integration Logs. Expand to see the arguments the LLM supplied and the result returned.
Date filter regression check: set both From and To to today and confirm today's rows still appear. This was broken before applyCreatedAtRange widened a date-only to.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
A tool type is missing from /tools | Feature flag not assigned | Assign webhooks / dynamic_tools / mcp_integrations |
| A Logs tab is missing | Same flags gate the tabs | As above |
| Client tool never runs | Not attached to the agent handling the call | Open the tool → Agents step → select agents |
Client tool log shows a raw stl_… id instead of a name | Tool was hard-deleted, so enrichment can't resolve it | Expected; the log is retained deliberately |
| Two tools with the same name, wrong one runs | Inline and library tool share a toolName | Inline wins rename one |
| Filtering by To = today returns nothing | Backend predates applyCreatedAtRange | Ensure the endpoint uses logQuery.utils |
| Webhook deliveries stop entirely | Endpoint auto-disabled after 50 consecutive failures | Check disabledReason, fix the receiver, re-enable (resets the counter) |
| Custom-mode delivery rejected with 400 | Predates JSON-aware interpolation | See WEBHOOKS.md quotes/newlines in analysis used to corrupt the body |
| Only one of several endpoints receives an event | Predates per-endpoint dedupe | Dedupe is keyed on (eventId, webhookId); a shared key used to suppress the rest |
| Custom deliveries unfilterable by event type | Older rows logged as custom_delivery | Kept in the filter list; clears with the 30-day TTL |
| Logs never appear for any in-call tool | Agent can't reach the ingest route | Check BACKEND_URL and x-api-secret on the Agent Server |
Changelog
| Date | Change |
|---|---|
| 2026-08 | Consolidated configuration on /tools; added TOOL_ADAPTERS registry |
| 2026-08 | /tools client tool CRUD fixed was posting to /dynamic-tools with an empty agentId (400) and reading the list from /shared-tools |
| 2026-08 | Added MCP tool call logging end-to-end (model, ingest route, Agent listener) |
| 2026-08 | Added Client Tool Logs and Integration Logs tabs; every tool type now has one |
| 2026-08 | Rebuilt all four log tabs on a shared LogsExplorer; added logQuery.utils.ts and fixed the exclusive to date bound |
| 2026-08 | Added enrichToolLogs so client tool logs show tool and agent names |
| 2026-08 | Deleted five unreachable files (see Removed Pages) |