Building AI Skills
Define new AI Copilot skills that read CRM data, ground in knowledge bases, and respect sharing.
Building AI Skills
A skill is the unit of AI functionality in HotCRM — "qualify this lead", "draft an email", "forecast revenue". This page shows how to build a new one.
When to build a skill
Build a skill when:
- The task is bounded and reusable (not a one-off).
- The output is structured enough to reason about (suggestion + rationale + action).
- It benefits from grounding in CRM data or a knowledge base.
If you just need a server-side workflow or a one-shot script, use a flow or hook instead.
Anatomy of a skill
A skill is a TypeScript file with the *.skill.ts suffix.
// packages/acme-extension/src/renewal_pitch.skill.ts
import { defineSkill } from '@objectstack/spec/ai';
export default defineSkill({
name: 'renewal_pitch',
label: 'Draft renewal pitch',
description: 'Generates a customer-facing renewal pitch grounded in their usage and contract history.',
// What the skill takes as input
input: {
contract_id: { type: 'reference', object: 'crm_contract', required: true },
tone: { type: 'enum', options: ['friendly', 'formal', 'urgent'], default: 'friendly' },
},
// What it returns
output: {
subject: { type: 'text' },
body: { type: 'text' },
talking_points: { type: 'array', items: { type: 'text' } },
citations: { type: 'array', items: { type: 'reference' } },
},
// What data sources the skill can read
data_sources: {
contract: { object: 'crm_contract', by: 'id' },
account: { object: 'crm_account', by: 'contract.account' },
primary_contact: { object: 'crm_contact', by: 'contract.account.primary_contact' },
usage: { object: 'usage_metric', by: 'contract.id', last_n_months: 6 },
},
// Knowledge bases the skill can ground in
knowledge_bases: ['sales_knowledge', 'product_information'],
// Tone, length, format
prompt_template: `
You are a sales rep writing a renewal pitch for {{primary_contact.name}}
at {{account.name}}, who has been a customer since {{account.customer_since}}.
Their contract for {{contract.value | currency}} expires on {{contract.end_date}}.
Their last 6 months of usage: {{usage.summary}}.
Write a {{tone}} renewal pitch. Cite specific value they've gotten.
Cite at most 2 KB articles for talking points.
`,
// Behaviour
behaviour: {
requires_confirmation_before_write: true,
respects_sharing: true,
log_invocations: true,
max_tokens: 800,
},
});Registering the skill with an agent
Skills are surfaced to users via an agent — typically the Sales Copilot or Service Copilot.
// packages/ai/src/sales_copilot.agent.ts (extension)
import { defineAgent } from '@objectstack/spec/ai';
export default defineAgent({
name: 'sales_copilot',
skills: [
'lead_qualification',
'email_drafting',
'revenue_forecasting',
'customer_360',
'renewal_pitch', // ← new skill registered here
],
});Or attach it directly to a record's inline-actions:
// packages/acme-extension/src/contract.page.ts
import { definePage } from '@objectstack/spec/ui';
export default definePage({
object: 'crm_contract',
inline_actions: [
{ type: 'ai_skill', skill: 'renewal_pitch', label: '✍️ Draft Renewal Pitch' },
],
});This adds a button to every contract detail page.
Grounding in knowledge bases
Reference one or more knowledge bases in knowledge_bases. The runtime will:
- Generate query embeddings from the prompt context.
- Search the bases for top-k passages.
- Inject them into the prompt with citation tokens.
- Surface citations in the output for the user to verify.
To define a custom knowledge base:
// packages/acme-extension/src/renewal_playbook.knowledge.ts
import { defineKnowledgeBase } from '@objectstack/spec/ai';
export default defineKnowledgeBase({
name: 'renewal_playbook',
label: 'Renewal Playbook',
sources: [
{ type: 'object', object: 'kb_article', filters: [['category', '=', 'renewal']] },
{ type: 'file_storage', folder: '/playbooks/renewals' },
{ type: 'confluence', space: 'CSM', label_filter: 'renewals' },
],
refresh_schedule: '0 2 * * *', // nightly at 2 AM
permissions: { read_for_profiles: ['sales_user', 'system_admin'] },
});Permissions and sharing
By default, skills run as the calling user — they see only records the user can see. This is non-negotiable for safety.
If a skill must read more (e.g., a manager-only forecast skill), declare an elevated_data_sources block and the runtime will require the calling user to hold a matching permission:
elevated_data_sources: {
all_team_opportunities: {
object: 'crm_opportunity',
requires_permission: 'view_team_pipeline',
},
},Testing skills
Unit-test the prompt logic and output shape:
import { runSkill } from '@objectstack/runtime/ai/testing';
import renewalPitch from './renewal_pitch.skill';
it('produces a pitch with citations', async () => {
const result = await runSkill(renewalPitch, {
input: { contract_id: 'ctr_123' },
as_user: 'usr_alex',
});
expect(result.subject).toContain('renewal');
expect(result.citations.length).toBeGreaterThan(0);
});Integration-test against a sandbox tenant with real CRM data.
Monitoring
After deployment, monitor:
- Invocation volume — how often is the skill used?
- Acceptance rate — how often does the user accept the output?
- Latency — p50 / p95.
- Error rate — schema mismatches, missing data, timeouts.
Surfaced in Setup → AI → Skills → Performance.
Tips
- ✅ Keep the prompt short and structured — better outputs and lower latency.
- ✅ Always include
citationsin the output schema — trust is built on traceability. - ✅ Start with
requires_confirmation_before_write: true— earn trust before automating. - ✅ Watch the acceptance rate — if <50%, the prompt or knowledge base needs work.
- ✅ Version your prompts — keep a history when you tune.
Related
- AI Copilot › Skills — the user-facing perspective.
- AI Copilot › Knowledge Bases — the RAG layer.
- Extending Objects — for the data model behind your skill.