Skip to content

Commit 71bd535

Browse files
feat(skills): added skills to agent block (#3149)
* feat(skills): added skills to agent block * improvement(skills): audit fixes, docs, icon, and UX polish * fix(skills): consolidate redundant permission checks in POST and DELETE * more friendly error for duplicate skills in the same workspace * fix(executor): loop sentinel-end wrongly queued (#3148) * fix(executor): loop sentinel-end wrongly queued * fix nested subflow error highlighting * fix(linear): align tool outputs, queries, and pagination with API (#3150) * fix(linear): align tool outputs, queries, and pagination with API * fix(linear): coerce first param to number, remove duplicate conditions, add null guard * fix(resolver): response format and evaluator metrics in deactivated branch (#3152) * fix(resolver): response format in deactivated branch * add evaluator metrics too * add child workflow id to the workflow block outputs * cleanup typing * feat(slack): add file attachment support to slack webhook trigger (#3151) * feat(slack): add file attachment support to slack webhook trigger * additional file handling * lint * ack comment * fix(skills): hide skill selection when disabled, remove dead code --------- Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
1 parent ed5ed97 commit 71bd535

File tree

29 files changed

+12177
-5
lines changed

29 files changed

+12177
-5
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: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import { db } from '@sim/db'
2+
import { skill } from '@sim/db/schema'
3+
import { createLogger } from '@sim/logger'
4+
import { and, desc, eq } from 'drizzle-orm'
5+
import { type NextRequest, NextResponse } from 'next/server'
6+
import { z } from 'zod'
7+
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
8+
import { generateRequestId } from '@/lib/core/utils/request'
9+
import { upsertSkills } from '@/lib/workflows/skills/operations'
10+
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
11+
12+
const logger = createLogger('SkillsAPI')
13+
14+
const SkillSchema = z.object({
15+
skills: z.array(
16+
z.object({
17+
id: z.string().optional(),
18+
name: z
19+
.string()
20+
.min(1, 'Skill name is required')
21+
.max(64)
22+
.regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, 'Name must be kebab-case (e.g. my-skill)'),
23+
description: z.string().min(1, 'Description is required').max(1024),
24+
content: z.string().min(1, 'Content is required').max(50000, 'Content is too large'),
25+
})
26+
),
27+
workspaceId: z.string().optional(),
28+
})
29+
30+
/** GET - Fetch all skills for a workspace */
31+
export async function GET(request: NextRequest) {
32+
const requestId = generateRequestId()
33+
const searchParams = request.nextUrl.searchParams
34+
const workspaceId = searchParams.get('workspaceId')
35+
36+
try {
37+
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
38+
if (!authResult.success || !authResult.userId) {
39+
logger.warn(`[${requestId}] Unauthorized skills access attempt`)
40+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
41+
}
42+
43+
const userId = authResult.userId
44+
45+
if (!workspaceId) {
46+
logger.warn(`[${requestId}] Missing workspaceId`)
47+
return NextResponse.json({ error: 'workspaceId is required' }, { status: 400 })
48+
}
49+
50+
const userPermission = await getUserEntityPermissions(userId, 'workspace', workspaceId)
51+
if (!userPermission) {
52+
logger.warn(`[${requestId}] User ${userId} does not have access to workspace ${workspaceId}`)
53+
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
54+
}
55+
56+
const result = await db
57+
.select()
58+
.from(skill)
59+
.where(eq(skill.workspaceId, workspaceId))
60+
.orderBy(desc(skill.createdAt))
61+
62+
return NextResponse.json({ data: result }, { status: 200 })
63+
} catch (error) {
64+
logger.error(`[${requestId}] Error fetching skills:`, error)
65+
return NextResponse.json({ error: 'Failed to fetch skills' }, { status: 500 })
66+
}
67+
}
68+
69+
/** POST - Create or update skills */
70+
export async function POST(req: NextRequest) {
71+
const requestId = generateRequestId()
72+
73+
try {
74+
const authResult = await checkSessionOrInternalAuth(req, { requireWorkflowId: false })
75+
if (!authResult.success || !authResult.userId) {
76+
logger.warn(`[${requestId}] Unauthorized skills update attempt`)
77+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
78+
}
79+
80+
const userId = authResult.userId
81+
const body = await req.json()
82+
83+
try {
84+
const { skills, workspaceId } = SkillSchema.parse(body)
85+
86+
if (!workspaceId) {
87+
logger.warn(`[${requestId}] Missing workspaceId in request body`)
88+
return NextResponse.json({ error: 'workspaceId is required' }, { status: 400 })
89+
}
90+
91+
const userPermission = await getUserEntityPermissions(userId, 'workspace', workspaceId)
92+
if (!userPermission || (userPermission !== 'admin' && userPermission !== 'write')) {
93+
logger.warn(
94+
`[${requestId}] User ${userId} does not have write permission for workspace ${workspaceId}`
95+
)
96+
return NextResponse.json({ error: 'Write permission required' }, { status: 403 })
97+
}
98+
99+
const resultSkills = await upsertSkills({
100+
skills,
101+
workspaceId,
102+
userId,
103+
requestId,
104+
})
105+
106+
return NextResponse.json({ success: true, data: resultSkills })
107+
} catch (validationError) {
108+
if (validationError instanceof z.ZodError) {
109+
logger.warn(`[${requestId}] Invalid skills data`, {
110+
errors: validationError.errors,
111+
})
112+
return NextResponse.json(
113+
{ error: 'Invalid request data', details: validationError.errors },
114+
{ status: 400 }
115+
)
116+
}
117+
if (validationError instanceof Error && validationError.message.includes('already exists')) {
118+
return NextResponse.json({ error: validationError.message }, { status: 409 })
119+
}
120+
throw validationError
121+
}
122+
} catch (error) {
123+
logger.error(`[${requestId}] Error updating skills`, error)
124+
return NextResponse.json({ error: 'Failed to update skills' }, { status: 500 })
125+
}
126+
}
127+
128+
/** DELETE - Delete a skill by ID */
129+
export async function DELETE(request: NextRequest) {
130+
const requestId = generateRequestId()
131+
const searchParams = request.nextUrl.searchParams
132+
const skillId = searchParams.get('id')
133+
const workspaceId = searchParams.get('workspaceId')
134+
135+
try {
136+
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
137+
if (!authResult.success || !authResult.userId) {
138+
logger.warn(`[${requestId}] Unauthorized skill deletion attempt`)
139+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
140+
}
141+
142+
const userId = authResult.userId
143+
144+
if (!skillId) {
145+
logger.warn(`[${requestId}] Missing skill ID for deletion`)
146+
return NextResponse.json({ error: 'Skill ID is required' }, { status: 400 })
147+
}
148+
149+
if (!workspaceId) {
150+
logger.warn(`[${requestId}] Missing workspaceId for deletion`)
151+
return NextResponse.json({ error: 'workspaceId is required' }, { status: 400 })
152+
}
153+
154+
const userPermission = await getUserEntityPermissions(userId, 'workspace', workspaceId)
155+
if (!userPermission || (userPermission !== 'admin' && userPermission !== 'write')) {
156+
logger.warn(
157+
`[${requestId}] User ${userId} does not have write permission for workspace ${workspaceId}`
158+
)
159+
return NextResponse.json({ error: 'Write permission required' }, { status: 403 })
160+
}
161+
162+
const existingSkill = await db.select().from(skill).where(eq(skill.id, skillId)).limit(1)
163+
164+
if (existingSkill.length === 0) {
165+
logger.warn(`[${requestId}] Skill not found: ${skillId}`)
166+
return NextResponse.json({ error: 'Skill not found' }, { status: 404 })
167+
}
168+
169+
if (existingSkill[0].workspaceId !== workspaceId) {
170+
logger.warn(`[${requestId}] Skill ${skillId} does not belong to workspace ${workspaceId}`)
171+
return NextResponse.json({ error: 'Skill not found' }, { status: 404 })
172+
}
173+
174+
await db.delete(skill).where(and(eq(skill.id, skillId), eq(skill.workspaceId, workspaceId)))
175+
176+
logger.info(`[${requestId}] Deleted skill: ${skillId}`)
177+
return NextResponse.json({ success: true })
178+
} catch (error) {
179+
logger.error(`[${requestId}] Error deleting skill:`, error)
180+
return NextResponse.json({ error: 'Failed to delete skill' }, { status: 500 })
181+
}
182+
}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export { ResponseFormat } from './response/response-format'
2424
export { ScheduleInfo } from './schedule-info/schedule-info'
2525
export { SheetSelectorInput } from './sheet-selector/sheet-selector-input'
2626
export { ShortInput } from './short-input/short-input'
27+
export { SkillInput } from './skill-input/skill-input'
2728
export { SlackSelectorInput } from './slack-selector/slack-selector-input'
2829
export { SliderInput } from './slider-input/slider-input'
2930
export { InputFormat } from './starter/input-format'

0 commit comments

Comments
 (0)