Bot Skills — Technical Documentation
Bot Skills add deterministic, multi-step conversation flows to the ICE (Intelligent Conversation Engine) pipeline. They intercept message processing after classification/escalation but before the freeform brain runs.
Architecture
Customer Message
│
▼
┌──────────┐
│ Classify │ ← Intent + sentiment + urgency
└──────────┘
│
▼
┌──────────┐
│ Escalate? │ ← Check escalation rules
└──────────┘
│ (not escalated)
▼
┌──────────┐
│ Active │ ← Resume if thread has activeSkillState
│ Skill? │
└──────────┘
│ (no active skill)
▼
┌──────────┐
│ Match │ ← matchSkill(): intent OR keyword + conditions
│ Skill? │
└──────────┘
│ (no match)
▼
┌──────────┐
│ Brain │ ← Freeform AI: decide → generate → comply
└──────────┘
Database Models
BotSkill
| Column | Type | Description |
|---|---|---|
id | String (cuid) | Primary key |
workspaceId | String | FK to Workspace |
name | String | Skill name |
description | String? | Optional description |
isEnabled | Boolean | Default true |
priority | Int | Lower = higher priority (default 0) |
triggerIntents | String[] | Array of intent types that trigger this skill |
triggerKeywords | String[] | Array of normalized keywords |
conditions | Json? | Array of SkillStepCondition objects |
steps | Json | Array of BotSkillStep objects |
Indexes: @@index([workspaceId, isEnabled])
BotSkillExecution
| Column | Type | Description |
|---|---|---|
id | String (cuid) | Primary key |
workspaceId | String | FK to Workspace |
skillId | String | FK to BotSkill |
threadId | String | FK to ConversationThread |
status | String | in_progress | completed | abandoned | failed |
currentStepId | String | Current step being executed |
stepsSnapshot | Json | Frozen copy of steps at execution start |
collectedData | Json | Key-value data collected from customer |
startedAt | DateTime | Execution start time |
completedAt | DateTime? | When skill finished |
failedReason | String? | Error description if failed |
Indexes: @@index([workspaceId]), @@index([threadId, status])
ConversationThread Addition
activeSkillState Json?— StoresBotSkillExecutionStatefor active skill
Shared Types
Key Types (@omnisocial/shared)
type BotSkillStepType = 'message' | 'collect_input' | 'condition' | 'tool_call' | 'transfer' | 'end';
interface BotSkillStep {
id: string;
type: BotSkillStepType;
name: string;
messages?: Record<string, string>; // locale map
inputPrompt?: Record<string, string>; // locale map
inputKey?: string;
inputValidation?: { type: 'text'|'email'|'phone'|'number'|'choice'; options?: string[]; regex?: string; errorMessage?: Record<string, string> };
branches?: Array<{ conditions: SkillStepCondition[]; nextStepId: string }>;
defaultNextStepId?: string;
toolName?: string;
toolInput?: Record<string, string>; // e.g. { query: "collectedData.orderNumber" }
toolResultKey?: string;
toolResultMessage?: Record<string, string>;
transferReason?: Record<string, string>;
transferPriority?: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
nextStepId?: string | null;
}
interface SkillStepCondition {
field: string;
operator: 'eq' | 'neq' | 'contains' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'matches';
value: unknown;
}
interface BotSkillExecutionState {
executionId: string;
skillId: string;
currentStepId: string;
stepsSnapshot: BotSkillStep[];
collectedData: Record<string, unknown>;
}
Service Layer
File: apps/api/src/services/bot-skill.service.ts
CRUD Functions
| Function | Description |
|---|---|
listSkills(workspaceId) | List all skills ordered by priority |
getSkill(workspaceId, skillId) | Get single skill |
createSkill(workspaceId, input) | Create skill (validates step refs, normalizes keywords) |
updateSkill(workspaceId, skillId, input) | Update skill (same validations) |
deleteSkill(workspaceId, skillId) | Delete skill |
toggleSkill(workspaceId, skillId, enabled) | Enable/disable skill |
Matching
matchSkill(workspaceId, classification, messageText):
- Load all enabled skills ordered by priority
- For each skill, check if intent matches OR normalized keyword is found in message
- If conditions are present, evaluate all structured conditions against classification context
- Return first match or
null
Execution Engine
startSkillExecution(workspaceId, threadId, skill):
- Snapshot the skill's steps into a new
BotSkillExecutionrecord - Set
activeSkillStateon the thread - Run
runStepsUntilPause()— executes steps in a loop until hittingcollect_input,transfer, orend - Return the accumulated reply text
continueSkillExecution(workspaceId, threadId, customerMessage):
- Read
activeSkillStatefrom thread - If current step is
collect_input, validate the customer's message and store it - Advance to next step and run
runStepsUntilPause() - Update execution state or clear it if completed
abandonSkillExecution(workspaceId, threadId):
- Marks execution as abandoned and clears
activeSkillState
Step Execution Loop
runStepsUntilPause() iterates through steps with a safety limit of 20 iterations:
| Step Type | Behavior |
|---|---|
message | Resolve locale message, add to reply, advance to nextStepId |
collect_input | Resolve locale prompt, add to reply, pause (return state) |
condition | Evaluate branches against collectedData, jump to matching step |
tool_call | Resolve inputs from collectedData, call tool executor, format result, advance |
transfer | Resolve reason, add to reply, return with action: 'transfer' |
end | Resolve message, add to reply, return with action: 'end' |
Keyword Normalization
normalizeKeyword(keyword):
- Lowercase + trim
- Arabic alef normalization:
أ,إ,آ→ا - Taa marbuta normalization:
ة→ه
Language Detection
Unicode script analysis — counts Arabic vs Latin characters. Falls back to en.
Locale Resolution
resolveLocaleMessage(messages, language):
- Try detected language
- Fall back to
en - Fall back to first available key
- Return empty string if no messages
API Routes
All routes are under /ice/ prefix on the existing ICE router.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /ice/skills | Any member | List all skills |
| GET | /ice/skills/:id | Any member | Get skill by ID |
| POST | /ice/skills | OWNER/ADMIN | Create new skill |
| PUT | /ice/skills/:id | OWNER/ADMIN | Update skill |
| DELETE | /ice/skills/:id | OWNER/ADMIN | Delete skill |
| PUT | /ice/skills/:id/toggle | OWNER/ADMIN | Toggle enabled/disabled |
| GET | /ice/skill-executions | Any member | List execution history |
Pipeline Integration
File: apps/api/src/workers/ice-process.worker.ts
Two insertion points in the message processing pipeline:
1. Skill Continuation (before classification)
If thread.activeSkillState exists, the message is routed to continueSkillExecution(). The reply is delivered and processing returns early — no classification or brain execution occurs.
2. Skill Matching (after escalation, before brain)
After escalation rules are checked, matchSkill() is called. If a skill matches, startSkillExecution() is called, the first reply is delivered, and processing returns early.
Frontend
Components
- SkillsTab (
agent-builder/SkillsTab.tsx) — Skill list with cards, toggle, delete, create/edit buttons - SkillEditorDialog (
agent-builder/SkillEditorDialog.tsx) — Full dialog for creating/editing skills with step editor
API Client
All methods on iceApi object in apps/web/src/api/ice.ts:
listSkills(),getSkill(id),createSkill(data),updateSkill(id, data),deleteSkill(id),toggleSkill(id, enabled),listSkillExecutions(params)
Testing
- Backend:
apps/api/src/services/__tests__/bot-skill.service.test.ts— CRUD, keyword normalization, step reference validation, skill matching, execution engine - Frontend:
apps/web/src/pages/conversation-engine/agent-builder/__tests__/SkillsTab.test.tsx— Rendering, toggle, delete, create/edit dialog integration
Design Decisions
- Structured conditions over string eval —
{ field, operator, value }objects prevent code injection - Locale maps over separate fields —
Record<string, string>scales to any number of languages - Step snapshots — Steps are frozen at execution start to prevent mid-conversation breakage if the skill is edited
- Structured toolInput —
Record<string, string>mapping instead of template strings avoids injection - Step ID validation — All
nextStepId/branch references are validated on create/update to prevent dangling references - Arabic keyword normalization — Alef variants and taa marbuta are normalized for consistent matching