Skip to main content
Arcanflows

Custom Tools

Build custom tools to extend your AI agent's capabilities with your own logic.

Overview

Custom tools allow you to extend your AI agent with your own business logic, integrations, and functions. You can create tools using JavaScript/TypeScript or connect to external services via webhooks.

Tool Types

1. JavaScript Function

Execute custom JavaScript code:

json
{
  "name": "calculate_shipping",
  "type": "function",
  "description": "Calculate shipping cost based on weight and destination",
  "code": "async ({ weight, destination, expedited }) => { const baseRate = destination === 'international' ? 15 : 5; const weightRate = weight * 0.5; const expeditedFee = expedited ? 10 : 0; return { cost: baseRate + weightRate + expeditedFee, currency: 'USD', estimated_days: expedited ? 2 : 5 }; }",
  "parameters": {
    "type": "object",
    "properties": {
      "weight": {
        "type": "number",
        "description": "Package weight in pounds"
      },
      "destination": {
        "type": "string",
        "enum": ["domestic", "international"],
        "description": "Shipping destination type"
      },
      "expedited": {
        "type": "boolean",
        "default": false,
        "description": "Whether to use expedited shipping"
      }
    },
    "required": ["weight", "destination"]
  }
}

2. Webhook Tool

Call an external endpoint:

json
{
  "name": "verify_identity",
  "type": "webhook",
  "description": "Verify user identity through external service",
  "config": {
    "url": "https://your-service.com/api/verify",
    "method": "POST",
    "headers": {
      "Authorization": "Bearer {{credentials.verification_api_key}}"
    },
    "body_template": {
      "name": "{{parameters.full_name}}",
      "dob": "{{parameters.date_of_birth}}",
      "ssn_last_4": "{{parameters.ssn_last_4}}"
    }
  },
  "parameters": {
    "type": "object",
    "properties": {
      "full_name": { "type": "string" },
      "date_of_birth": { "type": "string", "format": "date" },
      "ssn_last_4": { "type": "string", "pattern": "^[0-9]{4}$" }
    },
    "required": ["full_name", "date_of_birth", "ssn_last_4"]
  }
}

3. Composite Tool

Combine multiple tools into one:

json
{
  "name": "process_order",
  "type": "composite",
  "description": "Process a complete order (validate, charge, fulfill)",
  "steps": [
    {
      "tool": "validate_inventory",
      "params": { "product_id": "{{parameters.product_id}}" }
    },
    {
      "tool": "charge_payment",
      "params": {
        "amount": "{{steps.0.price}}",
        "customer_id": "{{parameters.customer_id}}"
      },
      "condition": "{{steps.0.in_stock === true}}"
    },
    {
      "tool": "create_fulfillment",
      "params": {
        "order_id": "{{steps.1.order_id}}",
        "address": "{{parameters.shipping_address}}"
      }
    }
  ]
}

Creating Custom Tools

Via UI

  1. Go to Agents → Select agent → Tools
  2. Click Create Tool
  3. Choose tool type (Function, Webhook, etc.)
  4. Configure the tool
  5. Test and save

Via API

bash
curl -X POST "https://api.arcanflows.com/api/v1/agents/{agent_id}/tools" \
  -H "X-API-Key: your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "my_custom_tool",
    "type": "function",
    "description": "Description for the AI agent",
    "code": "async (params) => { return { result: params.input * 2 }; }",
    "parameters": {
      "type": "object",
      "properties": {
        "input": { "type": "number" }
      },
      "required": ["input"]
    }
  }'

JavaScript Function Tools

Basic Structure

javascript
async (parameters, context) => {
  // parameters: The input from the AI agent
  // context: Additional context (user, tenant, etc.)

  // Your logic here
  const result = doSomething(parameters.input);

  // Return the result
  return {
    success: true,
    data: result
  };
}

Available Context

javascript
async (params, context) => {
  // User information
  const userId = context.user.id;
  const userEmail = context.user.email;

  // Tenant information
  const tenantId = context.tenant.id;

  // Agent information
  const agentId = context.agent.id;

  // Credentials from vault
  const apiKey = context.credentials.my_api_key;

  // Current timestamp
  const now = context.timestamp;

  return { userId, tenantId };
}

Making HTTP Requests

javascript
async (params, context) => {
  const response = await fetch('https://api.example.com/data', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${context.credentials.api_key}`
    },
    body: JSON.stringify({
      query: params.query
    })
  });

  if (!response.ok) {
    throw new Error(`API error: ${response.status}`);
  }

  return await response.json();
}

Error Handling

javascript
async (params, context) => {
  try {
    // Validate input
    if (!params.email || !params.email.includes('@')) {
      return {
        success: false,
        error: 'Invalid email address'
      };
    }

    // Process
    const result = await processEmail(params.email);

    return {
      success: true,
      data: result
    };
  } catch (error) {
    // Log error (available in execution logs)
    console.error('Tool error:', error);

    return {
      success: false,
      error: error.message
    };
  }
}

Working with Dates

javascript
async (params) => {
  const startDate = new Date(params.start_date);
  const endDate = new Date(params.end_date);

  const daysDiff = Math.ceil(
    (endDate - startDate) / (1000 * 60 * 60 * 24)
  );

  return {
    start: startDate.toISOString(),
    end: endDate.toISOString(),
    duration_days: daysDiff
  };
}

Data Transformation

javascript
async (params) => {
  const items = params.items || [];

  // Transform data
  const transformed = items.map(item => ({
    id: item.id,
    fullName: `${item.firstName} ${item.lastName}`,
    totalValue: item.quantity * item.price
  }));

  // Aggregate
  const total = transformed.reduce(
    (sum, item) => sum + item.totalValue, 0
  );

  return {
    items: transformed,
    count: transformed.length,
    total: total
  };
}

Example Tools

Price Calculator

json
{
  "name": "calculate_price",
  "type": "function",
  "description": "Calculate total price with discounts and tax",
  "code": "async ({ items, discount_code, tax_rate }) => { let subtotal = items.reduce((sum, item) => sum + (item.price * item.quantity), 0); let discount = 0; if (discount_code === 'SAVE10') discount = subtotal * 0.10; if (discount_code === 'SAVE20') discount = subtotal * 0.20; const afterDiscount = subtotal - discount; const tax = afterDiscount * (tax_rate / 100); const total = afterDiscount + tax; return { subtotal: subtotal.toFixed(2), discount: discount.toFixed(2), tax: tax.toFixed(2), total: total.toFixed(2) }; }",
  "parameters": {
    "type": "object",
    "properties": {
      "items": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "price": { "type": "number" },
            "quantity": { "type": "integer" }
          }
        }
      },
      "discount_code": { "type": "string" },
      "tax_rate": { "type": "number", "default": 8.5 }
    },
    "required": ["items"]
  }
}

Appointment Scheduler

json
{
  "name": "find_available_slots",
  "type": "function",
  "description": "Find available appointment slots for a given date",
  "code": "async ({ date, duration_minutes }, context) => { const workStart = 9; const workEnd = 17; const slotDuration = duration_minutes || 30; const booked = await context.db.query('SELECT start_time, end_time FROM appointments WHERE date = $1', [date]); const slots = []; for (let hour = workStart; hour < workEnd; hour++) { for (let min = 0; min < 60; min += slotDuration) { const slotStart = `${hour.toString().padStart(2, '0')}:${min.toString().padStart(2, '0')}`; const isBooked = booked.some(apt => apt.start_time <= slotStart && apt.end_time > slotStart); if (!isBooked) slots.push(slotStart); } } return { date, available_slots: slots, slot_duration: slotDuration }; }"
}

Email Validator

json
{
  "name": "validate_email",
  "type": "function",
  "description": "Validate email format and check if domain exists",
  "code": "async ({ email }) => { const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/; if (!emailRegex.test(email)) { return { valid: false, reason: 'Invalid email format' }; } const domain = email.split('@')[1]; try { const response = await fetch(`https://dns.google/resolve?name=${domain}&type=MX`); const data = await response.json(); const hasMX = data.Answer && data.Answer.length > 0; return { valid: hasMX, domain, has_mx_record: hasMX, reason: hasMX ? 'Valid email' : 'Domain has no MX records' }; } catch { return { valid: true, domain, has_mx_record: 'unknown', reason: 'Could not verify MX records' }; } }"
}

Security Considerations

Sandboxed Execution

Custom functions run in a sandboxed environment:

  • Limited execution time (30 seconds default)
  • Memory limits enforced
  • No file system access
  • No process spawning
  • Network requests allowed (with restrictions)

Input Validation

Always validate inputs:

javascript
async (params) => {
  // Type checking
  if (typeof params.amount !== 'number') {
    throw new Error('Amount must be a number');
  }

  // Range validation
  if (params.amount < 0 || params.amount > 10000) {
    throw new Error('Amount must be between 0 and 10000');
  }

  // Continue with valid input
  return processAmount(params.amount);
}

Sensitive Data

Never return sensitive data directly:

javascript
async (params, context) => {
  const user = await getUser(params.user_id);

  // Good - return only necessary fields
  return {
    id: user.id,
    name: user.name,
    email: user.email
  };

  // Bad - exposes sensitive data
  // return user; // includes password_hash, ssn, etc.
}

Testing Custom Tools

Unit Testing

javascript
// test-tool.js
const toolCode = require('./my-tool');

describe('calculate_price', () => {
  it('calculates correctly without discount', async () => {
    const result = await toolCode({
      items: [{ price: 10, quantity: 2 }],
      tax_rate: 10
    });

    expect(result.subtotal).toBe('20.00');
    expect(result.tax).toBe('2.00');
    expect(result.total).toBe('22.00');
  });
});

Integration Testing

bash
curl -X POST "https://api.arcanflows.com/api/v1/agents/{agent_id}/tools/{tool_id}/test" \
  -H "X-API-Key: your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "parameters": {
      "items": [{"price": 10, "quantity": 2}],
      "discount_code": "SAVE10",
      "tax_rate": 8
    }
  }'

Debugging

Console Logs

Use console.log for debugging (appears in execution logs):

javascript
async (params) => {
  console.log('Input:', JSON.stringify(params));

  const result = processData(params);
  console.log('Result:', JSON.stringify(result));

  return result;
}

Error Inspection

View tool execution errors in:

  1. AgentLogs → Filter by tool name
  2. API: GET /agents/{id}/tools/{tool_id}/executions

Best Practices

  1. Keep functions focused - One tool, one purpose
  2. Handle errors gracefully - Return meaningful error messages
  3. Validate inputs - Check types and ranges
  4. Document thoroughly - Clear descriptions help the AI use tools correctly
  5. Test extensively - Cover edge cases
  6. Monitor performance - Track execution times
  7. Use credentials vault - Never hardcode secrets