When to Build Custom AI Tooling
Off-the-shelf AI tools like Claude Code and Cursor handle most engineering tasks well. But every organization has domain-specific needs that generic tools cannot address: querying proprietary databases, generating code that follows internal frameworks, or answering questions about internal documentation. When you find your team doing the same AI-assisted task repeatedly, it is time to build a custom tool.
| Build Custom When... | Use Off-the-Shelf When... |
|---|---|
| You repeat the same AI workflow 5+ times per week | The task is general-purpose (write tests, review code) |
| The task requires domain knowledge not in public data | The task only needs public language/framework knowledge |
| You need to enforce specific output format/structure | Free-form output is acceptable |
| Multiple team members need the same AI-powered workflow | Only one person needs it occasionally |
Building a Codebase Q&A Slack Bot
One of the most impactful internal tools: a Slack bot that answers questions about your codebase. New engineers ask "Where is the payment logic?" and get an instant answer instead of waiting for a colleague.
// internal-tools/slack-codebase-bot/src/index.ts
import Anthropic from "@anthropic-ai/sdk";
import { App } from "@slack/bolt";
import { readFileSync, readdirSync, statSync } from "fs";
import { join } from "path";
const anthropic = new Anthropic();
const CODEBASE_PATH = process.env.CODEBASE_PATH || "/app/src";
// Index the codebase (simplified — use vector DB for large codebases)
function indexCodebase(dir: string, files: Map<string, string> = new Map()): Map<string, string> {
const entries = readdirSync(dir);
for (const entry of entries) {
const fullPath = join(dir, entry);
const stat = statSync(fullPath);
if (stat.isDirectory() && !entry.startsWith(".") && entry !== "node_modules") {
indexCodebase(fullPath, files);
} else if (stat.isFile() && /.(ts|tsx|js|jsx)$/.test(entry)) {
const content = readFileSync(fullPath, "utf-8");
files.set(fullPath.replace(CODEBASE_PATH, ""), content);
}
}
return files;
}
const codeIndex = indexCodebase(CODEBASE_PATH);
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET,
});
app.message(async ({ message, say }) => {
if (message.type !== "message" || !("text" in message)) return;
const question = message.text;
// Find relevant files (simplified — use embeddings for better search)
const relevantFiles = findRelevantFiles(question, codeIndex);
const context = relevantFiles
.map(([path, content]) => `--- ${path} ---\n${content}`)
.join("\n\n");
const response = await anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
system: `You are a helpful codebase assistant. Answer questions about the codebase
based on the provided code files. Be specific — reference file paths and function names.
If you are not sure, say so.`,
messages: [{
role: "user",
content: `Question: ${question}\n\nRelevant code:\n${context}`,
}],
});
const answer = response.content[0].type === "text" ? response.content[0].text : "Could not generate answer.";
await say(answer);
});
function findRelevantFiles(question: string, index: Map<string, string>): [string, string][] {
const keywords = question.toLowerCase().split(/\s+/);
const scored = [...index.entries()].map(([path, content]) => {
const score = keywords.reduce((acc, kw) =>
acc + (path.toLowerCase().includes(kw) ? 10 : 0) +
(content.toLowerCase().includes(kw) ? 1 : 0), 0);
return { path, content, score };
});
return scored
.sort((a, b) => b.score - a.score)
.slice(0, 5)
.map(({ path, content }) => [path, content]);
}
(async () => {
await app.start(process.env.PORT || 3000);
console.log("Codebase bot is running");
})();
Building a Domain-Specific Code Generator
// internal-tools/api-generator/src/generate.ts
import Anthropic from "@anthropic-ai/sdk";
import { writeFileSync, mkdirSync } from "fs";
const anthropic = new Anthropic();
interface APIEndpointSpec {
name: string; // e.g., "createOrder"
method: "GET" | "POST" | "PUT" | "DELETE";
path: string; // e.g., "/api/orders"
description: string;
requestBody?: string; // TypeScript type as string
responseBody: string; // TypeScript type as string
}
async function generateEndpoint(spec: APIEndpointSpec): Promise<void> {
const prompt = `Generate a Next.js API route handler with these specs:
- Name: ${spec.name}
- Method: ${spec.method}
- Path: ${spec.path}
- Description: ${spec.description}
${spec.requestBody ? `- Request Body Type: ${spec.requestBody}` : ""}
- Response Body Type: ${spec.responseBody}
Follow these conventions (our internal standards):
- Use Zod for request validation
- Use our auth middleware: import { withAuth } from "@/lib/auth"
- Use our database client: import { db } from "@/lib/db"
- Use our error handler: import { AppError } from "@/lib/errors"
- Return JSON with { success: boolean, data?: T, error?: string }
- Include proper error handling for 400, 401, 404, 500
- Generate the route handler file AND a test file
Return two code blocks: one for the route and one for the test.`;
const response = await anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 4096,
messages: [{ role: "user", content: prompt }],
});
const text = response.content[0].type === "text" ? response.content[0].text : "";
const codeBlocks = text.match(/```typescript([\s\S]*?)```/g) || [];
if (codeBlocks.length >= 2) {
const routeCode = codeBlocks[0].replace(/```typescript|\n```/g, "");
const testCode = codeBlocks[1].replace(/```typescript|\n```/g, "");
const routePath = `app/${spec.path}/route.ts`;
const testPath = `app/${spec.path}/route.test.ts`;
mkdirSync(`app/${spec.path}`, { recursive: true });
writeFileSync(routePath, routeCode.trim());
writeFileSync(testPath, testCode.trim());
console.log(`Generated: ${routePath}`);
console.log(`Generated: ${testPath}`);
}
}
// Usage: generate multiple endpoints from a spec file
const endpoints: APIEndpointSpec[] = [
{
name: "createOrder",
method: "POST",
path: "api/orders",
description: "Create a new order with line items",
requestBody: "{ items: { productId: string; quantity: number }[]; shippingAddress: Address }",
responseBody: "{ orderId: string; total: number; status: string }",
},
];
endpoints.forEach(generateEndpoint);
RAG for Internal Documentation
# Build a simple RAG system for internal docs:
# 1. Index your documentation
# Use a vector database (Pinecone, Weaviate, Chroma, or pgvector)
# Chunk your docs into ~500 token segments
# Generate embeddings with an embedding model
# Store in the vector database
# 2. At query time:
# Embed the user's question
# Find the top 5 most similar document chunks
# Send them as context to Claude with the question
# Return Claude's answer with source references
# 3. Keep the index updated
# Set up a CI job that re-indexes docs on every merge to main
# This ensures the AI always has current documentation
Start Small, Iterate
Do not try to build a sophisticated AI platform on day one. Start with the simplest useful tool: a script that calls the Claude API with a hardcoded prompt and your codebase context. If it saves time, iterate. Add better search, a Slack integration, or a web UI. The best internal tools grow organically from solving real pain points — not from grand architectural visions.
Summary
Building internal AI tools is the next level of AI-native engineering. Start when you notice your team repeating the same AI-assisted workflow multiple times per week. Build the simplest version first — often just a script calling the Claude API with domain-specific context. The highest-impact tools are codebase Q&A bots, domain-specific code generators, and RAG systems for internal documentation. Build for your team's specific needs, not for general-purpose AI capabilities.