Skip to content

Commit e3e2352

Browse files
committed
fix(integrations): harden suggested-skill add flow; document skill authoring
Address PR review feedback on the suggested-skills section: - Make useSkills the single source of truth for Added state by writing the created skill into the React Query cache onSuccess (fixes stale Added that survived a delete, and the lag that allowed a duplicate click) - Track in-flight adds in a Set so concurrent adds keep independent pending state and cannot be double-submitted - Surface failures with toast.error instead of swallowing the rejection - Extract the duplicated SkillTile into a shared workspace component Also document the new BlockMeta.skills field in the add-block and validate-integration skills (+ blocks AGENTS.md): skills must be grounded in the block's tools.access and sourced from real online use cases, never invented.
1 parent c85de13 commit e3e2352

12 files changed

Lines changed: 92 additions & 35 deletions

File tree

.agents/skills/add-block/SKILL.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,14 @@ export const {Service}BlockMeta = {
651651
alsoIntegrations: ['slack'], // Other blocks referenced in the prompt (optional)
652652
},
653653
],
654+
skills: [ // Optional but strongly encouraged
655+
{
656+
name: 'summarize-thread', // kebab-case, becomes the created skill's name
657+
description: 'One line: what it does and when to use it.',
658+
content:
659+
'# Summarize Thread\n\n...\n\n## Steps\n1. ...\n\n## Output\n...', // markdown
660+
},
661+
],
654662
} as const satisfies BlockMeta
655663
```
656664

@@ -665,6 +673,16 @@ export const {Service}BlockMeta = {
665673
- **`alsoIntegrations`** names other block types (e.g. `'slack'`, `'linear'`) referenced in the template prompt — helps the catalog surface this template when those blocks are selected
666674
- Place the export **after** the main `{Service}Block` export, at the very bottom of the file
667675

676+
#### `skills` — curated, ready-to-add agent skills
677+
678+
`skills` is an optional array of `SuggestedSkill` (`{ name, description, content }`) shown on the integration's detail page; users click **Add** to create the skill in their workspace. Aim for 3–5 skills for mainstream services, 2–3 for niche/low-level ones.
679+
680+
- **`name`** — kebab-case, lowercase letters/numbers/hyphens, ≤ 64 chars, unique within the integration, verb-led (e.g. `summarize-thread`).
681+
- **`description`** — one line, ≤ 1024 chars: what it does and when to use it.
682+
- **`content`** — markdown instructions for the agent (literal `\n` for newlines): a `# Title`, then `## Steps` and an output/guidance section. Keep ~600–2000 chars.
683+
- **Ground every skill in operations the block actually exposes.** Cross-check each skill's steps against the block's `tools.access` list — never describe an action the integration cannot perform (e.g. "receive messages" when the block only sends).
684+
- **Skills MUST be derived from real, popular use cases found online — never invented.** Before adding a skill, web-search the service's documented use cases (vendor use-case/solutions pages, official docs describing the workflow, reputable "top automations for X" articles). If you cannot source a use case as something people genuinely do with the service, do not add it. Do not hallucinate skills.
685+
668686
### Register in the blocksMeta object
669687

670688
After adding `{Service}BlockMeta` to the block file, register it in `apps/sim/blocks/registry.ts`:
@@ -888,6 +906,7 @@ All tool IDs referenced in `tools.access` and returned by `tools.config.tool` MU
888906
- [ ] Outputs match tool outputs
889907
- [ ] Block registered in `registry.ts` blocks object (alphabetically)
890908
- [ ] `{Service}BlockMeta` exported at bottom of block file with `tags` and `templates`
909+
- [ ] `skills` added to `{Service}BlockMeta`, each grounded in the block's `tools.access` and derived from a real online-sourced use case (not invented)
891910
- [ ] `BlockMeta` imported from `@/blocks/types` alongside `BlockConfig`
892911
- [ ] Block meta registered in `registry.ts` blocksMeta object (alphabetically)
893912
- [ ] If icon missing: asked user to provide SVG

.agents/skills/validate-integration/SKILL.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,12 @@ For **each tool** in `tools.access`:
207207
- [ ] `authMode` is set correctly (`AuthMode.OAuth` or `AuthMode.ApiKey`)
208208
- [ ] Block is registered in `blocks/registry.ts` alphabetically
209209

210+
### BlockMeta Skills (catalog)
211+
- [ ] `{Service}BlockMeta.skills` is present (3–5 for mainstream services, 2–3 for niche/low-level)
212+
- [ ] **Every skill is grounded** — its steps only use operations the block exposes in `tools.access`; flag any skill that implies an unsupported action (e.g. "receive messages" when the block only sends)
213+
- [ ] **Every skill is real, not hallucinated** — web-search the service and confirm each skill maps to a popular use case attested online (vendor use-case/solutions pages, official docs describing the workflow, reputable "top automations for X" articles). Rewrite or remove any skill you cannot source as something people genuinely do with the service.
214+
- [ ] Each skill has a kebab-case `name` (≤64 chars, unique), a one-line `description`, and markdown `content` with `# Title` + `## Steps` + an output/guidance section
215+
210216
### Block Inputs
211217
- [ ] `inputs` section lists all subBlock params that the block accepts
212218
- [ ] Input types match the subBlock types

.claude/commands/add-block.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -807,11 +807,25 @@ export const {Service}BlockMeta = {
807807
},
808808
// ... at least 6 more
809809
],
810+
skills: [ // SuggestedSkill[] — 3–5 mainstream, 2–3 niche
811+
{
812+
name: 'summarize-thread', // kebab-case, ≤64 chars, unique, verb-led
813+
description: 'One line: what it does and when to use it.', // ≤1024 chars
814+
content:
815+
'# Summarize Thread\n\n...\n\n## Steps\n1. ...\n\n## Output\n...', // markdown
816+
},
817+
// ... more
818+
],
810819
} as const satisfies BlockMeta
811820
```
812821

813822
Derive templates from the service's real use cases. Each prompt should name a concrete trigger, transformation, and output — not a generic description of what the service does.
814823

824+
`skills` are curated, ready-to-add agent skills shown on the integration's detail page (users click **Add** to create them in their workspace). Two hard rules:
825+
826+
- **Ground every skill in operations the block actually exposes** — cross-check each skill's steps against `tools.access`. Never describe an action the integration cannot perform.
827+
- **Derive skills from real, popular use cases found online — never invent them.** Web-search the service's documented use cases (vendor use-case/solutions pages, official docs describing the workflow, reputable "top automations for X" articles) and only add a skill you can source as something people genuinely do with the service. Do not hallucinate skills.
828+
815829
## Checklist Before Finishing
816830

817831
- [ ] `integrationType` is set to the correct `IntegrationType` enum value
@@ -831,6 +845,7 @@ Derive templates from the service's real use cases. Each prompt should name a co
831845
- [ ] Optional/rarely-used fields set to `mode: 'advanced'`
832846
- [ ] Timestamps and complex inputs have `wandConfig` enabled
833847
- [ ] Exported `{Service}BlockMeta` with at least 7 templates
848+
- [ ] `skills` added to `{Service}BlockMeta`, each grounded in `tools.access` and sourced from a real online use case (not invented)
834849

835850
## Final Validation (Required)
836851

.claude/commands/validate-integration.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,8 @@ For **each tool** in `tools.access`:
197197
- [ ] Has at least 7 templates, each with `icon`, `title`, `prompt`, `modules`, `category`, and `tags`
198198
- [ ] Prompts describe concrete use cases, not generic descriptions of what the service does
199199
- [ ] `alsoIntegrations` is set on any template whose prompt references another service
200+
- [ ] `skills` present (3–5 mainstream, 2–3 niche), each grounded in `tools.access` — flag any skill implying an unsupported action
201+
- [ ] **Each skill is real, not hallucinated** — web-search and confirm it maps to a popular use case attested online (vendor use-case pages, official docs describing the workflow, reputable "top automations" articles); rewrite/remove any you cannot source
200202

201203
### Block Inputs
202204
- [ ] `inputs` section lists all subBlock params that the block accepts

.cursor/commands/validate-integration.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ For **each tool** in `tools.access`:
186186
- [ ] `icon` references the correct icon component from `@/components/icons`
187187
- [ ] `authMode` is set correctly (`AuthMode.OAuth` or `AuthMode.ApiKey`)
188188
- [ ] Block is registered in `blocks/registry.ts` alphabetically
189+
- [ ] `{Service}BlockMeta.skills` present (3–5 mainstream, 2–3 niche), each grounded in `tools.access` and confirmed via web search to be a real, popular use case (not hallucinated) — rewrite/remove any you cannot source
189190

190191
### Block Inputs
191192
- [ ] `inputs` section lists all subBlock params that the block accepts

apps/sim/app/workspace/[workspaceId]/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ export type {
2727
SelectableConfig,
2828
} from './resource/resource'
2929
export { EMPTY_CELL_PLACEHOLDER, Resource, ResourceTable } from './resource/resource'
30+
export { SkillTile } from './skill-tile'
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { SkillTile } from './skill-tile'
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { AgentSkillsIcon } from '@/components/icons'
2+
3+
/**
4+
* Square tile bearing the agent-skills glyph. Shared chrome for any surface
5+
* that lists a skill (the Skills page and integration detail pages) so the two
6+
* do not drift.
7+
*/
8+
export function SkillTile() {
9+
return (
10+
<div className='size-9 flex-shrink-0'>
11+
<div className='flex size-full items-center justify-center rounded-xl border border-[var(--border-1)] bg-[var(--surface-4)] dark:bg-[var(--surface-5)]'>
12+
<AgentSkillsIcon className='size-5 text-[var(--text-icon)]' />
13+
</div>
14+
</div>
15+
)
16+
}

apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-skills-section.tsx

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
import { useMemo, useState } from 'react'
44
import { Check, Plus } from 'lucide-react'
55
import { usePostHog } from 'posthog-js/react'
6-
import { Chip } from '@/components/emcn'
7-
import { AgentSkillsIcon } from '@/components/icons'
6+
import { Chip, toast } from '@/components/emcn'
87
import { captureEvent } from '@/lib/posthog/client'
8+
import { SkillTile } from '@/app/workspace/[workspaceId]/components'
99
import type { SuggestedSkill } from '@/blocks/types'
1010
import { useCreateSkill, useSkills } from '@/hooks/queries/skills'
1111

@@ -15,16 +15,6 @@ interface IntegrationSkillsSectionProps {
1515
integrationType: string
1616
}
1717

18-
function SkillTile() {
19-
return (
20-
<div className='size-9 flex-shrink-0'>
21-
<div className='flex size-full items-center justify-center rounded-xl border border-[var(--border-1)] bg-[var(--surface-4)] dark:bg-[var(--surface-5)]'>
22-
<AgentSkillsIcon className='size-5 text-[var(--text-icon)]' />
23-
</div>
24-
</div>
25-
)
26-
}
27-
2818
interface SkillRowProps {
2919
skill: SuggestedSkill
3020
added: boolean
@@ -56,8 +46,8 @@ function SkillRow({ skill, added, pending, onAdd }: SkillRowProps) {
5646
/**
5747
* Curated, research-backed skills for an integration. Each row adds the skill
5848
* to the workspace via the same `useCreateSkill` mutation the Skills page uses;
59-
* a skill already present in the workspace (matched by name) renders as
60-
* "Added" instead of an add button.
49+
* `useSkills` is the single source of truth for the "Added" state, so a skill
50+
* removed elsewhere correctly reverts to "Add".
6151
*/
6252
export function IntegrationSkillsSection({
6353
skills,
@@ -67,27 +57,31 @@ export function IntegrationSkillsSection({
6757
const posthog = usePostHog()
6858
const { data: existingSkills = [] } = useSkills(workspaceId)
6959
const createSkill = useCreateSkill()
70-
const [pendingName, setPendingName] = useState<string | null>(null)
71-
const [optimisticAdded, setOptimisticAdded] = useState<ReadonlySet<string>>(new Set())
60+
const [pendingNames, setPendingNames] = useState<ReadonlySet<string>>(new Set())
7261

7362
const existingNames = useMemo(() => new Set(existingSkills.map((s) => s.name)), [existingSkills])
7463

7564
const handleAdd = async (skill: SuggestedSkill, position: number) => {
76-
setPendingName(skill.name)
65+
// Track each in-flight add independently so concurrent adds keep their own
66+
// "Adding..." state and cannot be double-submitted.
67+
setPendingNames((prev) => new Set(prev).add(skill.name))
7768
try {
7869
await createSkill.mutateAsync({ workspaceId, skill })
79-
// Mark added locally so the row flips to "Added" immediately — the list
80-
// refetch that backs `existingNames` lands after this mutation resolves.
81-
setOptimisticAdded((prev) => new Set(prev).add(skill.name))
8270
captureEvent(posthog, 'integration_skill_added', {
8371
workspace_id: workspaceId,
8472
integration_type: integrationType,
8573
skill_name: skill.name,
8674
position,
8775
skill_count: skills.length,
8876
})
77+
} catch {
78+
toast.error(`Failed to add "${skill.name}" — please try again`)
8979
} finally {
90-
setPendingName(null)
80+
setPendingNames((prev) => {
81+
const next = new Set(prev)
82+
next.delete(skill.name)
83+
return next
84+
})
9185
}
9286
}
9387

@@ -100,8 +94,8 @@ export function IntegrationSkillsSection({
10094
<SkillRow
10195
key={skill.name}
10296
skill={skill}
103-
added={existingNames.has(skill.name) || optimisticAdded.has(skill.name)}
104-
pending={pendingName === skill.name}
97+
added={existingNames.has(skill.name)}
98+
pending={pendingNames.has(skill.name)}
10599
onAdd={() => handleAdd(skill, index)}
106100
/>
107101
))}

apps/sim/app/workspace/[workspaceId]/skills/skills.tsx

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
ChipModalHeader,
1515
Search,
1616
} from '@/components/emcn'
17-
import { AgentSkillsIcon } from '@/components/icons'
17+
import { SkillTile } from '@/app/workspace/[workspaceId]/components'
1818
import { IntegrationTabsHeader } from '@/app/workspace/[workspaceId]/integrations/components/integration-tabs-header'
1919
import { ShowcaseWithExplore } from '@/app/workspace/[workspaceId]/integrations/components/showcase-with-explore'
2020
import { SkillModal } from '@/app/workspace/[workspaceId]/skills/components/skill-modal'
@@ -29,16 +29,6 @@ const logger = createLogger('SkillsSettings')
2929

3030
const SKILLS_LABEL = 'Skills'
3131

32-
function SkillTile() {
33-
return (
34-
<div className='size-9 flex-shrink-0'>
35-
<div className='flex size-full items-center justify-center rounded-xl border border-[var(--border-1)] bg-[var(--surface-4)] dark:bg-[var(--surface-5)]'>
36-
<AgentSkillsIcon className='size-5 text-[var(--text-icon)]' />
37-
</div>
38-
</div>
39-
)
40-
}
41-
4232
interface SkillItemProps {
4333
name: string
4434
description: string

0 commit comments

Comments
 (0)