This document explains how mOperator works under the hood. It's useful for understanding the system and troubleshooting issues.
┌─────────────────────────────────────────────────────────────┐
│ Slack Workspace │
│ User: @mOperator show me active campaigns │
│ User: /moperator bug Dashboard is slow │
└─────────────────┬───────────────────────────────────────────┘
│
v
┌─────────────────────────────────────────────────────────────┐
│ mOperator Backend │
│ POST /api/slack (for @mentions, DMs) │
│ POST /api/slack/commands (for /slash commands) │
│ POST /api/agent (for CLI) │
└────────────────┬──────────────────────────────────────────┬─┘
│ │
v v
┌────────────────────┐ ┌─────────────────┐
│ Slack Handling │ │ Integrations │
│ - Parse message │ │ (Auto-Discovery)
│ - Get context │ │ - Salesforce │
│ - Thread history │ │ - Linear │
│ - CSV export │ │ - GitHub │
└────────┬───────────┘ │ - Custom │
│ └────────┬────────┘
v │
┌────────────────────────────────────────────v──────┐
│ AI Model (Claude / GPT-4o) │
│ - Reads system prompt │
│ - Has access to integration tools │
│ - Decides which tool to call │
│ - Handles follow-up conversations │
└────────────────────────────────────────────────────┘
User: @mOperator show me active campaigns
Slack sends POST /api/slack with:
{
"type": "event_callback",
"event": {
"type": "app_mention",
"text": "<@U12345> show me active campaigns",
"user": "U99999",
"channel": "C12345",
"thread_ts": "1234567890.123456"
}
}
src/app/api/slack/route.ts:"show me active campaigns"bug:, feature:)If in a thread:
[
{ role: "user", content: "show me campaigns from last month" },
{ role: "assistant", content: "I found 3 campaigns..." },
{ role: "user", content: "filter to just active ones" },
]
Key rule: Consecutive messages from the same role must be merged. This is an Anthropic API requirement.
src/app/api/agent/route.ts and src/lib/ai.ts:
await generateText({
model: getAIModel(), // Claude or GPT-4o
system: SLACK_SYSTEM_PROMPT, // Instructions + active integrations
tools: getAllTools(), // Salesforce, Linear, GitHub tools
messages: [ // User message + history
{ role: "user", content: "show me active campaigns" }
],
onStepFinish: (step) => {
// Capture query results for CSV export
}
})
The system prompt says:
"You have access to Salesforce, Linear, and GitHub integrations. Use the appropriate tool to answer the question."
The AI looks at the tools available:
SALESFORCE_ACCESS_TOKEN is set):querySalesforce — Run SOQL queriesmanageSalesforceRecords — Create/update/deleteLINEAR_API_KEY is set):fileLinearIssue — Create bug or feature requestqueryLinearIssues — Search issuesGITHUB_TOKEN is set):getRepoCommits — Fetch commits and releasesThe AI calls the appropriate tool with parameters.
Example: User asks "show me active campaigns"
querySalesforce with: {
"soql": "SELECT Name, Status, CreatedDate FROM Campaign WHERE Status='Active'"
}src/lib/integrations/salesforce/tools.ts):onStepFinish() for potential CSV exportThe AI reads the tool results and generates a human-readable response:
I found 2 active campaigns:
- Holiday Sale 2024 — Created Dec 1
- New Year Campaign — Created Dec 27
Would you like details on any of these?
src/lib/slack.ts — sendSlackMessage():If the user asked for CSV or used words like "export":
if (wantsCSV(text) && result.records) {
const csv = recordsToCSV(result.records)
uploadSlackFile(channel, csv, 'export.csv')
}
recordsToCSV() converts array of records to CSV format and uploads via Slack API.
For /moperator bug, the flow is different:
// Immediate response (must be within 3 seconds)
return { response_type: "ephemeral", text: "Filing bug..." }
// Delayed response (posted minutes later via responseUrl)
waitUntil(async () => {
const result = await fileIssueFromMessage(text, 'bug')
await fetch(responseUrl, {
method: "POST",
body: JSON.stringify({
response_type: "in_channel",
text: Bug filed: <${result.url}|${result.identifier}>
})
})
})
This pattern uses Vercel's waitUntil() for background work.
Each integration is a self-contained module that mOperator discovers and loads dynamically.
src/lib/integrations/types.ts:
export interface Integration {
name: string // "Salesforce", "Linear"
description: string // "CRM and data queries"
capabilities: string[] // What the AI can do
examples: string[] // Sample prompts
isConfigured: () => boolean // Check env vars
getTools: () => Record<string, Tool> // AI SDK tools
}
src/lib/integrations/index.ts:
const ALL_INTEGRATIONS: Integration[] = [
salesforceIntegration, // Added manually in code
linearIntegration,
githubIntegration,
]
export function getActiveIntegrations(): Integration[] {
return ALL_INTEGRATIONS.filter(i => i.isConfigured())
}
Only integrations with required env vars appear in the system prompt.
isConfigured()getTools() on active integrationsIf you don't have SALESFORCE_ACCESS_TOKEN set, Salesforce tools never appear.
When a user replies in a thread, mOperator loads the conversation history:
src/lib/slack.ts — getThreadHistory():
{ role, content } pairsExample:
Raw messages:
[User] hi
[User] what's the status
[Bot] I'll check for you
[Bot] Here's the status...
[User] export this as csv
After merging:
[
{ role: "user", content: "hi\n\nwhat's the status" },
{ role: "assistant", content: "I'll check for you\n\nHere's the status..." },
{ role: "user", content: "export this as csv" },
]
This ensures the AI model (which requires alternating roles) can read the full conversation.
src/lib/agent-config.ts:
The system prompt is assembled dynamically:
You are mOperator, a Slack bot that helps marketing and sales teams...
[List of active integrations and their capabilities]
- Salesforce (CRM queries):
- Query Salesforce with natural language
- Example: "show me active campaigns"
- Linear (Issue tracking):
- File bugs and features
- Example: "bug: dashboard is slow"
[Instructions on CSV export, threading, etc.]
The key insight: The system prompt changes based on what's configured.
If GitHub isn't configured, the system prompt doesn't mention commit queries. The AI won't try to use tools that don't exist.
When a user asks for CSV or uses keywords like "export":
onStepFinish({ toolResults }) {
for (const result of toolResults) {
if (result.toolName === "querySalesforce") {
ctx.queryResults = result.output.records
}
}
} const csv = recordsToCSV(records)
// Name,Status,CreatedDate
// Campaign A,Active,2024-01-01
// Campaign B,Active,2024-01-15 await uploadSlackFile(channel, csv, 'export.csv', title, threadTs)CSV is capped at 10,000 rows to avoid performance issues.
Slash commands have a unique flow because they must respond within 3 seconds:
/moperator bug Dashboard spinner never stopsPOST /api/slack/commands with command, text, user_id, response_url async handler(ctx: CommandContext) {
// Immediate response (required within 3 seconds)
return {
response_type: "ephemeral",
text: "Filing bug report..."
} // Background work (sent later via responseUrl)
waitUntil(async () => {
const result = await fileIssueFromMessage(text, 'bug')
await fetch(responseUrl, {
method: "POST",
body: JSON.stringify({
response_type: "in_channel",
text: Bug filed: <${result.url}|${result.identifier}>
})
})
})
}
responseUrl after processing completesThis pattern is used for /moperator bug and /moperator feature.
All tools return a structured format:
{
success: true | false,
data?: any,
error?: string,
message?: string,
}
If a tool fails:
{ success: false, error: "specific message" }Example:
User: "Show me contacts"
Tool fails: { success: false, error: "Query syntax error: invalid field" }
AI response: "I tried to query contacts but got an error: invalid field. Try asking for just 'contacts' without specifying fields."
Message arrives
↓
Parse (remove @mention, check for CSV keyword)
↓
Get thread history
↓
Call generateText() with tools
↓
AI decides which tool(s) to call
↓
Tool runs (Salesforce query, file Linear issue, etc.)
↓
AI reads results
↓
AI generates response
↓
Send Slack message
↓
If CSV: upload file
npm run dev starts Next.js dev serverPOST /api/slack → Slack event handlerPOST /api/slack/commands → Slash command handlerPOST /api/agent → CLI endpointEach request is stateless; all state comes from Slack thread history or Redis.
src/app/api/slack/route.ts — Main Slack event handlersrc/app/api/slack/commands/route.ts — Slash command handlersrc/lib/ai.ts — AI model configurationsrc/lib/tools.ts — Tool assemblysrc/lib/slack.ts — Slack API utilitiessrc/lib/integrations/ — Integration modulessrc/lib/agent-config.ts — System prompt