Skip to content

Commit 8d75b84

Browse files
committed
improvement(ui): improved skills UI, validation, and permissions
1 parent 1e21ec1 commit 8d75b84

File tree

8 files changed

+191
-61
lines changed

8 files changed

+191
-61
lines changed

apps/docs/content/docs/en/skills/index.mdx

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ This means you can attach many skills to an agent without bloating its context w
1818

1919
## Creating Skills
2020

21-
Go to **Settings** (gear icon) and select **Skills** under the Tools section.
21+
Go to **Settings** and select **Skills** under the Tools section.
22+
23+
![Manage Skills](/static/skills/manage-skills.png)
2224

2325
Click **Add** to create a new skill with three fields:
2426

@@ -52,11 +54,22 @@ Use when the user asks you to write, optimize, or debug SQL queries.
5254
...
5355
```
5456

57+
**Recommended structure:**
58+
- **When to use** — Specific triggers and scenarios
59+
- **Instructions** — Step-by-step guidance with numbered lists
60+
- **Examples** — Input/output samples showing expected behavior
61+
- **Common Patterns** — Reusable approaches for frequent tasks
62+
- **Edge Cases** — Gotchas and special considerations
63+
64+
Keep skills focused and under 500 lines. If a skill grows too large, split it into multiple specialized skills.
65+
5566
## Adding Skills to an Agent
5667

5768
Open any **Agent** block and find the **Skills** dropdown below the tools section. Select the skills you want the agent to have access to.
5869

59-
Selected skills appear as chips that you can click to edit or remove.
70+
![Add Skill](/static/skills/add-skill.png)
71+
72+
Selected skills appear as cards that you can click to edit or remove.
6073

6174
### What Happens at Runtime
6275

@@ -69,12 +82,50 @@ When the workflow runs:
6982

7083
This works across all supported LLM providers — the `load_skill` tool uses standard tool-calling, so no provider-specific configuration is needed.
7184

72-
## Tips
85+
## Common Use Cases
86+
87+
Skills are most valuable when agents need specialized knowledge or multi-step workflows:
88+
89+
**Domain Expertise**
90+
- `api-integration-expert` — Best practices for calling specific APIs (authentication, rate limiting, error handling)
91+
- `data-transformation` — ETL patterns, data cleaning, and validation rules
92+
- `code-reviewer` — Code review guidelines specific to your team's standards
93+
94+
**Workflow Templates**
95+
- `bug-investigation` — Step-by-step debugging methodology (reproduce → isolate → test → fix)
96+
- `feature-implementation` — Development workflow from requirements to deployment
97+
- `document-generator` — Templates and formatting rules for technical documentation
98+
99+
**Company-Specific Knowledge**
100+
- `our-architecture` — System architecture diagrams, service dependencies, and deployment processes
101+
- `style-guide` — Brand guidelines, writing tone, UI/UX patterns
102+
- `customer-onboarding` — Standard procedures and common customer questions
103+
104+
**When to use skills vs. agent instructions:**
105+
- Use **skills** for knowledge that applies across multiple workflows or changes frequently
106+
- Use **agent instructions** for task-specific context that's unique to a single agent
107+
108+
## Best Practices
109+
110+
**Writing Effective Descriptions**
111+
- **Be specific and keyword-rich** — Instead of "Helps with SQL", write "Write optimized SQL queries for PostgreSQL, MySQL, and SQLite, including index recommendations and query plan analysis"
112+
- **Include activation triggers** — Mention specific words or phrases that should prompt the skill (e.g., "Use when the user mentions PDFs, forms, or document extraction")
113+
- **Keep it under 200 words** — Agents scan descriptions quickly; make every word count
73114

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"
115+
**Skill Scope and Organization**
75116
- **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
117+
- **Limit to 5-10 skills per agent** — More skills = more decision overhead; start small and add as needed
118+
- **Split large skills** — If a skill exceeds 500 lines, break it into focused sub-skills
119+
120+
**Content Structure**
121+
- **Use markdown formatting** — Headers, lists, and code blocks help agents parse and follow instructions
122+
- **Provide examples** — Show input/output pairs so agents understand expected behavior
123+
- **Be explicit about edge cases** — Don't assume agents will infer special handling
124+
125+
**Testing and Iteration**
126+
- **Test activation** — Run your workflow and verify the agent loads the skill when expected
127+
- **Check for false positives** — Make sure skills aren't activating when they shouldn't
128+
- **Refine descriptions** — If a skill isn't loading when needed, add more keywords to the description
78129

79130
## Learn More
80131

27.9 KB
Loading
56.2 KB
Loading

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

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -130,39 +130,52 @@ export function SkillInput({
130130
onOpenChange={setOpen}
131131
/>
132132

133-
{selectedSkills.length > 0 && (
134-
<div className='flex flex-wrap gap-[4px]'>
135-
{selectedSkills.map((stored) => {
136-
const fullSkill = workspaceSkills.find((s) => s.id === stored.skillId)
137-
return (
133+
{selectedSkills.length > 0 &&
134+
selectedSkills.map((stored) => {
135+
const fullSkill = workspaceSkills.find((s) => s.id === stored.skillId)
136+
return (
137+
<div
138+
key={stored.skillId}
139+
className='group relative flex flex-col overflow-hidden rounded-[4px] border border-[var(--border-1)] transition-all duration-200 ease-in-out'
140+
>
138141
<div
139-
key={stored.skillId}
140-
className='flex cursor-pointer items-center gap-[4px] rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] px-[6px] py-[2px] font-medium text-[12px] text-[var(--text-secondary)] hover:bg-[var(--surface-6)]'
142+
className='flex cursor-pointer items-center justify-between gap-[8px] rounded-t-[4px] bg-[var(--surface-4)] px-[8px] py-[6.5px]'
141143
onClick={() => {
142144
if (fullSkill && !disabled && !isPreview) {
143145
setEditingSkill(fullSkill)
144146
}
145147
}}
146148
>
147-
<AgentSkillsIcon className='h-[10px] w-[10px] text-[var(--text-tertiary)]' />
148-
<span className='max-w-[140px] truncate'>{resolveSkillName(stored)}</span>
149-
{!disabled && !isPreview && (
150-
<button
151-
type='button'
152-
onClick={(e) => {
153-
e.stopPropagation()
154-
handleRemove(stored.skillId)
155-
}}
156-
className='ml-[2px] rounded-[2px] p-[1px] text-[var(--text-tertiary)] hover:bg-[var(--surface-7)] hover:text-[var(--text-secondary)]'
149+
<div className='flex min-w-0 flex-1 items-center gap-[8px]'>
150+
<div
151+
className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center rounded-[4px]'
152+
style={{ backgroundColor: '#e0e0e0' }}
157153
>
158-
<XIcon className='h-[10px] w-[10px]' />
159-
</button>
160-
)}
154+
<AgentSkillsIcon className='h-[10px] w-[10px] text-[#333]' />
155+
</div>
156+
<span className='truncate font-medium text-[13px] text-[var(--text-primary)]'>
157+
{resolveSkillName(stored)}
158+
</span>
159+
</div>
160+
<div className='flex flex-shrink-0 items-center gap-[8px]'>
161+
{!disabled && !isPreview && (
162+
<button
163+
type='button'
164+
onClick={(e) => {
165+
e.stopPropagation()
166+
handleRemove(stored.skillId)
167+
}}
168+
className='flex items-center justify-center text-[var(--text-tertiary)] transition-colors hover:text-[var(--text-primary)]'
169+
aria-label='Remove skill'
170+
>
171+
<XIcon className='h-[13px] w-[13px]' />
172+
</button>
173+
)}
174+
</div>
161175
</div>
162-
)
163-
})}
164-
</div>
165-
)}
176+
</div>
177+
)
178+
})}
166179
</div>
167180

168181
<SkillModal

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-editor-subblock-layout.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
isSubBlockVisibleForMode,
77
} from '@/lib/workflows/subblocks/visibility'
88
import type { BlockConfig, SubBlockConfig, SubBlockType } from '@/blocks/types'
9+
import { usePermissionConfig } from '@/hooks/use-permission-config'
910
import { useWorkflowDiffStore } from '@/stores/workflow-diff'
1011
import { mergeSubblockState } from '@/stores/workflows/utils'
1112
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
@@ -35,6 +36,7 @@ export function useEditorSubblockLayout(
3536
const blockDataFromStore = useWorkflowStore(
3637
useCallback((state) => state.blocks?.[blockId]?.data, [blockId])
3738
)
39+
const { config: permissionConfig } = usePermissionConfig()
3840

3941
return useMemo(() => {
4042
// Guard against missing config or block selection
@@ -100,6 +102,9 @@ export function useEditorSubblockLayout(
100102
const visibleSubBlocks = (config.subBlocks || []).filter((block) => {
101103
if (block.hidden) return false
102104

105+
// Hide skill-input subblock when skills are disabled via permissions
106+
if (block.type === 'skill-input' && permissionConfig.disableSkills) return false
107+
103108
// Check required feature if specified - declarative feature gating
104109
if (!isSubBlockFeatureEnabled(block)) return false
105110

@@ -149,5 +154,6 @@ export function useEditorSubblockLayout(
149154
activeWorkflowId,
150155
isSnapshotView,
151156
blockDataFromStore,
157+
permissionConfig.disableSkills,
152158
])
153159
}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { useCustomTools } from '@/hooks/queries/custom-tools'
4040
import { useMcpServers, useMcpToolsQuery } from '@/hooks/queries/mcp'
4141
import { useCredentialName } from '@/hooks/queries/oauth-credentials'
4242
import { useReactivateSchedule, useScheduleInfo } from '@/hooks/queries/schedules'
43+
import { useSkills } from '@/hooks/queries/skills'
4344
import { useDeployChildWorkflow } from '@/hooks/queries/workflows'
4445
import { useSelectorDisplayName } from '@/hooks/use-selector-display-name'
4546
import { useVariablesStore } from '@/stores/panel'
@@ -618,6 +619,43 @@ const SubBlockRow = memo(function SubBlockRow({
618619
return `${toolNames[0]}, ${toolNames[1]} +${toolNames.length - 2}`
619620
}, [subBlock?.type, rawValue, customTools, workspaceId])
620621

622+
/**
623+
* Hydrates skill references to display names.
624+
* Resolves skill IDs to their current names from the skills query.
625+
*/
626+
const { data: workspaceSkills = [] } = useSkills(workspaceId || '')
627+
628+
const skillsDisplayValue = useMemo(() => {
629+
if (subBlock?.type !== 'skill-input' || !Array.isArray(rawValue) || rawValue.length === 0) {
630+
return null
631+
}
632+
633+
const skillNames = rawValue
634+
.map((skill: any) => {
635+
if (!skill || typeof skill !== 'object') return null
636+
637+
// Priority 1: Resolve skill name from the skills query (fresh data)
638+
if (skill.skillId) {
639+
const foundSkill = workspaceSkills.find((s) => s.id === skill.skillId)
640+
if (foundSkill?.name) return foundSkill.name
641+
}
642+
643+
// Priority 2: Fall back to stored name (for deleted skills)
644+
if (skill.name && typeof skill.name === 'string') return skill.name
645+
646+
// Priority 3: Use skillId as last resort
647+
if (skill.skillId) return skill.skillId
648+
649+
return null
650+
})
651+
.filter((name): name is string => !!name)
652+
653+
if (skillNames.length === 0) return null
654+
if (skillNames.length === 1) return skillNames[0]
655+
if (skillNames.length === 2) return `${skillNames[0]}, ${skillNames[1]}`
656+
return `${skillNames[0]}, ${skillNames[1]} +${skillNames.length - 2}`
657+
}, [subBlock?.type, rawValue, workspaceSkills])
658+
621659
const isPasswordField = subBlock?.password === true
622660
const maskedValue = isPasswordField && value && value !== '-' ? '•••' : null
623661

@@ -627,6 +665,7 @@ const SubBlockRow = memo(function SubBlockRow({
627665
dropdownLabel ||
628666
variablesDisplayValue ||
629667
toolsDisplayValue ||
668+
skillsDisplayValue ||
630669
knowledgeBaseDisplayName ||
631670
workflowSelectionName ||
632671
mcpServerDisplayName ||

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

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ interface SkillModalProps {
2727

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

30+
interface FieldErrors {
31+
name?: string
32+
description?: string
33+
content?: string
34+
general?: string
35+
}
36+
3037
export function SkillModal({
3138
open,
3239
onOpenChange,
@@ -43,7 +50,7 @@ export function SkillModal({
4350
const [name, setName] = useState('')
4451
const [description, setDescription] = useState('')
4552
const [content, setContent] = useState('')
46-
const [formError, setFormError] = useState('')
53+
const [errors, setErrors] = useState<FieldErrors>({})
4754
const [saving, setSaving] = useState(false)
4855

4956
useEffect(() => {
@@ -57,7 +64,7 @@ export function SkillModal({
5764
setDescription('')
5865
setContent('')
5966
}
60-
setFormError('')
67+
setErrors({})
6168
}
6269
}, [open, initialValues])
6370

@@ -71,24 +78,26 @@ export function SkillModal({
7178
}, [name, description, content, initialValues])
7279

7380
const handleSave = async () => {
81+
const newErrors: FieldErrors = {}
82+
7483
if (!name.trim()) {
75-
setFormError('Name is required')
76-
return
77-
}
78-
if (name.length > 64) {
79-
setFormError('Name must be 64 characters or less')
80-
return
81-
}
82-
if (!KEBAB_CASE_REGEX.test(name)) {
83-
setFormError('Name must be kebab-case (e.g. my-skill)')
84-
return
84+
newErrors.name = 'Name is required'
85+
} else if (name.length > 64) {
86+
newErrors.name = 'Name must be 64 characters or less'
87+
} else if (!KEBAB_CASE_REGEX.test(name)) {
88+
newErrors.name = 'Name must be kebab-case (e.g. my-skill)'
8589
}
90+
8691
if (!description.trim()) {
87-
setFormError('Description is required')
88-
return
92+
newErrors.description = 'Description is required'
8993
}
94+
9095
if (!content.trim()) {
91-
setFormError('Content is required')
96+
newErrors.content = 'Content is required'
97+
}
98+
99+
if (Object.keys(newErrors).length > 0) {
100+
setErrors(newErrors)
92101
return
93102
}
94103

@@ -113,7 +122,7 @@ export function SkillModal({
113122
error instanceof Error && error.message.includes('already exists')
114123
? error.message
115124
: 'Failed to save skill. Please try again.'
116-
setFormError(message)
125+
setErrors({ general: message })
117126
} finally {
118127
setSaving(false)
119128
}
@@ -135,12 +144,16 @@ export function SkillModal({
135144
value={name}
136145
onChange={(e) => {
137146
setName(e.target.value)
138-
if (formError) setFormError('')
147+
if (errors.name) setErrors((prev) => ({ ...prev, name: undefined }))
139148
}}
140149
/>
141-
<span className='text-[11px] text-[var(--text-muted)]'>
142-
Lowercase letters, numbers, and hyphens (e.g. my-skill)
143-
</span>
150+
{errors.name ? (
151+
<p className='text-[12px] text-[var(--text-error)]'>{errors.name}</p>
152+
) : (
153+
<span className='text-[11px] text-[var(--text-muted)]'>
154+
Lowercase letters, numbers, and hyphens (e.g. my-skill)
155+
</span>
156+
)}
144157
</div>
145158

146159
<div className='flex flex-col gap-[4px]'>
@@ -153,10 +166,13 @@ export function SkillModal({
153166
value={description}
154167
onChange={(e) => {
155168
setDescription(e.target.value)
156-
if (formError) setFormError('')
169+
if (errors.description) setErrors((prev) => ({ ...prev, description: undefined }))
157170
}}
158171
maxLength={1024}
159172
/>
173+
{errors.description && (
174+
<p className='text-[12px] text-[var(--text-error)]'>{errors.description}</p>
175+
)}
160176
</div>
161177

162178
<div className='flex flex-col gap-[4px]'>
@@ -169,13 +185,18 @@ export function SkillModal({
169185
value={content}
170186
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => {
171187
setContent(e.target.value)
172-
if (formError) setFormError('')
188+
if (errors.content) setErrors((prev) => ({ ...prev, content: undefined }))
173189
}}
174190
className='min-h-[200px] resize-y font-mono text-[13px]'
175191
/>
192+
{errors.content && (
193+
<p className='text-[12px] text-[var(--text-error)]'>{errors.content}</p>
194+
)}
176195
</div>
177196

178-
{formError && <span className='text-[11px] text-[var(--text-error)]'>{formError}</span>}
197+
{errors.general && (
198+
<p className='text-[12px] text-[var(--text-error)]'>{errors.general}</p>
199+
)}
179200
</div>
180201
</ModalBody>
181202
<ModalFooter className='items-center justify-between'>

0 commit comments

Comments
 (0)