Skip to content

Commit 6fea21c

Browse files
committed
improvement(skills): audit fixes, docs, icon, and UX polish
1 parent 84e77fe commit 6fea21c

File tree

12 files changed

+190
-28
lines changed

12 files changed

+190
-28
lines changed

apps/docs/content/docs/en/meta.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"connections",
1111
"mcp",
1212
"copilot",
13+
"skills",
1314
"knowledgebase",
1415
"variables",
1516
"execution",
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
---
2+
title: Agent Skills
3+
---
4+
5+
import { Callout } from 'fumadocs-ui/components/callout'
6+
7+
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.
8+
9+
## How Skills Work
10+
11+
Skills use **progressive disclosure** to keep agent context lean:
12+
13+
1. **Discovery** — Only skill names and descriptions are included in the agent's system prompt (~50-100 tokens each)
14+
2. **Activation** — When the agent decides a skill is relevant, it calls the `load_skill` tool to load the full instructions into context
15+
3. **Execution** — The agent follows the loaded instructions to complete the task
16+
17+
This means you can attach many skills to an agent without bloating its context window. The agent only loads what it needs.
18+
19+
## Creating Skills
20+
21+
Go to **Settings** (gear icon) and select **Skills** under the Tools section.
22+
23+
Click **Add** to create a new skill with three fields:
24+
25+
| Field | Description |
26+
|-------|-------------|
27+
| **Name** | A kebab-case identifier (e.g. `sql-expert`, `code-reviewer`). Max 64 characters. |
28+
| **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. |
29+
| **Content** | The full skill instructions in markdown. This is loaded when the agent activates the skill. |
30+
31+
<Callout type="info">
32+
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.
33+
</Callout>
34+
35+
### Writing Good Skill Content
36+
37+
Skill content follows the same conventions as [SKILL.md files](https://agentskills.io/specification):
38+
39+
```markdown
40+
# SQL Expert
41+
42+
## When to use this skill
43+
Use when the user asks you to write, optimize, or debug SQL queries.
44+
45+
## Instructions
46+
1. Always ask which database engine (PostgreSQL, MySQL, SQLite)
47+
2. Use CTEs over subqueries for readability
48+
3. Add index recommendations when relevant
49+
4. Explain query plans for optimization requests
50+
51+
## Common Patterns
52+
...
53+
```
54+
55+
## Adding Skills to an Agent
56+
57+
Open any **Agent** block and find the **Skills** dropdown below the tools section. Select the skills you want the agent to have access to.
58+
59+
Selected skills appear as chips that you can click to edit or remove.
60+
61+
### What Happens at Runtime
62+
63+
When the workflow runs:
64+
65+
1. The agent's system prompt includes an `<available_skills>` section listing each skill's name and description
66+
2. A `load_skill` tool is automatically added to the agent's available tools
67+
3. When the agent determines a skill is relevant to the current task, it calls `load_skill` with the skill name
68+
4. The full skill content is returned as a tool response, giving the agent detailed instructions
69+
70+
This works across all supported LLM providers — the `load_skill` tool uses standard tool-calling, so no provider-specific configuration is needed.
71+
72+
## Tips
73+
74+
- **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"
75+
- **One skill per domain** — A focused `sql-expert` skill works better than a broad `database-everything` skill
76+
- **Use markdown structure** — Headers, lists, and code blocks help the agent parse and follow instructions
77+
- **Test iteratively** — Run your workflow and check if the agent activates the skill when expected
78+
79+
## Learn More
80+
81+
- [Agent Skills specification](https://agentskills.io) — The open format for portable agent skills
82+
- [Example skills](https://github.com/anthropics/skills) — Browse community skill examples
83+
- [Best practices](https://agentskills.io/what-are-skills) — Writing effective skills

apps/sim/app/api/permission-groups/[id]/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const configSchema = z.object({
2424
hideFilesTab: z.boolean().optional(),
2525
disableMcpTools: z.boolean().optional(),
2626
disableCustomTools: z.boolean().optional(),
27+
disableSkills: z.boolean().optional(),
2728
hideTemplates: z.boolean().optional(),
2829
disableInvitations: z.boolean().optional(),
2930
hideDeployApi: z.boolean().optional(),

apps/sim/app/api/permission-groups/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const configSchema = z.object({
2525
hideFilesTab: z.boolean().optional(),
2626
disableMcpTools: z.boolean().optional(),
2727
disableCustomTools: z.boolean().optional(),
28+
disableSkills: z.boolean().optional(),
2829
hideTemplates: z.boolean().optional(),
2930
disableInvitations: z.boolean().optional(),
3031
hideDeployApi: z.boolean().optional(),

apps/sim/app/api/skills/route.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { db } from '@sim/db'
22
import { skill } from '@sim/db/schema'
33
import { createLogger } from '@sim/logger'
4-
import { desc, eq } from 'drizzle-orm'
4+
import { and, desc, eq } from 'drizzle-orm'
55
import { type NextRequest, NextResponse } from 'next/server'
66
import { z } from 'zod'
77
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
@@ -21,7 +21,7 @@ const SkillSchema = z.object({
2121
.max(64)
2222
.regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, 'Name must be kebab-case (e.g. my-skill)'),
2323
description: z.string().min(1, 'Description is required').max(1024),
24-
content: z.string().min(1, 'Content is required'),
24+
content: z.string().min(1, 'Content is required').max(50000, 'Content is too large'),
2525
})
2626
),
2727
workspaceId: z.string().optional(),
@@ -121,12 +121,14 @@ export async function POST(req: NextRequest) {
121121
{ status: 400 }
122122
)
123123
}
124+
if (validationError instanceof Error && validationError.message.includes('already exists')) {
125+
return NextResponse.json({ error: validationError.message }, { status: 409 })
126+
}
124127
throw validationError
125128
}
126129
} catch (error) {
127130
logger.error(`[${requestId}] Error updating skills`, error)
128-
const errorMessage = error instanceof Error ? error.message : 'Failed to update skills'
129-
return NextResponse.json({ error: errorMessage }, { status: 500 })
131+
return NextResponse.json({ error: 'Failed to update skills' }, { status: 500 })
130132
}
131133
}
132134

@@ -181,7 +183,7 @@ export async function DELETE(request: NextRequest) {
181183
return NextResponse.json({ error: 'Skill not found' }, { status: 404 })
182184
}
183185

184-
await db.delete(skill).where(eq(skill.id, skillId))
186+
await db.delete(skill).where(and(eq(skill.id, skillId), eq(skill.workspaceId, workspaceId)))
185187

186188
logger.info(`[${requestId}] Deleted skill: ${skillId}`)
187189
return NextResponse.json({ success: true })

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/skill-input/skill-input.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
'use client'
22

33
import { useCallback, useMemo, useState } from 'react'
4-
import { Plus, Sparkles, XIcon } from 'lucide-react'
4+
import { Plus, XIcon } from 'lucide-react'
55
import { useParams } from 'next/navigation'
66
import { Combobox, type ComboboxOptionGroup } from '@/components/emcn'
7+
import { AgentSkillsIcon } from '@/components/icons'
78
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
89
import { SkillModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/skills/components/skill-modal'
910
import type { SkillDefinition } from '@/hooks/queries/skills'
@@ -79,7 +80,7 @@ export function SkillInput({
7980
return {
8081
label: s.name,
8182
value: `skill-${s.id}`,
82-
icon: Sparkles,
83+
icon: AgentSkillsIcon,
8384
onSelect: () => {
8485
const newSkills: StoredSkill[] = [...selectedSkills, { skillId: s.id, name: s.name }]
8586
setValue(newSkills)
@@ -143,7 +144,7 @@ export function SkillInput({
143144
}
144145
}}
145146
>
146-
<Sparkles className='h-[10px] w-[10px] text-[var(--text-tertiary)]' />
147+
<AgentSkillsIcon className='h-[10px] w-[10px] text-[var(--text-tertiary)]' />
147148
<span className='max-w-[140px] truncate'>{resolveSkillName(stored)}</span>
148149
{!disabled && !isPreview && (
149150
<button

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/skills/components/skill-modal.tsx

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,19 @@ interface SkillModalProps {
2121
open: boolean
2222
onOpenChange: (open: boolean) => void
2323
onSave: () => void
24+
onDelete?: (skillId: string) => void
2425
initialValues?: SkillDefinition
2526
}
2627

2728
const KEBAB_CASE_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/
2829

29-
export function SkillModal({ open, onOpenChange, onSave, initialValues }: SkillModalProps) {
30+
export function SkillModal({
31+
open,
32+
onOpenChange,
33+
onSave,
34+
onDelete,
35+
initialValues,
36+
}: SkillModalProps) {
3037
const params = useParams()
3138
const workspaceId = params.workspaceId as string
3239

@@ -101,16 +108,20 @@ export function SkillModal({ open, onOpenChange, onSave, initialValues }: SkillM
101108
})
102109
}
103110
onSave()
104-
} catch {
105-
// Error is handled by React Query
111+
} catch (error) {
112+
const message =
113+
error instanceof Error && error.message.includes('already exists')
114+
? error.message
115+
: 'Failed to save skill. Please try again.'
116+
setFormError(message)
106117
} finally {
107118
setSaving(false)
108119
}
109120
}
110121

111122
return (
112123
<Modal open={open} onOpenChange={onOpenChange}>
113-
<ModalContent size='lg'>
124+
<ModalContent size='xl'>
114125
<ModalHeader>{initialValues ? 'Edit Skill' : 'Create Skill'}</ModalHeader>
115126
<ModalBody>
116127
<div className='flex flex-col gap-[16px]'>
@@ -127,9 +138,9 @@ export function SkillModal({ open, onOpenChange, onSave, initialValues }: SkillM
127138
if (formError) setFormError('')
128139
}}
129140
/>
130-
{formError && (
131-
<span className='text-[11px] text-[var(--text-error)]'>{formError}</span>
132-
)}
141+
<span className='text-[11px] text-[var(--text-muted)]'>
142+
Lowercase letters, numbers, and hyphens (e.g. my-skill)
143+
</span>
133144
</div>
134145

135146
<div className='flex flex-col gap-[4px]'>
@@ -163,15 +174,26 @@ export function SkillModal({ open, onOpenChange, onSave, initialValues }: SkillM
163174
className='min-h-[200px] resize-y font-mono text-[13px]'
164175
/>
165176
</div>
177+
178+
{formError && <span className='text-[11px] text-[var(--text-error)]'>{formError}</span>}
166179
</div>
167180
</ModalBody>
168-
<ModalFooter>
169-
<Button variant='default' onClick={() => onOpenChange(false)}>
170-
Cancel
171-
</Button>
172-
<Button variant='tertiary' onClick={handleSave} disabled={saving || !hasChanges}>
173-
{saving ? 'Saving...' : initialValues ? 'Update' : 'Create'}
174-
</Button>
181+
<ModalFooter className='items-center justify-between'>
182+
{initialValues && onDelete ? (
183+
<Button variant='destructive' onClick={() => onDelete(initialValues.id)}>
184+
Delete
185+
</Button>
186+
) : (
187+
<div />
188+
)}
189+
<div className='flex gap-2'>
190+
<Button variant='default' onClick={() => onOpenChange(false)}>
191+
Cancel
192+
</Button>
193+
<Button variant='tertiary' onClick={handleSave} disabled={saving || !hasChanges}>
194+
{saving ? 'Saving...' : initialValues ? 'Update' : 'Create'}
195+
</Button>
196+
</div>
175197
</ModalFooter>
176198
</ModalContent>
177199
</Modal>

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/skills/skills.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,10 @@ export function Skills() {
187187
}
188188
}}
189189
onSave={handleSkillSaved}
190+
onDelete={(skillId) => {
191+
setEditingSkill(null)
192+
handleDeleteClick(skillId)
193+
}}
190194
initialValues={editingSkill ?? undefined}
191195
/>
192196

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
Server,
1414
Settings,
1515
ShieldCheck,
16-
Sparkles,
1716
User,
1817
Users,
1918
Wrench,
@@ -35,7 +34,7 @@ import {
3534
SModalSidebarSection,
3635
SModalSidebarSectionTitle,
3736
} from '@/components/emcn'
38-
import { McpIcon } from '@/components/icons'
37+
import { AgentSkillsIcon, McpIcon } from '@/components/icons'
3938
import { useSession } from '@/lib/auth/auth-client'
4039
import { getSubscriptionStatus } from '@/lib/billing/client'
4140
import { getEnv, isTruthy } from '@/lib/core/config/env'
@@ -159,7 +158,7 @@ const allNavigationItems: NavigationItem[] = [
159158
},
160159
{ id: 'integrations', label: 'Integrations', icon: Connections, section: 'tools' },
161160
{ id: 'custom-tools', label: 'Custom Tools', icon: Wrench, section: 'tools' },
162-
{ id: 'skills', label: 'Skills', icon: Sparkles, section: 'tools' },
161+
{ id: 'skills', label: 'Skills', icon: AgentSkillsIcon, section: 'tools' },
163162
{ id: 'mcp', label: 'MCP Tools', icon: McpIcon, section: 'tools' },
164163
{ id: 'environment', label: 'Environment', icon: FolderCode, section: 'system' },
165164
{ id: 'apikeys', label: 'API Keys', icon: Key, section: 'system' },
@@ -269,6 +268,9 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
269268
if (item.id === 'custom-tools' && permissionConfig.disableCustomTools) {
270269
return false
271270
}
271+
if (item.id === 'skills' && permissionConfig.disableSkills) {
272+
return false
273+
}
272274

273275
// Self-hosted override allows showing the item when not on hosted
274276
if (item.selfHostedOverride && !isHosted) {

apps/sim/components/icons.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5436,3 +5436,24 @@ export function EnrichSoIcon(props: SVGProps<SVGSVGElement>) {
54365436
</svg>
54375437
)
54385438
}
5439+
5440+
export function AgentSkillsIcon(props: SVGProps<SVGSVGElement>) {
5441+
return (
5442+
<svg
5443+
{...props}
5444+
xmlns='http://www.w3.org/2000/svg'
5445+
width='24'
5446+
height='24'
5447+
viewBox='0 0 32 32'
5448+
fill='none'
5449+
>
5450+
<path d='M16 0.5L29.4234 8.25V23.75L16 31.5L2.57661 23.75V8.25L16 0.5Z' fill='currentColor' />
5451+
<path
5452+
d='M16 6L24.6603 11V21L16 26L7.33975 21V11L16 6Z'
5453+
fill='currentColor'
5454+
stroke='var(--background, white)'
5455+
strokeWidth='3'
5456+
/>
5457+
</svg>
5458+
)
5459+
}

0 commit comments

Comments
 (0)