Skip to main content
Practical recipes showing how to use CodeCall’s meta-tools for common real-world scenarios. Each example includes the full AgentScript code and expected output.
These examples assume tools are registered and available via CodeCall. See the Quick Start for setup instructions.

CRM: Multi-Tool CRUD

Combine user and activity tools to build a complete CRM workflow. Based on the CRM Demo.

App Wiring

src/app.ts
@App({
  plugins: [
    CodeCallPlugin.init({
      mode: 'codecall_only',
      topK: 10,
    }),
  ],
  tools: [
    UsersListTool, UsersGetTool, UsersCreateTool,
    UsersUpdateTool, UsersDeleteTool,
    ActivitiesListTool, ActivitiesLogTool, ActivitiesStatsTool,
  ],
})
export class CrmApp {}

Tool Catalog

ToolDescription
users:listFilter users by status/role with pagination
users:getFetch profile by ID or email
users:createCreate new user with validation
users:updatePatch name, role, status, or last login
users:deleteRemove a user
activities:listRecent activities with user/type filters
activities:logAppend activity, auto-update lastLoginAt
activities:statsActivity counts grouped by type

Script: Active Admin Dashboard

This script fetches all active administrators, pulls their recent activity in parallel, and returns a summary object suitable for rendering in a dashboard UI.
// Fetch active admins and their recent activity
const users = await callTool('users:list', { status: 'active', role: 'admin' });
const stats = await callTool('activities:stats', {});

const adminDetails = await __safe_parallel(
  users.users.map(u => () => callTool('activities:list', {
    userId: u.id,
    limit: 5,
  }))
);

return {
  adminCount: users.users.length,
  admins: users.users.map((u, i) => ({
    name: u.name,
    email: u.email,
    recentActivity: adminDetails[i].activities.length,
  })),
  activityStats: stats,
};

Script: Create User and Log Activity

A common pattern is to create a record and immediately log a related activity. This script chains two sequential tool calls.
// Create a new user
const newUser = await callTool('users:create', {
  name: codecallContext.userName,
  email: codecallContext.userEmail,
  role: 'member',
  status: 'active',
});

// Log the creation activity
await callTool('activities:log', {
  userId: newUser.id,
  type: 'account_created',
  metadata: { createdBy: 'system' },
});

return {
  message: 'User created successfully',
  user: { id: newUser.id, name: newUser.name, email: newUser.email },
};

Data Pipeline: ETL with Filtering

Fetch data from multiple sources, join, filter, and return a clean result. This pattern is especially powerful because the join and filtering logic runs inside the MCP server, keeping token usage low.
// Fetch users and their orders in parallel
const [users, orders] = await __safe_parallel([
  () => callTool('users:list', { status: 'active', limit: 100 }),
  () => callTool('orders:list', { status: 'pending', limit: 500 }),
]);

// Join orders with user data
const userMap = {};
for (const u of users.users) {
  userMap[u.id] = u;
}

const enrichedOrders = orders.orders
  .filter(o => userMap[o.userId])
  .map(o => ({
    orderId: o.id,
    amount: o.amount,
    userName: userMap[o.userId].name,
    userEmail: userMap[o.userId].email,
  }));

// Filter high-value orders
const highValue = enrichedOrders.filter(o => o.amount > 100);

return {
  totalOrders: orders.orders.length,
  enrichedCount: enrichedOrders.length,
  highValueOrders: highValue,
  totalHighValueAmount: highValue.reduce((sum, o) => sum + o.amount, 0),
};
The join and filter happen inside the MCP server, not in the LLM context. This saves thousands of tokens compared to returning raw data to the model.

Batch Processing with Parallel Execution

Process a list of items in parallel with controlled concurrency. The maxConcurrency option prevents overwhelming downstream services with too many simultaneous requests.
// Get all pending tasks
const tasks = await callTool('tasks:list', { status: 'pending', limit: 50 });

// Process each task in parallel (max 5 concurrent)
const results = await __safe_parallel(
  tasks.items.map(task => () =>
    callTool('tasks:process', { id: task.id }, { throwOnError: false })
  ),
  { maxConcurrency: 5 }
);

// Separate successes and failures
const processed = [];
const failed = [];

for (let i = 0; i < results.length; i++) {
  if (results[i].success) {
    processed.push({ id: tasks.items[i].id, result: results[i].data });
  } else {
    failed.push({ id: tasks.items[i].id, error: results[i].error.message });
  }
}

return {
  total: tasks.items.length,
  processed: processed.length,
  failed: failed.length,
  failures: failed,
};
__safe_parallel has a maximum of 100 operations. For larger batches, paginate and process in chunks.

Chunked Batch Processing

When your batch exceeds the 100-operation limit, split the work into pages and process each page sequentially while running items within each page in parallel.
const PAGE_SIZE = 50;
const MAX_PAGES = 20;  // Bounded upper limit
let allResults = [];

for (let page = 0; page < MAX_PAGES; page++) {
  const batch = await callTool('tasks:list', {
    status: 'pending',
    limit: PAGE_SIZE,
    offset: page * PAGE_SIZE,
  });

  if (batch.items.length === 0) break;

  const results = await __safe_parallel(
    batch.items.map(task => () =>
      callTool('tasks:process', { id: task.id }, { throwOnError: false })
    ),
    { maxConcurrency: 10 }
  );

  allResults = allResults.concat(results);
  if (batch.items.length < PAGE_SIZE) break;
}

return {
  totalProcessed: allResults.length,
  succeeded: allResults.filter(r => r.success).length,
  failed: allResults.filter(r => !r.success).length,
};

Aggregation Dashboard

Pull data from multiple services and combine into a single dashboard object. This pattern reduces multiple LLM round-trips into a single codecall:execute call.
// Fetch all dashboard data in parallel
const [
  userStats,
  orderStats,
  revenueData,
  supportTickets,
] = await __safe_parallel([
  () => callTool('analytics:userStats', { period: '30d' }),
  () => callTool('analytics:orderStats', { period: '30d' }),
  () => callTool('billing:revenue', { period: '30d' }),
  () => callTool('support:openTickets', {}),
]);

return {
  dashboard: {
    users: {
      total: userStats.total,
      active: userStats.active,
      newThisMonth: userStats.newThisMonth,
      growthRate: `${((userStats.newThisMonth / userStats.total) * 100).toFixed(1)}%`,
    },
    orders: {
      total: orderStats.total,
      pending: orderStats.pending,
      averageValue: orderStats.averageValue,
    },
    revenue: {
      total: revenueData.total,
      mrr: revenueData.mrr,
      currency: revenueData.currency,
    },
    support: {
      openTickets: supportTickets.count,
      avgResponseTime: supportTickets.avgResponseTimeHours,
    },
  },
  generatedAt: new Date().toISOString(),
};

Error-Resilient Workflow

Combine retry, fallback, and partial success patterns for a robust workflow. This recipe demonstrates three resilience techniques:
  1. Retry with backoff — automatically retry transient failures while skipping validation errors.
  2. Cache-then-database fallback — try a fast cache first, fall back to a slower source on miss.
  3. Partial success collection — gather results from multiple channels, continuing even if some fail.
// Helper: retry with backoff
const retry = async (toolName, args, maxAttempts = 3) => {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const result = await callTool(toolName, args, { throwOnError: false });
    if (result.success) return result.data;

    // Don't retry validation errors
    if (result.error.code === 'VALIDATION_ERROR') {
      return { error: result.error.message };
    }

    if (attempt === maxAttempts) {
      return { error: `Failed after ${maxAttempts} attempts: ${result.error.message}` };
    }
    console.warn(`${toolName} attempt ${attempt} failed, retrying...`);
  }
};

// Fetch user with retry
const user = await retry('users:get', { id: codecallContext.userId });
if (user.error) return { error: user.error };

// Try cache first, fall back to database
const prefsResult = await callTool('cache:get', { key: `prefs:${user.id}` }, {
  throwOnError: false,
});
const prefs = prefsResult.success
  ? prefsResult.data
  : await callTool('db:getUserPreferences', { userId: user.id });

// Collect notifications (partial success OK)
const channels = ['email', 'sms', 'push'];
const notifications = [];

for (const channel of channels) {
  const result = await callTool('notifications:pending', {
    userId: user.id,
    channel,
  }, { throwOnError: false });

  if (result.success) {
    notifications.push(...result.data.items.map(n => ({ ...n, channel })));
  } else {
    console.warn(`Failed to fetch ${channel} notifications: ${result.error.message}`);
  }
}

return {
  user: { name: user.name, email: user.email },
  preferences: prefs,
  notifications,
  notificationChannelsAvailable: channels.length,
};

Conditional Tool Selection

Branching logic based on tool results, running entirely server-side. The LLM calls codecall:search separately to discover tools, then writes a script that handles different cases.
// Get user and decide next action based on status
const user = await callTool('users:get', { id: codecallContext.targetUserId });

let action;
if (user.status === 'pending') {
  // Send welcome email
  action = await callTool('email:send', {
    to: user.email,
    template: 'welcome',
  }, { throwOnError: false });
} else if (user.status === 'inactive') {
  // Check last login
  const activity = await callTool('activities:list', {
    userId: user.id,
    type: 'login',
    limit: 1,
  });

  if (activity.activities.length === 0) {
    action = await callTool('email:send', {
      to: user.email,
      template: 'reactivation',
    }, { throwOnError: false });
  } else {
    action = { skipped: true, reason: 'User has recent login activity' };
  }
} else {
  action = { skipped: true, reason: `User status is ${user.status}` };
}

return {
  user: { id: user.id, name: user.name, status: user.status },
  actionTaken: action,
};

Data Validation and Cleanup

Use AgentScript to validate records in bulk and flag or fix inconsistencies before they reach downstream consumers.
// Pull all contacts that were modified in the last 24 hours
const contacts = await callTool('contacts:list', {
  modifiedSince: new Date(Date.now() - 86400000).toISOString(),
  limit: 200,
});

const issues = [];
const fixed = [];

for (const contact of contacts.items) {
  const problems = [];

  // Check for missing email
  if (!contact.email || contact.email.trim() === '') {
    problems.push('missing_email');
  }

  // Check for duplicate phone formatting
  if (contact.phone && !contact.phone.startsWith('+')) {
    // Attempt to normalize the phone number
    const updated = await callTool('contacts:update', {
      id: contact.id,
      phone: `+1${contact.phone.replace(/\D/g, '')}`,
    }, { throwOnError: false });

    if (updated.success) {
      fixed.push({ id: contact.id, field: 'phone' });
    } else {
      problems.push('phone_format_unfixable');
    }
  }

  if (problems.length > 0) {
    issues.push({ id: contact.id, name: contact.name, problems });
  }
}

return {
  scanned: contacts.items.length,
  issuesFound: issues.length,
  autoFixed: fixed.length,
  remainingIssues: issues,
};
Validation scripts make excellent scheduled workflows. Pair them with a cron-triggered prompt to run nightly data quality checks.

Common Patterns Reference

These short snippets illustrate patterns you can reuse across any recipe.

Sequential Chain

When one call depends on the output of a previous call, use plain await:
const org = await callTool('orgs:get', { id: codecallContext.orgId });
const members = await callTool('orgs:members', { orgId: org.id, limit: 50 });
return { orgName: org.name, memberCount: members.total };

Fan-Out / Fan-In

Dispatch multiple independent calls, then combine the results:
const ids = ['id-1', 'id-2', 'id-3', 'id-4'];
const details = await __safe_parallel(
  ids.map(id => () => callTool('items:get', { id }))
);
return { items: details };

Guard Clause

Exit early when preconditions are not met:
const user = await callTool('users:get', { id: codecallContext.userId });
if (user.role !== 'admin') {
  return { error: 'Only admins can perform this action' };
}
// ... continue with admin-only logic

Accumulator Loop

Build up a result across multiple pages of data:
let allItems = [];
let cursor = null;
const MAX_PAGES = 20;  // Bounded upper limit

for (let page = 0; page < MAX_PAGES; page++) {
  const result = await callTool('items:list', {
    cursor,
    limit: 100,
  });
  allItems = allItems.concat(result.items);
  cursor = result.nextCursor;
  if (!cursor) break;
}

return { totalItems: allItems.length, items: allItems };

Next Steps

AgentScript Guide

Master the scripting language: APIs, patterns, and best practices

Configuration

Tool visibility modes, VM presets, and embedding strategies

Security Model

How scripts are validated and sandboxed

CRM Demo Guide

Run the full CRM reference app locally