Dynamic Tool Calling
End-to-end documentation for dynamic tool calling in Oktivo. This feature allows users to configure custom HTTP API endpoints that AI agents can invoke during live calls. Includes both per-agent inline tools and the organization-scoped shared tool library.
⚠️ UI relocated (2026-08). The standalone
/tool-librarypage has been deleted. Shared tools are now created and managed at/tools→ Add client tool (ClientToolWizard), which attaches them to agents in the same flow. Their in-call executions appear at/logs→ Client Tool Logs./tool-libraryredirects to/tools.The backend both collections, the linking model and the Agent Server resolution described below is unchanged. See TOOLS-AND-LOGS.md for how this system sits alongside webhooks and MCP integrations.
Architecture Overview
Two Tool Systems
Oktivo supports two complementary tool systems:
| Aspect | Inline Tools (per-agent) | Shared Tools (library) |
|---|---|---|
| Scope | Single agent | Organization-wide |
| Collection | DynamicTool | SharedDynamicTool |
| Unique key | (agentId, toolName) | (orgUID, toolName) |
| Created from | Agent Editor → Tools sub-tab | /tools → Add client tool |
| Linked to agent | Automatic (belongs to agent) | Via linkedToolIds[] on Agent model |
| Updates propagate | N/A (single agent) | Automatically agents resolve at call time |
| Deletion | Soft-delete per agent | Soft-delete removes from all agents |
Merge rule at call time: When resolving tools for an agent, inline tools take precedence over shared tools on toolName collision.
Feature Access Control
Dynamic tools use a dedicated dynamic_tools feature flag:
- Superadmin adds
dynamic_toolsto Organization'splan.features[] - Admin assigns
dynamic_toolsto Customer'sfeatures[] - Frontend checks
hasFeature('dynamic_tools')before rendering UI - Backend routes use
requireFeatureAccess(...FEATURE_MAP.manageDynamicTools)middleware
Data Models
DynamicTool Collection (Per-Agent)
Stores inline HTTP endpoints scoped to a single agent.
| Field | Type | Description |
|---|---|---|
toolId | String | Unique ID, prefix dtl_ |
agentId | String | FK to Agent |
customerUID | String | Tenant isolation |
orgUID | String | Organization scope |
baseUrl | String | e.g. https://api.example.com |
path | String | e.g. /v1/leads |
fullUrl | String | Computed: baseUrl + path |
method | Enum | GET, POST, PUT, PATCH, DELETE |
headers | Array | [{ key, value, enabled }] |
bodyType | Enum | json, form, raw, none |
bodyTemplate | String | Body with {{variable}} placeholders |
auth | Object | { type, token?, headerName?, username?, password? } encrypted at rest |
toolName | String | LLM identifier (snake_case, unique per agent) |
toolDescription | String | Instructions for LLM on when to use |
parameterSchema | Object | JSON Schema for LLM parameter collection |
parentToolId | String/null | Grouping by baseUrl |
timeoutMs | Number | 1000–30000, default 10000 |
retryOnFailure | Boolean | Auto-retry on failure |
maxRetries | Number | 0–5 |
isActive | Boolean | Soft-active flag |
deletedAt | Date/null | Soft-delete timestamp |
Indexes: { agentId: 1 }, { agentId: 1, toolName: 1 } (unique), { agentId: 1, deletedAt: 1 }, { parentToolId: 1 }
SharedDynamicTool Collection (shared tool library)
Organization-scoped reusable tool definitions. Same fields as DynamicTool but scoped differently.
| Field | Type | Description |
|---|---|---|
toolId | String | Unique ID, prefix stl_ |
orgUID | String | Organization scope (indexed) |
userId | String | Creator user ID |
baseUrl | String | e.g. https://api.example.com |
path | String | e.g. /v1/leads |
fullUrl | String | Computed: baseUrl + path |
method | Enum | GET, POST, PUT, PATCH, DELETE |
headers | Array | [{ key, value, enabled }] |
bodyType | Enum | json, form, raw, none |
bodyTemplate | String | Body with {{variable}} placeholders |
auth | Object | Encrypted at rest (same as DynamicTool) |
toolName | String | Unique per org (snake_case) |
toolDescription | String | LLM instructions |
parameterSchema | Object | JSON Schema |
parentToolId | String/null | Grouping |
timeoutMs | Number | 1000–30000, default 10000 |
retryOnFailure | Boolean | Auto-retry |
maxRetries | Number | 0–5 |
isActive | Boolean | Active flag |
deletedAt | Date/null | Soft-delete |
Indexes: { orgUID: 1 }, { orgUID: 1, toolName: 1 } (unique), { orgUID: 1, deletedAt: 1 }, { parentToolId: 1 }
Agent Model Extension
// Added to Agent schema
linkedToolIds: { type: [String], default: [] }Stores toolId references to SharedDynamicTool documents. Resolved at call time.
DynamicToolLog Collection
Execution logs for tool invocations during calls. 30-day TTL. (Unchanged from original system.)
API Endpoints
Inline Tool Routes (JWT + dynamic_tools feature gate)
| Method | Path | Handler | Description |
|---|---|---|---|
| POST | /dynamic-tools | createTool | Create inline tool for agent |
| GET | /dynamic-tools?agentId=X | listTools | List tools grouped by baseUrl |
| GET | /dynamic-tools/:toolId | getTool | Get single tool |
| PUT | /dynamic-tools/:toolId | updateTool | Partial update |
| DELETE | /dynamic-tools/:toolId | deleteTool | Soft-delete (cascade if parent) |
| POST | /dynamic-tools/import/curl | importCurl | Parse cURL → tool config |
| POST | /dynamic-tools/verify | verifyEndpoint | Server-side endpoint test |
Shared Tool Routes (JWT + dynamic_tools feature gate)
| Method | Path | Handler | Description |
|---|---|---|---|
| POST | /shared-tools | createSharedTool | Create org-scoped shared tool |
| GET | /shared-tools | listSharedTools | List active shared tools for org |
| GET | /shared-tools/:toolId | getSharedTool | Get single shared tool |
| PUT | /shared-tools/:toolId | updateSharedTool | Update shared tool |
| DELETE | /shared-tools/:toolId | deleteSharedTool | Soft-delete shared tool |
Agent-Tool Linking Routes (JWT + dynamic_tools feature gate)
| Method | Path | Handler | Description |
|---|---|---|---|
| GET | /agents/:agentId/linked-tools | getLinkedTools | Get full SharedDynamicTool docs for linked tools |
| POST | /agents/:agentId/linked-tools | linkTool | Link shared tool to agent. Body: { toolId } |
| DELETE | /agents/:agentId/linked-tools/:toolId | unlinkTool | Unlink shared tool from agent |
Internal Routes (x-api-secret auth)
| Method | Path | Handler | Description |
|---|---|---|---|
| GET | /internal/agent-tools/:agentId | getAgentToolsInternal | Returns merged inline + shared tools with decrypted auth |
| POST | /internal/dynamic-tool-logs | createToolLogInternal | Create execution log |
Migration Routes (x-api-secret auth)
| Method | Path | Handler | Description |
|---|---|---|---|
| POST | /internal/scripts/migrate-shared-tools | migrateSharedTools | Migrate per-agent tools to shared library |
| GET | /internal/scripts/migration/shared-tools/status | getMigrationStatus | Get migration state and counts |
Tool Resolution (Merge Logic)
When the Agent Server needs tools for an agent, the internal endpoint merges both systems:
Key rule: Inline tools always win on toolName collision. This allows agents to override a shared tool with a custom version.
File Structure
Client Backend
client_backend/src/
├── types/
│ ├── dynamic-tool.types.ts # IDynamicTool, enums (shared)
│ └── shared-tool.types.ts # ISharedDynamicTool interface
├── models/
│ ├── DynamicTool.ts # Per-agent tool schema
│ ├── SharedDynamicTool.ts # Org-scoped shared tool schema
│ ├── DynamicToolLog.ts # Execution logs (TTL)
│ └── Agent.ts # + linkedToolIds field
├── services/
│ └── dynamic-tool.service.ts # Business logic + getAgentToolsInternal (merge)
├── controllers/
│ ├── dynamic-tool.controller.ts # Inline tool CRUD + verify + logs
│ ├── shared-tool.controller.ts # Shared tool CRUD
│ ├── agent.controller.ts # + getLinkedTools, linkTool, unlinkTool
│ └── migration.controller.ts # Migration script handlers
├── routes/
│ ├── dynamic-tool.routes.ts # Inline tool routes
│ ├── dynamic-tool-internal.routes.ts # Internal routes
│ ├── shared-tool.routes.ts # Shared tool routes
│ ├── agent.routes.ts # + linked-tools routes
│ └── script.routes.ts # + migration routes
└── app.ts # All routes mountedFrontend
app/src/
├── services/
│ ├── dynamicTool.service.ts # Inline tool API client + execution logs
│ └── sharedTool.service.ts # Shared tool CRUD + agent assignments
├── components/features/tools/
│ ├── ClientToolWizard.tsx # /tools multi-step create/edit (registered in TOOL_TYPE_CONFIGS)
│ ├── steps/ClientAgentsStep.tsx # Attach to agents as part of the same save
│ ├── tool-registry.ts # list / get / remove / setActive adapter
│ ├── curl-parser.ts # Shell-aware cURL import
│ └── logs/ClientToolLogsSection.tsx # /logs → Client Tool Logs tab
├── components/features/
│ ├── DynamicToolForm.tsx # Agent Editor form (manual + cURL, verify, variable badges)
│ ├── DynamicToolsSection.tsx # Agent Editor: inline tools + link picker
│ └── DynamicToolGroupList.tsx # Grouped display
├── hooks/useToolAssignableAgents.ts # Agent list + current attachment state
├── pages/customer/
│ ├── ToolsPage.tsx # /tools cards + unified list
│ ├── ToolCreatorPage.tsx # /tools/new/:type, /tools/edit/:type?id=
│ └── AgentEditorPage.tsx # Step 3: Tools & Transfers
└── routes.tsx # /tools routes; /tool-library redirectsUI Integration
Tools Page (/tools)
Shared tools are one of three tool types on the unified Tools page. ToolList renders all types through TOOL_ADAPTERS, so search, delete and the active toggle are the same code for every type.
The ClientToolWizard flow: Basic Info → Endpoint → Request Body → Auth & Settings → Agents → Verify & Create. The Agents step is a required stop rather than an optional extra a tool attached to no agent is invisible to every agent, which reads as "the tool silently does nothing". Selecting none is still allowed, but only after the warning has been shown.
On save the wizard writes the tool, then replaces the whole agent set in one PUT /shared-tools/:toolId/assignments call so a change cannot half-apply. If the tool saves but assignment fails, that is reported separately a partial success is never presented as a clean one.
Editing deep-links as /tools/edit/client?id=<toolId> so the wizard can re-fetch after a refresh instead of falling back to a blank create form.
Agent Editor (Step 3: Tools & Transfers)
Dynamic Tools and IVR moved to their own dedicated step:
AgentEditorPage.tsx
├── Step 1: Agent Setup
├── Step 2: Voice & Settings
├── Step 3: Tools & Transfers ← Dynamic Tools + IVR here
│ ├── DynamicToolsSection
│ │ ├── Inline link picker (no modal inline UI)
│ │ ├── Linked shared tools display
│ │ ├── Inline tools display
│ │ └── DynamicToolForm (add/edit)
│ └── IVR & Call Transfer Configuration
└── Step 4: AnalysisDynamicToolForm Enhancements
- Agent selector (optional): In the Agent Editor form, select an agent to auto-link after creation. On
/toolsthis is the dedicated Agents step instead. - Agent variable badges: When agent selected, shows clickable
{{variable}}badges below body template for quick insertion - Verify required: Tool cannot be saved until "Test Endpoint" passes successfully
- Improved validation: Body template
{{...}}validation now skips JSON content (no false positives on{{ "key": "value" }})
DynamicToolsSection (Agent Editor)
- Inline link picker: "Link from Library" button toggles an inline panel (no modal) with checkboxes
- Linked tools display: Shows shared tools with method badge, name, URL, "Shared" badge, and Unlink button
- Backward compatible: Inline tool creation still works as before
End-to-End Flow
Creating a Shared Tool and Linking to Agent
Tool Resolution at Call Time
Migration Script
Migrates existing per-agent DynamicTool documents to SharedDynamicTool:
- Fetches all active DynamicTool documents
- Groups by
(orgUID, toolName, baseUrl)for deduplication - Creates one SharedDynamicTool per unique group
- Links the shared tool to all agents that had it (
$addToSet linkedToolIds) - Preserves original DynamicTool documents (no deletion)
- Skips and logs errors per tool, continues processing
Trigger: POST /internal/scripts/migrate-shared-tools (x-api-secret auth) Status: GET /internal/scripts/migration/shared-tools/status
How to Test
Prerequisites
- Backend running (
npm run devinclient_backend/) - Frontend running (
npm run devinapp/) - User with
dynamic_toolsfeature enabled - At least one agent created
Test Shared Tool CRUD
- Navigate to Tools in the sidebar (
/tools) - Click the Add client tool card
- Fill: name=
test_webhook, description=Test tool, baseUrl=https://webhook.site/your-id, path=/, method=POST - On Verify & Create, click Test Endpoint → verify green success response
- Click Save → tool appears in the unified tool list with a
Clientbadge - Click the row to reopen the wizard → modify description → Save
- Click the trash icon → confirm → tool removed and detached from every agent
Test Agent Attachment
- Open a client tool from
/toolsand go to the Agents step - Select one or more agents → complete the wizard → Save
- Reopen the tool → verify the same agents are pre-selected
- Deselect all → Save → verify the warning is shown and the tool detaches
- Go to Agent Editor → Step 3 (Tools & Transfers)
- Click Link from Library → verify the inline picker shows available tools
- Select a tool → click Link → verify it appears under "Linked from Library"
- Click Unlink → verify the tool is removed from the linked section
Both surfaces write the same Agent.linkedToolIds, so a change made in one is visible in the other after a reload.
Test Tool Resolution
- Create an agent with 1 inline tool (
inline_tool) and link 1 shared tool (shared_tool) - Make a test call to the agent
- Verify in Agent Server logs: both tools registered
- Trigger both tools during conversation
- Verify execution logs in
/dynamic-tool-logs
Test Name Collision
- Create inline tool with name
send_dataon an agent - Create shared tool with same name
send_dataand link to same agent - Make a test call → verify only the inline version is used (check URL in logs)
Test Verify Requirement
- On
/tools, start a new client tool - Try to save WITHOUT clicking "Test Endpoint" → verify error toast appears
- Click "Test Endpoint" → wait for success → save succeeds
Test Execution Logging
- Attach a client tool to an agent and place a call that triggers it
- Go to
/logs→ Client Tool Logs - Verify the row shows the resolved tool name (not a raw
stl_…id), the agent name, and aSharedbadge for library tools - Expand the row → verify URL, method, request headers/body, response status and body are present
- Set both From and To to today → verify today's rows still appear
Error Handling
| Scenario | HTTP Status | Response |
|---|---|---|
| Invalid tool payload (missing required fields) | 400 | { success: false, message: "toolName is required" } |
| Duplicate toolName within org | 409 | { success: false, message: "A tool with this name already exists" } |
| Shared tool not found or wrong org | 404 | { success: false, message: "Tool not found" } |
| Link to non-existent/deleted tool | 404 | { success: false, message: "Shared tool not found or has been deleted" } |
| Link to already-linked tool | 409 | { success: false, message: "Tool is already linked to this agent" } |
| Agent not found for linking | 404 | { success: false, message: "Agent not found" } |
| Save without verify | UI block | Toast: "Please verify the endpoint before saving" |
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
"Add client tool" card missing on /tools | dynamic_tools feature not enabled | Superadmin: add to org. Admin: assign to customer |
| Shared tool not appearing in agent's linked tools | Tool soft-deleted or isActive=false | Check deletedAt and isActive in DB |
| Linked tool not used during call | Name collision with inline tool | Inline takes precedence rename one |
| "Link from Library" shows no tools | No shared tools created yet | Create one at /tools → Add client tool |
| Tool saved but never runs | Attached to no agent | Reopen the tool → Agents step → select agents |
| No rows in Client Tool Logs | Agent Server can't reach the ingest route | Check BACKEND_URL and x-api-secret on the Agent Server |
Body template {{...}} false error | JSON content detected as placeholder | Fixed: validator now skips JSON-like content |
| Migration skipping tools | Duplicate (orgUID, toolName) already exists | Expected tool already migrated |
| Agent Editor missing "Tools & Transfers" step | Feature flags not enabled | Enable dynamic_tools or ivr_call_transfer |
Changelog
| Date | Change |
|---|---|
| 2025-05 | Initial per-agent Dynamic Tool system |
| 2025-06 | Added shared tool library (org-scoped), agent linking, Tool Library page, migration script |
| 2025-06 | Moved Dynamic Tools + IVR to dedicated "Tools & Transfers" step in Agent Editor |
| 2025-06 | Added verify-before-save requirement, agent variable badges in body template |
| 2025-06 | Fixed body template validation (no false positives on JSON curly braces) |
| 2025-06 | Replaced "Link from Library" modal with inline picker (z-index fix) |
| 2026-08 | Shared tool UI moved to /tools (ClientToolWizard); ToolLibraryPage deleted and /tool-library now redirects |
| 2026-08 | Added an Agents step to the wizard plus GET/PUT /shared-tools/assignments so attachment is part of the same save |
| 2026-08 | Fixed /tools client tool CRUD was posting to /dynamic-tools with an empty agentId (400) and reading the list from /shared-tools |
| 2026-08 | Added the Client Tool Logs tab at /logs, plus enrichToolLogs so rows show tool and agent names instead of raw ids |
| 2026-08 | Rewrote cURL import as a shell-aware tokenizer (curl-parser.ts) quoted bodies, -u, --data-urlencode, -XPOST |