Custom Functions and Secrets Management Guide

This guide covers how to create secure custom functions for Ragwalla agents and manage secrets safely.

Table of Contents

Overview

Custom functions allow you to extend agent capabilities with your own business logic. These functions:

  • Execute in a secure sandbox - Isolated from the main system
  • Support async operations - Make API calls, database queries, etc.
  • Access secrets securely - Without exposing them in code
  • Run at edge locations - Low latency worldwide
  • Scale automatically - Handle any load without configuration

How Functions Work

┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│    Agent    │────▶│  Function    │────▶│   Sandbox   │
│             │◀────│  Execution   │◀────│  (Isolated) │
└─────────────┘     └──────────────┘     └─────────────┘
                           │
                    ┌──────▼───────┐
                    │   Secrets     │
                    │   Manager     │
                    └──────────────┘

Quick Start

1. Create a Simple Function

const calculateShipping = {
  type: 'function',
  function: {
    name: 'calculate_shipping',
    description: 'Calculate shipping cost based on weight and destination',
    parameters: {
      type: 'object',
      properties: {
        weight: { 
          type: 'number', 
          description: 'Package weight in pounds' 
        },
        destination: { 
          type: 'string', 
          description: 'Destination country code' 
        }
      },
      required: ['weight', 'destination']
    },
    implementation: `
      export default function(args) {
        const { weight, destination } = args;

        // Base rates by destination
        const rates = {
          'US': 5.00,
          'CA': 8.00,
          'EU': 12.00,
          'default': 15.00
        };

        const baseRate = rates[destination] || rates.default;
        const weightCost = weight * 1.50;

        return {
          cost: baseRate + weightCost,
          currency: 'USD',
          estimatedDays: destination === 'US' ? 3 : 7
        };
      }
    `
  }
};

// Add to agent
await addToolToAgent(agentId, calculateShipping);

2. Function with External API Call

const fetchWeather = {
  type: 'function',
  function: {
    name: 'get_weather',
    description: 'Get current weather for a location',
    parameters: {
      type: 'object',
      properties: {
        city: { type: 'string' },
        units: { 
          type: 'string', 
          enum: ['celsius', 'fahrenheit'],
          default: 'celsius'
        }
      },
      required: ['city']
    },
    implementation: `
      export default async function(args, context) {
        const { city, units = 'celsius' } = args;

        // Access secret API key from context
        const apiKey = context.secrets.WEATHER_API_KEY;

        const response = await fetch(
          \`https://api.weatherapi.com/v1/current.json?key=\${apiKey}&q=\${city}&units=\${units}\`
        );

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

        const data = await response.json();

        return {
          temperature: data.current.temp,
          conditions: data.current.condition.text,
          humidity: data.current.humidity,
          wind: data.current.wind_kph
        };
      }
    `
  }
};

3. Add the Function to Your Agent

// First, store the secret
await fetch('https://example.ai.ragwalla.com/v1/secrets', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'WEATHER_API_KEY',
    value: 'your-weather-api-key-here',
    description: 'API key for weather service'
  })
});

// Then add the function to your agent
await fetch(`https://example.ai.ragwalla.com/v1/agents/${agentId}/tools`, {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(fetchWeather)
});

Creating Custom Functions

Function Structure

Every function must export a default function:

// Synchronous function
export default function(args) {
  // Your logic here
  return result;
}

// Asynchronous function
export default async function(args, context) {
  // Your async logic here
  return result;
}

Function Parameters

Functions receive two arguments:

  1. args - The input parameters from the LLM
  2. context - Runtime context including secrets
export default async function(args, context) {
  // Access function arguments
  const { param1, param2 } = args;

  // Access secrets
  const apiKey = context.secrets.MY_API_KEY;

  // Access metadata
  const { agentId, threadId } = context.metadata;

  return {
    success: true,
    data: 'Result'
  };
}

Return Values

Functions should return JSON-serializable data:

// Good returns
return { status: 'success', data: [1, 2, 3] };
return 'Simple string result';
return 42;
return true;

// Bad returns (will cause errors)
return undefined;
return new Date(); // Use date.toISOString() instead
return someClassInstance; // Convert to plain object

The Secure Sandbox

Sandbox Environment

Functions run in an isolated environment with:

  • No file system access - Cannot read/write local files
  • No process access - Cannot spawn processes or access system
  • Limited globals - Only safe JavaScript APIs available
  • Network restrictions - Outbound only, no server capabilities
  • Memory limits - Automatic cleanup after execution
  • Time limits - Functions timeout after 30 seconds

Available APIs

Functions have access to:

// ✅ Available
- fetch() - Make HTTP requests
- console.log() - Logging (visible in debug mode)
- JSON, Math, Date, Array, Object - Standard JS APIs
- Promise, async/await - Async operations
- crypto.randomUUID() - Generate UUIDs
- TextEncoder/TextDecoder - Text encoding
- URL, URLSearchParams - URL manipulation
- setTimeout (limited to 30s total execution)

// ❌ Not Available
- fs, path - No file system
- process, child_process - No system access
- require(), import() - No dynamic imports
- eval(), Function() - No dynamic code execution
- __dirname, __filename - No file paths
- Buffer - Use TextEncoder/Decoder instead

Network Access

Functions can make outbound HTTP requests:

export default async function(args, context) {
  // ✅ Allowed: Outbound requests
  const response = await fetch('https://api.example.com/data');

  // ✅ Allowed: Custom headers
  const authResponse = await fetch('https://api.example.com/auth', {
    headers: {
      'Authorization': `Bearer ${context.secrets.API_TOKEN}`,
      'Content-Type': 'application/json'
    }
  });

  // ❌ Not Allowed: Starting servers
  // const server = http.createServer() - Will fail

  // ❌ Not Allowed: Direct database connections
  // const pg = new Client() - Will fail

  return await response.json();
}

Managing Secrets

Creating Secrets

Store sensitive values securely:

// Create a secret
await fetch('https://example.ai.ragwalla.com/v1/secrets', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'STRIPE_API_KEY',
    value: 'sk_live_abc123...',
    description: 'Stripe API key for payment processing',
    metadata: {
      environment: 'production',
      service: 'stripe'
    }
  })
});

Using Secrets in Functions

Access secrets through the context parameter:

export default async function(args, context) {
  // Get secret value
  const stripeKey = context.secrets.STRIPE_API_KEY;

  if (!stripeKey) {
    throw new Error('STRIPE_API_KEY secret not configured');
  }

  // Use the secret
  const stripe = new Stripe(stripeKey);
  const charge = await stripe.charges.create({
    amount: args.amount,
    currency: 'usd',
    source: args.token
  });

  return {
    chargeId: charge.id,
    status: charge.status
  };
}

Secret Best Practices

  1. Never hardcode secrets
// ❌ Bad
const apiKey = 'sk_live_abc123...';

// ✅ Good
const apiKey = context.secrets.API_KEY;
  1. Validate secret existence
export default async function(args, context) {
  const requiredSecrets = ['API_KEY', 'API_SECRET'];

  for (const secret of requiredSecrets) {
    if (!context.secrets[secret]) {
      throw new Error(`Missing required secret: ${secret}`);
    }
  }

  // Continue with function logic
}
  1. Use descriptive names
// ❌ Bad
context.secrets.KEY
context.secrets.TOKEN

// ✅ Good
context.secrets.STRIPE_API_KEY
context.secrets.GITHUB_OAUTH_TOKEN

Managing Multiple Environments

Use secret metadata for environment management:

// Development secret
await createSecret({
  name: 'DATABASE_URL',
  value: 'postgresql://localhost:5432/dev',
  metadata: { environment: 'development' }
});

// Production secret
await createSecret({
  name: 'DATABASE_URL',
  value: 'postgresql://prod-server/db',
  metadata: { environment: 'production' }
});

// In your function
export default async function(args, context) {
  const env = context.metadata.environment || 'development';
  const dbUrl = context.secrets[`DATABASE_URL_${env.toUpperCase()}`];

  // Use appropriate database URL
}

Function Best Practices

1. Input Validation

Always validate inputs:

export default function(args) {
  // Validate required fields
  if (!args.email || !args.amount) {
    throw new Error('Missing required fields: email, amount');
  }

  // Validate types
  if (typeof args.amount !== 'number' || args.amount <= 0) {
    throw new Error('Amount must be a positive number');
  }

  // Validate format
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!emailRegex.test(args.email)) {
    throw new Error('Invalid email format');
  }

  // Process validated input
  return processPayment(args.email, args.amount);
}

2. Error Handling

Provide clear error messages:

export default async function(args, context) {
  try {
    const response = await fetch('https://api.example.com/data');

    if (!response.ok) {
      // Include helpful context
      throw new Error(
        `API request failed: ${response.status} ${response.statusText}`
      );
    }

    return await response.json();

  } catch (error) {
    // Distinguish between different error types
    if (error.name === 'TypeError') {
      throw new Error('Network error: Unable to reach API');
    }

    // Re-throw with context
    throw new Error(`Function failed: ${error.message}`);
  }
}

3. Performance Optimization

// Use caching for expensive operations
const cache = new Map();

export default async function(args) {
  const cacheKey = JSON.stringify(args);

  // Check cache first
  if (cache.has(cacheKey)) {
    return cache.get(cacheKey);
  }

  // Perform expensive operation
  const result = await expensiveOperation(args);

  // Cache for 5 minutes
  cache.set(cacheKey, result);
  setTimeout(() => cache.delete(cacheKey), 5 * 60 * 1000);

  return result;
}

4. Structured Responses

Return consistent, well-structured data:

export default async function(args) {
  try {
    const data = await fetchData(args.id);

    return {
      success: true,
      data: data,
      metadata: {
        timestamp: new Date().toISOString(),
        version: '1.0'
      }
    };

  } catch (error) {
    return {
      success: false,
      error: {
        message: error.message,
        code: 'FETCH_ERROR'
      }
    };
  }
}

Advanced Patterns

1. Batch Processing

Handle multiple items efficiently:

export default async function(args, context) {
  const { items, operation } = args;

  // Process in parallel with concurrency limit
  const batchSize = 5;
  const results = [];

  for (let i = 0; i < items.length; i += batchSize) {
    const batch = items.slice(i, i + batchSize);
    const batchResults = await Promise.all(
      batch.map(item => processItem(item, operation))
    );
    results.push(...batchResults);
  }

  return {
    processed: results.length,
    results: results
  };
}

2. Retry Logic

Implement resilient API calls:

async function fetchWithRetry(url, options, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url, options);

      if (response.ok) {
        return response;
      }

      // Don't retry client errors
      if (response.status >= 400 && response.status < 500) {
        throw new Error(`Client error: ${response.status}`);
      }

    } catch (error) {
      if (i === maxRetries - 1) throw error;

      // Exponential backoff
      await new Promise(resolve => 
        setTimeout(resolve, Math.pow(2, i) * 1000)
      );
    }
  }
}

export default async function(args, context) {
  const response = await fetchWithRetry(
    'https://api.example.com/data',
    {
      headers: {
        'Authorization': `Bearer ${context.secrets.API_KEY}`
      }
    }
  );

  return await response.json();
}

3. Data Transformation

Transform data between formats:

export default function(args) {
  const { data, fromFormat, toFormat } = args;

  // CSV to JSON
  if (fromFormat === 'csv' && toFormat === 'json') {
    const lines = data.split('\n');
    const headers = lines[0].split(',');

    return lines.slice(1).map(line => {
      const values = line.split(',');
      return headers.reduce((obj, header, index) => {
        obj[header.trim()] = values[index]?.trim();
        return obj;
      }, {});
    });
  }

  // JSON to CSV
  if (fromFormat === 'json' && toFormat === 'csv') {
    const jsonData = typeof data === 'string' ? JSON.parse(data) : data;
    const headers = Object.keys(jsonData[0]);

    const csv = [
      headers.join(','),
      ...jsonData.map(row => 
        headers.map(header => row[header] || '').join(',')
      )
    ].join('\n');

    return csv;
  }

  throw new Error(`Unsupported conversion: ${fromFormat} to ${toFormat}`);
}

4. Complex Business Logic

Implement sophisticated workflows:

export default async function(args, context) {
  const { orderId, action } = args;

  // Multi-step order processing
  switch (action) {
    case 'process_payment':
      // Validate order
      const order = await fetchOrder(orderId);
      if (order.status !== 'pending') {
        throw new Error(`Order ${orderId} is not pending`);
      }

      // Process payment
      const payment = await processPayment({
        amount: order.total,
        currency: order.currency,
        apiKey: context.secrets.PAYMENT_API_KEY
      });

      // Update inventory
      await updateInventory(order.items);

      // Send confirmation
      await sendEmail({
        to: order.customerEmail,
        template: 'order_confirmation',
        data: { orderId, payment }
      });

      return {
        success: true,
        orderId,
        paymentId: payment.id,
        status: 'processed'
      };

    case 'cancel_order':
      // Complex cancellation logic
      return await cancelOrder(orderId, context);

    default:
      throw new Error(`Unknown action: ${action}`);
  }
}

Debugging Functions

Using Console Logs

export default async function(args, context) {
  console.log('Function called with args:', args);

  try {
    const result = await someOperation(args);
    console.log('Operation successful:', result);
    return result;

  } catch (error) {
    console.error('Operation failed:', error);
    throw error;
  }
}

Testing Functions Locally

Create a test harness:

// test-function.js
import myFunction from './my-function.js';

// Mock context
const mockContext = {
  secrets: {
    API_KEY: 'test-key-123'
  },
  metadata: {
    agentId: 'agent_test',
    threadId: 'thread_test'
  }
};

// Test the function
async function test() {
  try {
    const result = await myFunction(
      { city: 'London' },
      mockContext
    );
    console.log('Success:', result);
  } catch (error) {
    console.error('Error:', error);
  }
}

test();

Common Issues

  1. Function Timeout
// ❌ May timeout
export default async function(args) {
  const results = [];
  for (const item of args.items) {
    results.push(await slowOperation(item)); // Sequential
  }
  return results;
}

// ✅ Faster parallel processing
export default async function(args) {
  const results = await Promise.all(
    args.items.map(item => slowOperation(item))
  );
  return results;
}
  1. Memory Limits
// ❌ May run out of memory
export default async function(args) {
  const hugeArray = new Array(10000000).fill(0);
  // Process huge array
}

// ✅ Process in chunks
export default async function(args) {
  const chunkSize = 1000;
  for (let i = 0; i < args.totalItems; i += chunkSize) {
    await processChunk(i, Math.min(i + chunkSize, args.totalItems));
  }
}

Security Considerations

1. Never Trust User Input

export default async function(args, context) {
  // Sanitize user input
  const sanitizedQuery = args.query
    .replace(/[<>]/g, '') // Remove potential HTML
    .substring(0, 100);   // Limit length

  // Validate against whitelist
  const allowedOperations = ['search', 'filter', 'sort'];
  if (!allowedOperations.includes(args.operation)) {
    throw new Error('Invalid operation');
  }

  // Continue with safe input
}

2. Secure API Requests

export default async function(args, context) {
  // Never expose secrets in URLs
  // ❌ Bad
  const url = `https://api.example.com/data?key=${context.secrets.API_KEY}`;

  // ✅ Good - Use headers
  const response = await fetch('https://api.example.com/data', {
    headers: {
      'Authorization': `Bearer ${context.secrets.API_KEY}`
    }
  });
}

3. Rate Limiting

Implement rate limiting for expensive operations:

const rateLimiter = new Map();

export default async function(args, context) {
  const userId = context.metadata.userId;
  const now = Date.now();

  // Check rate limit
  const userLimits = rateLimiter.get(userId) || [];
  const recentCalls = userLimits.filter(time => now - time < 60000); // Last minute

  if (recentCalls.length >= 10) {
    throw new Error('Rate limit exceeded. Max 10 calls per minute.');
  }

  // Track this call
  recentCalls.push(now);
  rateLimiter.set(userId, recentCalls);

  // Perform operation
  return await performOperation(args);
}

Examples

E-commerce Order Processing

export default async function(args, context) {
  const { action, orderId, data } = args;

  const actions = {
    calculate_tax: async () => {
      const order = await fetchOrder(orderId);
      const taxRate = await getTaxRate(order.shippingAddress);
      return {
        subtotal: order.subtotal,
        taxRate: taxRate,
        taxAmount: order.subtotal * taxRate,
        total: order.subtotal * (1 + taxRate)
      };
    },

    apply_discount: async () => {
      const { code } = data;
      const discount = await validateDiscountCode(code);

      if (!discount.valid) {
        throw new Error(`Invalid discount code: ${code}`);
      }

      const order = await fetchOrder(orderId);
      const discountAmount = order.subtotal * discount.percentage;

      return {
        discountCode: code,
        discountPercentage: discount.percentage,
        discountAmount: discountAmount,
        newTotal: order.total - discountAmount
      };
    },

    process_refund: async () => {
      const { amount, reason } = data;
      const stripe = new Stripe(context.secrets.STRIPE_API_KEY);

      const order = await fetchOrder(orderId);
      const refund = await stripe.refunds.create({
        charge: order.chargeId,
        amount: Math.round(amount * 100), // Convert to cents
        reason: reason
      });

      await updateOrderStatus(orderId, 'refunded');
      await sendEmail({
        to: order.customerEmail,
        template: 'refund_confirmation',
        data: { amount, reason, refundId: refund.id }
      });

      return {
        refundId: refund.id,
        amount: amount,
        status: 'completed'
      };
    }
  };

  if (!actions[action]) {
    throw new Error(`Unknown action: ${action}`);
  }

  return await actions[action]();
}

Data Analysis Function

export default function(args) {
  const { data, analysis } = args;

  const analyses = {
    summary: () => {
      const values = data.map(item => item.value);
      return {
        count: values.length,
        sum: values.reduce((a, b) => a + b, 0),
        average: values.reduce((a, b) => a + b, 0) / values.length,
        min: Math.min(...values),
        max: Math.max(...values)
      };
    },

    trend: () => {
      // Simple linear regression
      const n = data.length;
      const sumX = data.reduce((sum, _, i) => sum + i, 0);
      const sumY = data.reduce((sum, item) => sum + item.value, 0);
      const sumXY = data.reduce((sum, item, i) => sum + i * item.value, 0);
      const sumX2 = data.reduce((sum, _, i) => sum + i * i, 0);

      const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
      const intercept = (sumY - slope * sumX) / n;

      return {
        slope: slope,
        intercept: intercept,
        trend: slope > 0 ? 'increasing' : slope < 0 ? 'decreasing' : 'stable',
        forecast: (x) => slope * x + intercept
      };
    },

    outliers: () => {
      const values = data.map(item => item.value);
      const mean = values.reduce((a, b) => a + b, 0) / values.length;
      const stdDev = Math.sqrt(
        values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / values.length
      );

      const outliers = data.filter(item => 
        Math.abs(item.value - mean) > 2 * stdDev
      );

      return {
        outliers: outliers,
        count: outliers.length,
        threshold: mean + 2 * stdDev
      };
    }
  };

  if (!analyses[analysis]) {
    throw new Error(`Unknown analysis type: ${analysis}`);
  }

  return analyses[analysis]();
}

External Service Integration

export default async function(args, context) {
  const { service, operation, data } = args;

  const services = {
    github: {
      create_issue: async () => {
        const response = await fetch(
          `https://api.github.com/repos/${data.owner}/${data.repo}/issues`,
          {
            method: 'POST',
            headers: {
              'Authorization': `token ${context.secrets.GITHUB_TOKEN}`,
              'Accept': 'application/vnd.github.v3+json'
            },
            body: JSON.stringify({
              title: data.title,
              body: data.body,
              labels: data.labels || []
            })
          }
        );

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

        const issue = await response.json();
        return {
          issueNumber: issue.number,
          url: issue.html_url,
          state: issue.state
        };
      }
    },

    slack: {
      send_message: async () => {
        const response = await fetch(context.secrets.SLACK_WEBHOOK_URL, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            channel: data.channel,
            text: data.message,
            attachments: data.attachments
          })
        });

        return { sent: response.ok };
      }
    }
  };

  if (!services[service] || !services[service][operation]) {
    throw new Error(`Unknown service operation: ${service}.${operation}`);
  }

  return await services[service][operation]();
}

Summary

Custom functions in Ragwalla provide a secure, scalable way to extend agent capabilities with your own business logic. Key benefits:

  • Secure Sandbox - Functions run in isolation with no system access
  • Secrets Management - Sensitive values are never exposed in code
  • Edge Deployment - Functions run close to users worldwide
  • Automatic Scaling - Handle any load without configuration
  • Easy Integration - Simple JavaScript functions with async support

Whether you're processing payments, analyzing data, or integrating with external services, custom functions provide the flexibility to build sophisticated agent behaviors while maintaining security and performance.

Ready to create your first custom function? Check out our Agent Developer Guide or explore more examples.