Skip to Content
FeaturesSkills System

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 | Databaseskills MongoDB collection schema


Why Skills, Not Prompts

ApproachProblem
Hard-code knowledge in system promptsPrompts become unmanageable; can’t update per tenant without code changes
Pass everything in user messageWastes tokens on every call; LLM mixes instructions with task content
Inject all skills upfrontToken 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...
FieldRequiredNotes
nameYesUnique kebab-case identifier. Used as the argument to load_skill.
descriptionYesRouting 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 platform

References 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

CategoryPurposeScope
client_contextAuto-generated client context file (company, products, audience, tone, competitors)Per-tenant — always injected in full, not via manifest
sopStandard operating procedures for each agent role (how to write a blog post, how to do keyword research)Global
platform_guidePlatform-specific rules (character limits, format rules, tone for Instagram vs LinkedIn)Global
templateOutput structure templates (blog post format, social calendar format, report format)Global
referenceKPI definitions, industry benchmarks, deliverable type definitionsGlobal
brand_voiceTenant-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: true
  • tenantId = 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-sop
  • platform-character-limits
  • blog-post-template
  • social-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:

  1. Client context file — full Markdown, written to the temp directory as client-context.md
  2. Skills manifest — a generated _SKILLS_AVAILABLE.md file 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 context

Loading a reference section:

Agent calls: load_skill({ name: 'copywriting-sop', section: 'examples' }) → Returns only the 'examples' reference block, not the full skill content

Injection 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:

ScenarioOld (upfront inject)New (two-stage)
Copywriter — blog postAll 4 assigned skills injectedManifest (~200 tokens) + 2 loaded skills (~1,200 tokens)
Copywriter — social postAll 4 assigned skills injectedManifest (~200 tokens) + 2 different skills (~600 tokens)
SEO Specialist — keyword researchAll 5 assigned skills injectedManifest (~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:

AppAccess
DashboardTenant admin can view context file, edit brand voice skill, trigger context file refresh
DM PortalDM reviewer can edit global SOPs, templates, and per-tenant overrides — including the description field
ManageSuper 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 description field is as important as the content field — 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.md

The 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.json

skills-library/ (repository root) — initial global skills in Markdown, seeded into MongoDB on pnpm db:seed.

© 2026 Leadmetrics — Internal use only