Skills System
Purpose
Inject domain-specific marketing knowledge into agents at runtime — brand voice guides, SEO rules, platform SOPs, the client context file — without hard-coding knowledge into prompts. Skills are Markdown files with YAML frontmatter stored in MongoDB.
Loading is two-stage: the agent first receives a lightweight manifest of available skills (names + routing descriptions only), then calls a load_skill tool to pull in the full content of whichever skills are relevant to the current task. Only the client context file is always injected in full upfront.
Related: Onboarding Agent — generates the client context file | LLM Providers — how skills are injected per adapter | Database —
skillsMongoDB collection schema
Why Skills, Not Prompts
| Approach | Problem |
|---|---|
| Hard-code knowledge in system prompts | Prompts become unmanageable; can’t update per tenant without code changes |
| Pass everything in user message | Wastes tokens on every call; LLM mixes instructions with task content |
| Inject all skills upfront | Token budget fills up even when most skills are irrelevant to the task |
| Two-stage skills (manifest + lazy load) | Agent only loads what’s relevant; token cost scales with task, not skill library size |
Skill Authoring
Each skill is a Markdown file with required YAML frontmatter.
Frontmatter fields
---
name: copywriting-sop
description: >
Use when writing blog posts, social copy, email campaigns, or any
client-facing long-form or short-form content. Don't use for data
analysis, keyword research, or technical configuration tasks.
---
# Copywriting SOP
...skill instructions...| Field | Required | Notes |
|---|---|---|
name | Yes | Unique kebab-case identifier. Used as the argument to load_skill. |
description | Yes | Routing logic — tells the agent when to load this skill. Write as “Use when… Don’t use when…” so the agent can make a binary relevance decision from the manifest alone. |
Description writing rules:
- Lead with “Use when” — name the specific task types this skill helps with
- Follow with “Don’t use when” — list tasks where loading this skill wastes tokens
- Keep under 50 words — the manifest lists all skills; descriptions must be scannable
- Be specific about task type, not about content domain — agents route on task type
References
Large auxiliary content (example outputs, lookup tables, character limit tables, benchmark data) lives in a references/ subdirectory alongside the skill file. References are stored as separate content blocks in MongoDB and loaded only if the agent requests them via load_skill with a section argument.
skills-library/
└── copywriting-sop/
├── SKILL.md # Main instructions (loaded via load_skill)
└── references/
├── examples.md # Before/after copy examples
└── tone-cheatsheet.md # Quick tone reference per platformReferences are not listed in the manifest — the skill’s main content should tell the agent which reference sections exist if they are useful.
Skill Categories
| Category | Purpose | Scope |
|---|---|---|
client_context | Auto-generated client context file (company, products, audience, tone, competitors) | Per-tenant — always injected in full, not via manifest |
sop | Standard operating procedures for each agent role (how to write a blog post, how to do keyword research) | Global |
platform_guide | Platform-specific rules (character limits, format rules, tone for Instagram vs LinkedIn) | Global |
template | Output structure templates (blog post format, social calendar format, report format) | Global |
reference | KPI definitions, industry benchmarks, deliverable type definitions | Global |
brand_voice | Tenant-specific brand voice override (if the context file tone isn’t enough) | Per-tenant override |
client_context is the only category that bypasses two-stage loading. Every other category goes through the manifest.
Skill Storage (MongoDB)
Skills are stored in the MongoDB skills collection, not on disk. The skills-library/ directory in the repo contains the initial global skills that are seeded into MongoDB on first run.
MongoDB: skills collection
{
_id: ObjectId,
tenantId: string | null, // null = global skill
name: string, // kebab-case, unique per tenantId scope
description: string, // routing description — shown in manifest
category: string, // see categories above
content: string, // Markdown content (main SKILL.md body)
references: Array<{ // optional auxiliary sections
name: string, // section identifier (e.g. 'examples')
content: string, // Markdown content
}>,
isClientContext: boolean, // true = this tenant's context file
assignedAgentRoles: AgentRole[], // which agents receive this skill in their manifest
version: number, // incremented on each edit
createdAt: Date,
updatedAt: Date,
}Client context file is a special skill:
category: 'client_context',isClientContext: truetenantId= the tenant it was generated for- Auto-generated by the Onboarding Agent; reviewed and approved by tenant admin
- Injected in full into every agent run — not through the manifest mechanism
Skill Assignment
Default (global) skills
Agent roles are configured with a default set of global skills in agent_configs (or as system defaults). Every activity for that agent role includes these skills in its manifest.
Example — Copywriter manifest always includes:
copywriting-sopplatform-character-limitsblog-post-templatesocial-post-template
Whether the agent actually loads all four depends on what it determines is relevant to the specific task.
Tenant-specific overrides
Skills with tenantId set override the global skill of the same category for that tenant’s agent runs. The overriding skill appears in the manifest in place of the global one.
Example: A tenant has a custom brand_voice skill. The Copywriter’s manifest for that tenant shows the custom brand voice entry instead of the global default.
Client context file (always injected)
The tenant’s context file is always injected in full regardless of the skill assignment config. It is the minimum viable context for any agent working on a tenant’s deliverables and is always relevant.
Skill resolution
interface SkillManifestEntry {
name: string;
description: string;
_id: string;
hasRefs: boolean; // true if references[] is non-empty
}
interface ResolvedSkillSet {
contextFile: Skill | null; // always injected in full
manifest: SkillManifestEntry[]; // metadata-only; loaded on demand
}
async function resolveSkillsForActivity(
tenantId: string,
agentRole: AgentRole,
): Promise<ResolvedSkillSet> {
// 1. Get global skills assigned to this agent role
const globalSkills = await skillsCollection.find({
tenantId: null,
assignedAgentRoles: agentRole,
}).toArray();
// 2. Get tenant-specific overrides for this agent role
const tenantSkills = await skillsCollection.find({
tenantId,
assignedAgentRoles: agentRole,
isClientContext: { $ne: true },
}).toArray();
// 3. Always include the client context file (full content, not manifest)
const contextFile = await skillsCollection.findOne({
tenantId,
isClientContext: true,
});
// 4. Tenant skills override globals of the same category
const skillMap = new Map(globalSkills.map(s => [s.category, s]));
for (const skill of tenantSkills) skillMap.set(skill.category, skill);
// 5. Build manifest — name + description only, no full content
const manifest: SkillManifestEntry[] = [...skillMap.values()].map(s => ({
name: s.name,
description: s.description,
_id: s._id.toString(),
hasRefs: s.references.length > 0,
}));
return { contextFile, manifest };
}Two-Stage Loading at Runtime
Stage 1 — Manifest injection (pre-run)
The agent starts with two items in its context:
- Client context file — full Markdown, written to the temp directory as
client-context.md - Skills manifest — a generated
_SKILLS_AVAILABLE.mdfile listing each skill’s name and description
# Available Skills
The following skills are available for this task. Call `load_skill` with the
skill name to retrieve the full instructions for any skill that applies.
---
**copywriting-sop**
Use when writing blog posts, social copy, email campaigns, or any client-facing
long-form or short-form content. Don't use for data analysis or keyword research.
---
**blog-post-template**
Use when structuring a new blog post output. Don't use for social media formats
or short-form copy.
---
**platform-character-limits**
Use when publishing or formatting content for a specific social platform.
Load this to check character limits, image specs, and hashtag rules.Stage 2 — On-demand loading via load_skill tool
During execution, the agent calls load_skill for each relevant skill:
name: load_skill
description:
Load the full instructions for a skill listed in the skills manifest. Only call
this for skills that are directly relevant to the current task. Calling
irrelevant skills wastes your context budget.
inputs:
name (string, required) — the skill name exactly as listed in the manifest
section (string, optional) — name of a reference section to load instead of
the main skill content (only if the skill has
hasRefs: true and you need auxiliary detail)Invocation flow:
Agent reads _SKILLS_AVAILABLE.md
│
▼
Agent determines: which skills apply to this task?
→ "I'm writing a blog post. I need copywriting-sop and blog-post-template."
→ "platform-character-limits is not relevant for a long-form blog post."
│
▼
Agent calls: load_skill({ name: 'copywriting-sop' })
│
▼
Tool dispatcher:
1. Verify name is in the resolved manifest for this tenant/role
2. Fetch full content from MongoDB by name + tenantId scope
3. Return content string to agent
│
▼
Agent incorporates skill content into working context
│
▼
Agent calls: load_skill({ name: 'blog-post-template' })
│
▼
Agent proceeds with task using loaded contextLoading a reference section:
Agent calls: load_skill({ name: 'copywriting-sop', section: 'examples' })
→ Returns only the 'examples' reference block, not the full skill contentInjection Flow Summary
Activity worker picks up job
│
▼
resolveSkillsForActivity(tenantId, agentRole)
→ contextFile: full client context Markdown
→ manifest: [{ name, description, _id, hasRefs }, ...]
│
▼
Create temp directory /tmp/skills-<uuid>/
Write: client-context.md (full content — always present)
Write: _SKILLS_AVAILABLE.md (generated manifest — names + descriptions)
│
▼
Register load_skill tool in adapter tool list
│
▼
Pass tempDir to adapter
Claude: --add-dir /tmp/skills-<uuid>
Codex: symlink tempDir into CODEX_HOME/skills/ (Codex reads from skills dir automatically)
Gemini: symlink tempDir into ~/.gemini/skills/ (Gemini reads from skills dir automatically)
Ollama/OpenAI: client-context.md + _SKILLS_AVAILABLE.md prepended to system prompt
load_skill registered as a tool
│
▼
Agent runs
→ reads manifest, calls load_skill() for relevant skills
→ each load_skill call fetches from MongoDB on demand
│
▼
finally: rm -rf /tmp/skills-<uuid>Token Budget Impact
The two-stage approach bounds the token cost of skills to what the agent actually uses:
| Scenario | Old (upfront inject) | New (two-stage) |
|---|---|---|
| Copywriter — blog post | All 4 assigned skills injected | Manifest (~200 tokens) + 2 loaded skills (~1,200 tokens) |
| Copywriter — social post | All 4 assigned skills injected | Manifest (~200 tokens) + 2 different skills (~600 tokens) |
| SEO Specialist — keyword research | All 5 assigned skills injected | Manifest (~250 tokens) + 1–2 loaded skills (~800 tokens) |
Skills not loaded by the agent consume only their manifest entry (~30–50 tokens each). This directly reduces pressure on the combined skills + RAG token budget (see OQ-25).
Skills Manager UI
Available in all three apps with different access levels:
| App | Access |
|---|---|
| Dashboard | Tenant admin can view context file, edit brand voice skill, trigger context file refresh |
| DM Portal | DM reviewer can edit global SOPs, templates, and per-tenant overrides — including the description field |
| Manage | Super admin can manage all global skills (CRUD), seed new agent skill sets, edit descriptions |
Skills are stored in MongoDB — edits in the UI immediately update the content and description fields. Version history is preserved via the version counter.
Note: The
descriptionfield is as important as thecontentfield — a poorly written description causes agents to skip loading a skill they need, or to load one they don’t. The DM Portal Skills Manager should display both fields with equal prominence.
Screen reference: W7 (Context File management), W8 (Tenant Agent Configuration) in workflow-screens.md
Skills Library Structure
skills-library/ (repository root) — initial global skills seeded into MongoDB on pnpm db:seed.
skills-library/
├── copywriting-sop/
│ ├── SKILL.md
│ └── references/
│ ├── examples.md
│ └── tone-cheatsheet.md
├── blog-post-template/
│ └── SKILL.md
├── social-post-template/
│ └── SKILL.md
├── platform-character-limits/
│ ├── SKILL.md
│ └── references/
│ └── platform-specs-table.md
├── seo-research-sop/
│ ├── SKILL.md
│ └── references/
│ └── keyword-scoring-guide.md
├── social-media-sop/
│ └── SKILL.md
├── paid-ads-sop/
│ └── SKILL.md
├── data-analyst-sop/
│ └── SKILL.md
└── kpi-definitions/
└── SKILL.mdThe seed script reads each SKILL.md (frontmatter + body), reads each references/*.md file, and upserts the combined document into MongoDB.
Package Location
packages/skills/
skills/
├── src/
│ ├── resolver.ts # resolveSkillsForActivity() — returns contextFile + manifest
│ ├── injector.ts # createTempDir, write context file + manifest file, cleanup
│ ├── load-skill-tool.ts # load_skill tool handler — fetches full content from MongoDB
│ ├── manifest.ts # buildManifestFile() — renders _SKILLS_AVAILABLE.md
│ └── types.ts
└── package.jsonskills-library/ (repository root) — initial global skills in Markdown, seeded into MongoDB on pnpm db:seed.