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
- Quick Start
- Creating Custom Functions
- The Secure Sandbox
- Managing Secrets
- Function Best Practices
- Advanced Patterns
- Debugging Functions
- Security Considerations
- Examples
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:
- args - The input parameters from the LLM
- 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
- Never hardcode secrets
// ❌ Bad
const apiKey = 'sk_live_abc123...';
// ✅ Good
const apiKey = context.secrets.API_KEY;
- 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
}
- 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
- 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;
}
- 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.