Custom Tools
Mandala ships with roughly 230 built-in integration tools, but there's always a sliver of logic that's specific to your company: a proprietary calculation, a lookup against an internal API, a formatting rule nobody outside your team would recognize. A custom tool is a small, reusable capability you define yourself with a name, a description, a JSON schema, and a piece of JavaScript code, so an Agent can call it exactly like it would call any built-in tool.
A custom tool is executable code the agent calls with arguments. This is different from a Skill, which is reusable natural-language instructions the agent reads as guidance rather than executes.
Creating a Custom Tool
Custom tools can be created from Workspace Settings → Custom Tools, where they're saved once and reusable across every workflow in the workspace. Each tool has four parts:
- Name — comes from the
namefield inside the JSON schema'sfunctionobject. It must be unique within the workspace, but you can edit it (along with the description, schema, and code) at any time after the tool is created. - Description — the
function.descriptionfield. This is what the agent's LLM reads to decide when it should call the tool, so it should describe the tool's purpose clearly rather than how it works internally. - JSON Schema — an OpenAI function-calling style schema: a top-level
type: "function", afunction.name, afunction.description, andfunction.parameters(a JSON Schema object withtype: "object", apropertiesmap, and arequiredarray). This is what tells the LLM which arguments it can pass. - JavaScript Code — the function body that runs when the tool is called. It can reference any parameter from the schema directly by name, and reference workspace environment variables with
{{VARIABLE_NAME}}syntax.
Both the schema and the code have an AI-assisted "generate" option (the wand icon) if you'd rather describe what you want in plain language than write it by hand.
Attaching to an Agent
Custom tools become available to an agent through the Tools section of an Agent block, alongside built-in integrations and MCP tools. From there you can:
- Pick an existing custom tool from the workspace's saved list, or
- Create a brand new one inline, using the same schema/code modal, scoped to that block
When attaching a tool you can also pre-fill some of its parameters with fixed values or workflow variables. Any parameter you pre-fill this way is merged with whatever the LLM supplies at call time, so the LLM only needs to decide the pieces you didn't already pin down.
How It Executes
When the agent's LLM decides to call a custom tool, it does so mid-conversation, supplying arguments that match the parameters declared in the JSON schema. Mandala then:
- Merges those LLM-supplied arguments with any parameters you pre-filled when attaching the tool.
- Runs your JavaScript as an async function body, with the merged parameters available directly by name and
{{ENV_VAR}}references resolved from workspace environment variables. - Returns whatever the code returns back to the agent as the tool's result, which the LLM then reasons over to continue the conversation.
The code executes with a configurable timeout, and has access to fetch() for calling external APIs — this is typically how a custom tool reaches into a company-internal system that has no dedicated Mandala integration.
Example
Say your company has an internal API for looking up a customer's loyalty tier. The schema:
{
"type": "function",
"function": {
"name": "getLoyaltyTier",
"description": "Look up a customer's current loyalty tier and points balance by their customer ID.",
"parameters": {
"type": "object",
"properties": {
"customerId": {
"type": "string",
"description": "The internal customer ID, e.g. CUST-10432"
}
},
"required": ["customerId"]
}
}
}And the code:
const apiKey = {{LOYALTY_API_KEY}};
const response = await fetch(`https://internal.example.com/api/loyalty/${customerId}`, {
headers: { Authorization: `Bearer ${apiKey}` }
});
if (!response.ok) {
throw new Error(`Loyalty lookup failed with status ${response.status}`);
}
return await response.json();Once attached to an Agent block, the agent can call getLoyaltyTier whenever a customer's tier is relevant to the conversation, passing the customerId it has gathered from earlier context.