Adding Tools to an AI Agent
What are tools?
Tools give an AI Agent capabilities beyond conversation — calling APIs, executing code, or searching knowledge bases. Without tools, an agent can only chat.
Steps
- Create the agent first: create_ai_agent { projectId, name, description } (create_tool requires an agent with an auto-provisioned flow)
- create_tool { aiAgentId, toolType, name, config }
- The tool is automatically wired into the agent's flow
- Test with talk_to_agent
Rule of thumb: each tool in an agent flow should have a unique toolId. If you need more logic, parameters, validation, or HTTP calls for that tool, add them inside the same tool branch instead of creating another tool with the same toolId.
Parameter schema rules (config.parameters)
parameters is a JSON Schema string that Cognigy passes VERBATIM to the LLM provider. create_tool / update_tool validate it and reject violations, because a broken schema either fails every conversation turn with a provider 400 (strict models) or is silently dropped (the tool is called with no arguments). The contract:
- Top level:
{"type":"object","properties":{...},"required":[...]}—requiredis mandatory (use[]if nothing is required). - EVERY property needs
"type"AND"description". Allowed types:string,number,integer,boolean,object,array,null. arrayproperties need"items". Nested objects with"properties"also need their own"required"."additionalProperties": falseis injected automatically at every object level.- Strict-mode models (OpenAI Responses API, e.g. gpt-5.x, where strict is forced on): list EVERY property key in
required; make a parameter optional with a nullable type —{"type":["string","null"],"description":"..."}. Non-strict providers ignore this, so it is the safe default. - Nested object properties and
integerare valid at runtime but not renderable by the Cognigy UI's graphical parameter builder — the node's Parameters section then shows the raw JSON editor. That is cosmetic; prefer flat schemas when you don't need nesting.
Tool types (create_tool)
tool — General-purpose tool with custom logic
create_tool {
aiAgentId: "...",
toolType: "tool",
name: "Fetch Weather",
config: {
toolId: "fetch_weather",
description: "Fetches current weather for a city",
parameters: '{"type":"object","properties":{"city":{"type":"string","description":"City to fetch the weather for"}},"required":["city"]}',
toolResponseValue: "{{JSON.stringify(input.result)}}"
}
}
IMPORTANT: The node label in the flow uses toolId (snake_case), not the display name. Always provide toolId.
The Resolve Tool Action node is auto-created with an answer field that controls what is returned to the LLM. Default for general-purpose tools: {{JSON.stringify(input.result)}}. Set toolResponseValue to customize it.
knowledge — Search a Knowledge Store
create_tool {
aiAgentId: "...",
toolType: "knowledge",
name: "Search FAQ",
config: {
knowledgeStoreId: "507f1f77bcf86cd799439011",
toolId: "search_faq",
description: "Search the FAQ knowledge base"
}
}
send_email — Send emails
create_tool {
aiAgentId: "...",
toolType: "send_email",
name: "Send Email",
config: {
toolId: "send_email",
description: "Send an email to support",
recipient: "support@example.com"
}
}
mcp — Connect to an EXTERNAL MCP server (specialized — not for general use)
⚠️ ONLY use this when the user explicitly asks to integrate with an external MCP (Model Context Protocol) server and provides a server URL. For general tool requests (e.g., "unlock account", "check status"), use toolType "tool" instead.
create_tool {
aiAgentId: "...",
toolType: "mcp",
name: "External MCP",
config: {
mcpName: "my-mcp-server",
mcpServerUrl: "https://mcp.example.com",
timeout: 30
}
}
http — Call an external HTTP API
Creates a composite node structure with optional pre/post-processing Code nodes:
aiAgentJobTool (the tool node the LLM sees) └─ [Code node: pre-process] (optional) └─ HTTP Request node (makes the API call) └─ [Code node: post-process] (optional)
HTTP response shape — read this before writing postProcessCode
The HTTP Request node writes the response into the input object. Two things about this are not obvious from the schema and silently break tools when missed:
Default storage key. The response is stored at
input.httprequestby default. This is the same key used in the defaulttoolResponseValueof{{JSON.stringify(input.httprequest)}}.Wrapping. The value at
input.httprequestis NOT the raw response body. It is a wrapper object — typically:input.httprequest = { result: <parsed body>, // JSON for application/json responses, string otherwise statusCode: 200, length: <bytes> }So if the API returns
{ "meals": [...] }, yourpostProcessCodereadsinput.httprequest.result.meals, NOTinput.httprequest.mealsand NOTinput.httprequest.body.meals.
If you are unsure of the exact shape in your Cognigy version, instrument the post-process on the very first run (input.debug = { keys: Object.keys(input.httprequest), sample: input.httprequest };) instead of guessing — a wrong key returns undefined, the Resolve Tool Action serializes that, and the LLM honestly reports "no results found" even when the API call succeeded.
Code node rules
Both preProcessCode and postProcessCode run inside Cognigy Code nodes. Two rules apply:
- No top-level
return. Code nodes are not full functions; the engine rejectsreturnat the top level. Mutateinputdirectly —input.foo = bar;instead ofreturn { foo: bar };. If you need to short-circuit, set a flag and check it in a downstream node. inputis the only persistent surface. Anything you write toinput.*is visible to subsequent nodes (post-process, Resolve, downstream flow). Localconst/letvariables disappear at the end of the node.
Example — simple GET with post-processing
create_tool {
aiAgentId: "...",
toolType: "http",
name: "Get Weather",
config: {
toolId: "get_weather",
description: "Fetches current weather for a location",
parameters: '{"type":"object","properties":{"city":{"type":"string","description":"City to fetch the weather for"}},"required":["city"]}',
url: "https://api.weather.com/v1/current?q={{input.aiAgent.toolArgs.city}}",
method: "GET",
headers: { "X-Api-Key": "your-api-key" },
postProcessCode: "input.weather = input.httprequest.result.current; delete input.httprequest;"
}
}
Example — POST with pre/post-processing and custom tool response
create_tool {
aiAgentId: "...",
toolType: "http",
name: "Create Order",
config: {
toolId: "create_order",
description: "Creates a new order in the order management system",
parameters: '{"type":"object","properties":{"items":{"type":"array","description":"Ordered items","items":{"type":"string","description":"SKU of one item"}},"customerId":{"type":"string","description":"Customer identifier"}},"required":["items","customerId"]}',
url: "https://api.example.com/orders",
method: "POST",
headers: { "Authorization": "Bearer {{context.apiToken}}" },
body: "{{JSON.stringify(input.orderPayload)}}",
preProcessCode: "input.orderPayload = { items: input.aiAgent.toolArgs.items, customer: input.aiAgent.toolArgs.customerId, timestamp: new Date().toISOString() };",
postProcessCode: "input.orderResult = { orderId: input.httprequest.result.id, status: input.httprequest.result.status }; delete input.httprequest;",
toolResponseValue: "{{JSON.stringify(input.orderResult)}}"
}
}
Tool response value (toolResponseValue)
The toolResponseValue field controls what the Resolve Tool Action node returns to the LLM as the tool's result. This is a CognigyScript expression. It applies to BOTH http and general-purpose tool types.
Defaults (if omitted):
- http tools:
{{JSON.stringify(input.httprequest)}}— returns the full wrapper described in HTTP response shape above, not just the parsed body. For most tools you will want to override this with a trimmed payload from yourpostProcessCode. - general-purpose tools:
{{JSON.stringify(input.result)}}— returns theinput.resultfield
IMPORTANT: The Resolve Tool Action node MUST have an answer value, otherwise nothing is returned to the LLM and the tool appears to produce no output. If your code stores results in a custom field, set toolResponseValue to match.
Common patterns:
{{JSON.stringify(input.result)}}— return a processed result object (default for general-purpose tools){{JSON.stringify(input.orderResult)}}— return a named result from post-processing{{input.summary}}— return a plain string value{{JSON.stringify(input.httprequest.result)}}— return only the parsed body of an HTTP response, dropping thestatusCode/lengthwrapper
Accessing tool parameters in code
Inside AI Agent tool branches, the LLM's tool call arguments are at input.aiAgent.toolArgs, NOT input.data. For example, if the tool defines a city parameter, access it as:
- Code nodes:
input.aiAgent.toolArgs.city - CognigyScript fields (URLs, body templates):
{{input.aiAgent.toolArgs.city}}
Under an LLM Prompt (llmPromptV2) node the path is different: args land at input.llmPrompt.toolArgs, and input.aiAgent is null. Reading input.aiAgent.toolArgs there yields undefined — the tool then runs with no arguments and silently falls back (mock/default) with no error. For tools that may run under either node type, read defensively:
const args =
(input.llmPrompt && input.llmPrompt.toolArgs) ||
(input.aiAgent && input.aiAgent.toolArgs) ||
{};
Adding logic inside tools (manage_flow_nodes)
After creating a toolType: "tool", you can add flow nodes inside the tool's branch to build custom logic. This is the recommended way to add conversation logic — nodes should live inside tools, not as standalone nodes in the flow.
Workflow
- Create the tool —
create_toolreturns atoolNodeId - Add the first node using
parentNodeId= toolNodeId andmode=appendChild - Add subsequent nodes using
parentNodeId= previous node ID andmode=append
If you already created the tool for this action, stop there and reuse that same toolNodeId. Do not call create_tool again for the same toolId.
Note: Both appendChild and append work correctly when targeting a tool node. The handler automatically places new nodes in the correct execution chain (before the Resolve Tool Action node) regardless of which mode you use.
Example — tool with validation logic and response
Step 1: Create the tool
create_tool {
aiAgentId: "...",
toolType: "tool",
name: "Check Order Status",
config: {
toolId: "check_order_status",
description: "Looks up the status of a customer order",
parameters: '{"type":"object","properties":{"orderId":{"type":"string","description":"Order number to look up"}},"required":["orderId"]}'
}
}
→ returns toolNodeId: "abc123..."
Step 2: Add a Code node inside the tool branch
manage_flow_nodes {
operation: "create",
flowId: "<flowId>",
parentNodeId: "abc123...",
mode: "appendChild",
nodeType: "code",
label: "Validate Order ID",
config: { code: "if (!input.aiAgent.toolArgs.orderId) { input.error = 'Missing order ID'; }" }
}
→ returns nodeId: "def456..."
Step 3: Add an If/Else node after the Code node
manage_flow_nodes {
operation: "create",
flowId: "<flowId>",
parentNodeId: "def456...",
mode: "append",
nodeType: "ifThenElse",
label: "Has Error?",
config: { condition: "{{input.error}}" }
}
Supported node types for tool branches
All node types supported by manage_flow_nodes can be used inside tool branches: say, question, ifThenElse, lookup, setSessionContext, code, goTo, sleep, httpRequest.
See the flow-nodes skill for config schemas of each node type.
Updating tools (update_tool)
Modify an existing tool node's label or config:
update_tool {
aiAgentId: "...",
toolNodeId: "...",
name: "Updated Weather Tool",
config: { description: "Updated description for the LLM" }
}
For http tools, include HTTP fields in config to update child nodes:
update_tool {
aiAgentId: "...",
toolNodeId: "...",
toolType: "http",
config: {
url: "https://api.weather.com/v2/current",
postProcessCode: "input.result = input.httprequest.result;"
}
}
If the tool was originally created without preProcessCode / postProcessCode, passing them here will provision and wire the missing Code node automatically — no need to delete and recreate the tool. To target a specific existing child by ID (useful after a rename, or when the tool's flow contains other tools with the same label prefix), pass httpNodeId / preProcessNodeId / postProcessNodeId / resolveNodeId from the childNodes block in create_tool's response.
Managing tools
- List: list_resources { resourceType: "tool", aiAgentId }
- Remove: delete_resource { resourceType: "tool", id: toolId, aiAgentId }
- Update: update_tool { aiAgentId, toolNodeId, name?, config? }
- Tool IDs come from create_tool response or list_resources
- Tool IDs must be unique within an agent flow. If a tool already exists for an action, reuse it.
Tools under an LLM Prompt node (flowId addressing)
LLM Prompt (llmPromptV2) flows have no agent resource, so address their tools by flowId:
- Create: create_tool { flowId, toolType, name, config }
- List: list_resources { resourceType: "tool", flowId }
- Update: update_tool { flowId, toolNodeId, ... }
- Remove: delete_resource { resourceType: "tool", id: toolId, flowId }
Tools attach to the flow's aiAgentJob node when one exists, otherwise to its llmPromptV2 node. Under an LLM Prompt node, only tool, mcp, and http are supported. Branch logic works the same, but the tool arguments arrive at input.llmPrompt.toolArgs, not input.aiAgent.toolArgs (input.aiAgent is null in an LLM Prompt branch). Code and CognigyScript inside these tool branches must read input.llmPrompt.toolArgs.<param>; the input.aiAgent.toolArgs examples above apply only to AI Agent Job flows. See the flow-nodes guide's Notes for the defensive read that covers both.
Create LLM Prompt nodes only on explicit user request; see the flow-nodes guide.
Prerequisites
- Agent MUST be created via create_ai_agent (not manually) — tools need the auto-provisioned flow
- For knowledge tools, the knowledge store must exist (use manage_knowledge first)
Knowledge: tool vs persona-level
- Default (ALWAYS use unless told otherwise): Use create_tool with toolType "knowledge" to attach knowledge stores. This gives the agent a dedicated search tool it can invoke during conversations.
- Exception: Only attach knowledge to the agent persona (via create_ai_agent or update_ai_agent) if the user EXPLICITLY asks to put it on the persona. Do not default to persona-level attachment.