From 6fea21c9aa66df8e9ed9eb150a8ecfe76c675409 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 5 Feb 2026 22:43:57 -0800 Subject: [PATCH] improvement(skills): audit fixes, docs, icon, and UX polish --- apps/docs/content/docs/en/meta.json | 1 + apps/docs/content/docs/en/skills/index.mdx | 83 +++++++++++++++++++ .../app/api/permission-groups/[id]/route.ts | 1 + apps/sim/app/api/permission-groups/route.ts | 1 + apps/sim/app/api/skills/route.ts | 12 +-- .../components/skill-input/skill-input.tsx | 7 +- .../skills/components/skill-modal.tsx | 50 +++++++---- .../components/skills/skills.tsx | 4 + .../settings-modal/settings-modal.tsx | 8 +- apps/sim/components/icons.tsx | 21 +++++ .../components/access-control.tsx | 9 ++ .../handlers/agent/skills-resolver.ts | 21 ++++- 12 files changed, 190 insertions(+), 28 deletions(-) create mode 100644 apps/docs/content/docs/en/skills/index.mdx diff --git a/apps/docs/content/docs/en/meta.json b/apps/docs/content/docs/en/meta.json index b934947a3..2f3899857 100644 --- a/apps/docs/content/docs/en/meta.json +++ b/apps/docs/content/docs/en/meta.json @@ -10,6 +10,7 @@ "connections", "mcp", "copilot", + "skills", "knowledgebase", "variables", "execution", diff --git a/apps/docs/content/docs/en/skills/index.mdx b/apps/docs/content/docs/en/skills/index.mdx new file mode 100644 index 000000000..1af685ceb --- /dev/null +++ b/apps/docs/content/docs/en/skills/index.mdx @@ -0,0 +1,83 @@ +--- +title: Agent Skills +--- + +import { Callout } from 'fumadocs-ui/components/callout' + +Agent Skills are reusable packages of instructions that give your AI agents specialized capabilities. Based on the open [Agent Skills](https://agentskills.io) format, skills let you capture domain expertise, workflows, and best practices that agents can load on demand. + +## How Skills Work + +Skills use **progressive disclosure** to keep agent context lean: + +1. **Discovery** — Only skill names and descriptions are included in the agent's system prompt (~50-100 tokens each) +2. **Activation** — When the agent decides a skill is relevant, it calls the `load_skill` tool to load the full instructions into context +3. **Execution** — The agent follows the loaded instructions to complete the task + +This means you can attach many skills to an agent without bloating its context window. The agent only loads what it needs. + +## Creating Skills + +Go to **Settings** (gear icon) and select **Skills** under the Tools section. + +Click **Add** to create a new skill with three fields: + +| Field | Description | +|-------|-------------| +| **Name** | A kebab-case identifier (e.g. `sql-expert`, `code-reviewer`). Max 64 characters. | +| **Description** | A short explanation of what the skill does and when to use it. This is what the agent reads to decide whether to activate the skill. Max 1024 characters. | +| **Content** | The full skill instructions in markdown. This is loaded when the agent activates the skill. | + + + The description is critical — it's the only thing the agent sees before deciding to load a skill. Be specific about when and why the skill should be used. + + +### Writing Good Skill Content + +Skill content follows the same conventions as [SKILL.md files](https://agentskills.io/specification): + +```markdown +# SQL Expert + +## When to use this skill +Use when the user asks you to write, optimize, or debug SQL queries. + +## Instructions +1. Always ask which database engine (PostgreSQL, MySQL, SQLite) +2. Use CTEs over subqueries for readability +3. Add index recommendations when relevant +4. Explain query plans for optimization requests + +## Common Patterns +... +``` + +## Adding Skills to an Agent + +Open any **Agent** block and find the **Skills** dropdown below the tools section. Select the skills you want the agent to have access to. + +Selected skills appear as chips that you can click to edit or remove. + +### What Happens at Runtime + +When the workflow runs: + +1. The agent's system prompt includes an `` section listing each skill's name and description +2. A `load_skill` tool is automatically added to the agent's available tools +3. When the agent determines a skill is relevant to the current task, it calls `load_skill` with the skill name +4. The full skill content is returned as a tool response, giving the agent detailed instructions + +This works across all supported LLM providers — the `load_skill` tool uses standard tool-calling, so no provider-specific configuration is needed. + +## Tips + +- **Keep descriptions actionable** — Instead of "Helps with SQL", write "Write optimized SQL queries for PostgreSQL, MySQL, and SQLite, including index recommendations and query plan analysis" +- **One skill per domain** — A focused `sql-expert` skill works better than a broad `database-everything` skill +- **Use markdown structure** — Headers, lists, and code blocks help the agent parse and follow instructions +- **Test iteratively** — Run your workflow and check if the agent activates the skill when expected + +## Learn More + +- [Agent Skills specification](https://agentskills.io) — The open format for portable agent skills +- [Example skills](https://github.com/anthropics/skills) — Browse community skill examples +- [Best practices](https://agentskills.io/what-are-skills) — Writing effective skills diff --git a/apps/sim/app/api/permission-groups/[id]/route.ts b/apps/sim/app/api/permission-groups/[id]/route.ts index 977cb1bbf..bdad32bdb 100644 --- a/apps/sim/app/api/permission-groups/[id]/route.ts +++ b/apps/sim/app/api/permission-groups/[id]/route.ts @@ -24,6 +24,7 @@ const configSchema = z.object({ hideFilesTab: z.boolean().optional(), disableMcpTools: z.boolean().optional(), disableCustomTools: z.boolean().optional(), + disableSkills: z.boolean().optional(), hideTemplates: z.boolean().optional(), disableInvitations: z.boolean().optional(), hideDeployApi: z.boolean().optional(), diff --git a/apps/sim/app/api/permission-groups/route.ts b/apps/sim/app/api/permission-groups/route.ts index a72726c5a..003c3131b 100644 --- a/apps/sim/app/api/permission-groups/route.ts +++ b/apps/sim/app/api/permission-groups/route.ts @@ -25,6 +25,7 @@ const configSchema = z.object({ hideFilesTab: z.boolean().optional(), disableMcpTools: z.boolean().optional(), disableCustomTools: z.boolean().optional(), + disableSkills: z.boolean().optional(), hideTemplates: z.boolean().optional(), disableInvitations: z.boolean().optional(), hideDeployApi: z.boolean().optional(), diff --git a/apps/sim/app/api/skills/route.ts b/apps/sim/app/api/skills/route.ts index 005420c27..074ef98ec 100644 --- a/apps/sim/app/api/skills/route.ts +++ b/apps/sim/app/api/skills/route.ts @@ -1,7 +1,7 @@ import { db } from '@sim/db' import { skill } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { desc, eq } from 'drizzle-orm' +import { and, desc, eq } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' @@ -21,7 +21,7 @@ const SkillSchema = z.object({ .max(64) .regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, 'Name must be kebab-case (e.g. my-skill)'), description: z.string().min(1, 'Description is required').max(1024), - content: z.string().min(1, 'Content is required'), + content: z.string().min(1, 'Content is required').max(50000, 'Content is too large'), }) ), workspaceId: z.string().optional(), @@ -121,12 +121,14 @@ export async function POST(req: NextRequest) { { status: 400 } ) } + if (validationError instanceof Error && validationError.message.includes('already exists')) { + return NextResponse.json({ error: validationError.message }, { status: 409 }) + } throw validationError } } catch (error) { logger.error(`[${requestId}] Error updating skills`, error) - const errorMessage = error instanceof Error ? error.message : 'Failed to update skills' - return NextResponse.json({ error: errorMessage }, { status: 500 }) + return NextResponse.json({ error: 'Failed to update skills' }, { status: 500 }) } } @@ -181,7 +183,7 @@ export async function DELETE(request: NextRequest) { return NextResponse.json({ error: 'Skill not found' }, { status: 404 }) } - await db.delete(skill).where(eq(skill.id, skillId)) + await db.delete(skill).where(and(eq(skill.id, skillId), eq(skill.workspaceId, workspaceId))) logger.info(`[${requestId}] Deleted skill: ${skillId}`) return NextResponse.json({ success: true }) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/skill-input/skill-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/skill-input/skill-input.tsx index 834413ade..e0b456e8b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/skill-input/skill-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/skill-input/skill-input.tsx @@ -1,9 +1,10 @@ 'use client' import { useCallback, useMemo, useState } from 'react' -import { Plus, Sparkles, XIcon } from 'lucide-react' +import { Plus, XIcon } from 'lucide-react' import { useParams } from 'next/navigation' import { Combobox, type ComboboxOptionGroup } from '@/components/emcn' +import { AgentSkillsIcon } from '@/components/icons' import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' import { SkillModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/skills/components/skill-modal' import type { SkillDefinition } from '@/hooks/queries/skills' @@ -79,7 +80,7 @@ export function SkillInput({ return { label: s.name, value: `skill-${s.id}`, - icon: Sparkles, + icon: AgentSkillsIcon, onSelect: () => { const newSkills: StoredSkill[] = [...selectedSkills, { skillId: s.id, name: s.name }] setValue(newSkills) @@ -143,7 +144,7 @@ export function SkillInput({ } }} > - + {resolveSkillName(stored)} {!disabled && !isPreview && ( - + + {initialValues && onDelete ? ( + + ) : ( +
+ )} +
+ + +
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/skills/skills.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/skills/skills.tsx index 976a30888..dadebcd9e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/skills/skills.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/skills/skills.tsx @@ -187,6 +187,10 @@ export function Skills() { } }} onSave={handleSkillSaved} + onDelete={(skillId) => { + setEditingSkill(null) + handleDeleteClick(skillId) + }} initialValues={editingSkill ?? undefined} /> diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx index be8f3eeac..84ef6a61e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx @@ -13,7 +13,6 @@ import { Server, Settings, ShieldCheck, - Sparkles, User, Users, Wrench, @@ -35,7 +34,7 @@ import { SModalSidebarSection, SModalSidebarSectionTitle, } from '@/components/emcn' -import { McpIcon } from '@/components/icons' +import { AgentSkillsIcon, McpIcon } from '@/components/icons' import { useSession } from '@/lib/auth/auth-client' import { getSubscriptionStatus } from '@/lib/billing/client' import { getEnv, isTruthy } from '@/lib/core/config/env' @@ -159,7 +158,7 @@ const allNavigationItems: NavigationItem[] = [ }, { id: 'integrations', label: 'Integrations', icon: Connections, section: 'tools' }, { id: 'custom-tools', label: 'Custom Tools', icon: Wrench, section: 'tools' }, - { id: 'skills', label: 'Skills', icon: Sparkles, section: 'tools' }, + { id: 'skills', label: 'Skills', icon: AgentSkillsIcon, section: 'tools' }, { id: 'mcp', label: 'MCP Tools', icon: McpIcon, section: 'tools' }, { id: 'environment', label: 'Environment', icon: FolderCode, section: 'system' }, { id: 'apikeys', label: 'API Keys', icon: Key, section: 'system' }, @@ -269,6 +268,9 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { if (item.id === 'custom-tools' && permissionConfig.disableCustomTools) { return false } + if (item.id === 'skills' && permissionConfig.disableSkills) { + return false + } // Self-hosted override allows showing the item when not on hosted if (item.selfHostedOverride && !isHosted) { diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 2e1e48778..3ed87fe60 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -5436,3 +5436,24 @@ export function EnrichSoIcon(props: SVGProps) { ) } + +export function AgentSkillsIcon(props: SVGProps) { + return ( + + + + + ) +} diff --git a/apps/sim/ee/access-control/components/access-control.tsx b/apps/sim/ee/access-control/components/access-control.tsx index 83f2f28dc..dbbeac17e 100644 --- a/apps/sim/ee/access-control/components/access-control.tsx +++ b/apps/sim/ee/access-control/components/access-control.tsx @@ -367,6 +367,12 @@ export function AccessControl() { category: 'Tools', configKey: 'disableCustomTools' as const, }, + { + id: 'disable-skills', + label: 'Skills', + category: 'Tools', + configKey: 'disableSkills' as const, + }, { id: 'hide-trace-spans', label: 'Trace Spans', @@ -950,6 +956,7 @@ export function AccessControl() { !editingConfig?.hideFilesTab && !editingConfig?.disableMcpTools && !editingConfig?.disableCustomTools && + !editingConfig?.disableSkills && !editingConfig?.hideTraceSpans && !editingConfig?.disableInvitations && !editingConfig?.hideDeployApi && @@ -969,6 +976,7 @@ export function AccessControl() { hideFilesTab: allVisible, disableMcpTools: allVisible, disableCustomTools: allVisible, + disableSkills: allVisible, hideTraceSpans: allVisible, disableInvitations: allVisible, hideDeployApi: allVisible, @@ -989,6 +997,7 @@ export function AccessControl() { !editingConfig?.hideFilesTab && !editingConfig?.disableMcpTools && !editingConfig?.disableCustomTools && + !editingConfig?.disableSkills && !editingConfig?.hideTraceSpans && !editingConfig?.disableInvitations && !editingConfig?.hideDeployApi && diff --git a/apps/sim/executor/handlers/agent/skills-resolver.ts b/apps/sim/executor/handlers/agent/skills-resolver.ts index f8281b2e9..00d0f1e83 100644 --- a/apps/sim/executor/handlers/agent/skills-resolver.ts +++ b/apps/sim/executor/handlers/agent/skills-resolver.ts @@ -6,6 +6,14 @@ import type { SkillInput } from '@/executor/handlers/agent/types' const logger = createLogger('SkillsResolver') +function escapeXml(str: string): string { + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') +} + interface SkillMetadata { name: string description: string @@ -67,18 +75,25 @@ export async function resolveSkillContent( /** * Build the system prompt section that lists available skills. + * Uses XML format per the agentskills.io integration guide. */ export function buildSkillsSystemPromptSection(skills: SkillMetadata[]): string { if (!skills.length) return '' - const skillList = skills.map((s) => `- ${s.name}: ${s.description}`).join('\n') + const skillEntries = skills + .map( + (s) => + ` \n ${escapeXml(s.description)}\n ` + ) + .join('\n') return [ '', 'You have access to the following skills. Use the load_skill tool to activate a skill when relevant.', '', - 'Available skills:', - skillList, + '', + skillEntries, + '', ].join('\n') }