Skip to content

Commit 1feeb8c

Browse files
committed
feat(integrations): suggest curated skills per integration with one-click add
Curate research-backed, capability-grounded skills for every catalog integration and surface them on the integration detail page. Each skill maps to operations the block actually supports and can be added to the workspace in one click; track adds in PostHog. - Add SuggestedSkill type + skills field on BlockMeta; populate skills for all 193 catalog integrations (3 audit passes for grounding/sourcing) - getSuggestedSkillsForBlock() with versioned-type (e.g. notion_v2) base fallback - Skills section on the integration detail page with add/added states - integration_skill_added PostHog event with workspace/integration metadata
1 parent 2c7b1ca commit 1feeb8c

198 files changed

Lines changed: 5104 additions & 2 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,19 @@ import {
1414
} from '@/lib/integrations'
1515
import { getServiceConfigByProviderId } from '@/lib/oauth'
1616
import { ConnectOAuthModal } from '@/app/workspace/[workspaceId]/components/connect-oauth-modal'
17+
import { IntegrationSkillsSection } from '@/app/workspace/[workspaceId]/integrations/[block]/integration-skills-section'
1718
import { ConnectServiceAccountModal } from '@/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal'
1819
import { IntegrationSection } from '@/app/workspace/[workspaceId]/integrations/components/integration-section'
1920
import { IntegrationTile } from '@/app/workspace/[workspaceId]/integrations/components/integrations-showcase'
2021
import {
2122
CONNECT_MODE,
2223
CONNECT_QUERY_PARAM,
2324
} from '@/app/workspace/[workspaceId]/integrations/connect-route'
24-
import { getTemplatesForBlock, type ScopedBlockTemplate } from '@/blocks/registry'
25+
import {
26+
getSuggestedSkillsForBlock,
27+
getTemplatesForBlock,
28+
type ScopedBlockTemplate,
29+
} from '@/blocks/registry'
2530
import { useWorkspaceCredentials } from '@/hooks/queries/credentials'
2631
import { useOAuthReturnRouter } from '@/hooks/use-oauth-return'
2732

@@ -46,6 +51,7 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration
4651
const searchParams = useSearchParams()
4752
const Icon = blockTypeToIconMap[integration.type]
4853
const matchingTemplates = getTemplatesForBlock(integration.type)
54+
const suggestedSkills = getSuggestedSkillsForBlock(integration.type)
4955
const oauthService = resolveOAuthServiceForIntegration(integration)
5056
const [oauthOpen, setOAuthOpen] = useState(false)
5157

@@ -210,6 +216,14 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration
210216
</IntegrationSection>
211217
)}
212218

219+
{suggestedSkills.length > 0 && (
220+
<IntegrationSkillsSection
221+
skills={suggestedSkills}
222+
workspaceId={workspaceId}
223+
integrationType={integration.type}
224+
/>
225+
)}
226+
213227
{matchingTemplates.length > 0 && (
214228
<TemplatesSection
215229
integration={integration}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
'use client'
2+
3+
import { useMemo, useState } from 'react'
4+
import { Check, Plus } from 'lucide-react'
5+
import { usePostHog } from 'posthog-js/react'
6+
import { Chip } from '@/components/emcn'
7+
import { AgentSkillsIcon } from '@/components/icons'
8+
import { captureEvent } from '@/lib/posthog/client'
9+
import type { SuggestedSkill } from '@/blocks/types'
10+
import { useCreateSkill, useSkills } from '@/hooks/queries/skills'
11+
12+
interface IntegrationSkillsSectionProps {
13+
skills: readonly SuggestedSkill[]
14+
workspaceId: string
15+
integrationType: string
16+
}
17+
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+
28+
interface SkillRowProps {
29+
skill: SuggestedSkill
30+
added: boolean
31+
pending: boolean
32+
onAdd: () => void
33+
}
34+
35+
function SkillRow({ skill, added, pending, onAdd }: SkillRowProps) {
36+
return (
37+
<div className='flex items-center gap-2.5 rounded-lg p-2'>
38+
<SkillTile />
39+
<div className='flex min-w-0 flex-1 flex-col'>
40+
<span className='truncate text-[14px] text-[var(--text-body)]'>{skill.name}</span>
41+
<span className='truncate text-[12px] text-[var(--text-muted)]'>{skill.description}</span>
42+
</div>
43+
{added ? (
44+
<Chip variant='filled' leftIcon={Check} disabled flush>
45+
Added
46+
</Chip>
47+
) : (
48+
<Chip variant='primary' leftIcon={Plus} onClick={onAdd} disabled={pending} flush>
49+
{pending ? 'Adding...' : 'Add'}
50+
</Chip>
51+
)}
52+
</div>
53+
)
54+
}
55+
56+
/**
57+
* Curated, research-backed skills for an integration. Each row adds the skill
58+
* 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.
61+
*/
62+
export function IntegrationSkillsSection({
63+
skills,
64+
workspaceId,
65+
integrationType,
66+
}: IntegrationSkillsSectionProps) {
67+
const posthog = usePostHog()
68+
const { data: existingSkills = [] } = useSkills(workspaceId)
69+
const createSkill = useCreateSkill()
70+
const [pendingName, setPendingName] = useState<string | null>(null)
71+
72+
const existingNames = useMemo(() => new Set(existingSkills.map((s) => s.name)), [existingSkills])
73+
74+
const handleAdd = async (skill: SuggestedSkill, position: number) => {
75+
setPendingName(skill.name)
76+
try {
77+
await createSkill.mutateAsync({ workspaceId, skill })
78+
captureEvent(posthog, 'integration_skill_added', {
79+
workspace_id: workspaceId,
80+
integration_type: integrationType,
81+
skill_name: skill.name,
82+
position,
83+
skill_count: skills.length,
84+
})
85+
} finally {
86+
setPendingName(null)
87+
}
88+
}
89+
90+
return (
91+
<section className='flex flex-col'>
92+
<span className='pl-0.5 text-[var(--text-muted)] text-small'>Skills</span>
93+
<div className='mt-[9px] mb-3 h-px bg-[var(--border)]' />
94+
<div className='-mx-2 flex flex-col gap-y-0.5'>
95+
{skills.map((skill, index) => (
96+
<SkillRow
97+
key={skill.name}
98+
skill={skill}
99+
added={existingNames.has(skill.name)}
100+
pending={pendingName === skill.name}
101+
onAdd={() => handleAdd(skill, index)}
102+
/>
103+
))}
104+
</div>
105+
</section>
106+
)
107+
}

apps/sim/blocks/blocks/agentmail.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,4 +690,27 @@ export const AgentMailBlockMeta = {
690690
alsoIntegrations: ['slack'],
691691
},
692692
],
693+
skills: [
694+
{
695+
name: 'triage-inbox-messages',
696+
description:
697+
'Read new messages in an AgentMail inbox, classify them, and reply or escalate as needed.',
698+
content:
699+
'# Triage Inbox Messages\n\nProcess unread email in an AgentMail inbox and act on each thread.\n\n## Steps\n1. List recent messages in the inbox and identify which threads are unread or unanswered.\n2. Read each thread for context including prior replies.\n3. Classify intent (question, request, spam, follow-up needed) and urgency.\n4. Draft and send a reply on the thread for routine items, or escalate by flagging the ones needing a human.\n\n## Output\nA summary of threads handled: who, the classification, and the action taken (replied, escalated, ignored).',
700+
},
701+
{
702+
name: 'extract-verification-code',
703+
description:
704+
'Read a verification or OTP email in an AgentMail inbox and extract the code or confirmation link.',
705+
content:
706+
'# Extract Verification Code\n\nPull a 2FA/OTP code or confirmation link from an email so an agent can complete a signup or login flow.\n\n## Steps\n1. Search the inbox for the most recent message from the expected sender or matching the subject.\n2. Read the message body and extract the verification code or the confirmation URL.\n3. Return only the code or link.\n\n## Output\nThe extracted code or link. If multiple recent matches exist, return the newest and note its timestamp.',
707+
},
708+
{
709+
name: 'send-and-track-outreach',
710+
description:
711+
'Send an email from an AgentMail inbox and monitor the thread for a reply to continue the conversation.',
712+
content:
713+
'# Send and Track Outreach\n\nSend an outbound email and follow the resulting thread.\n\n## Steps\n1. Compose the message with a clear subject and body from the provided details.\n2. Send it from the AgentMail inbox to the recipient.\n3. Check the thread for a reply; when one arrives, read it and determine the next action.\n\n## Output\nConfirm the message was sent with the thread ID. When a reply arrives, summarize it and recommend the next step.',
714+
},
715+
],
693716
} as const satisfies BlockMeta

apps/sim/blocks/blocks/agentphone.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -822,4 +822,27 @@ export const AgentPhoneBlockMeta = {
822822
alsoIntegrations: ['zendesk'],
823823
},
824824
],
825+
skills: [
826+
{
827+
name: 'send-sms-notification',
828+
description:
829+
'Send an SMS or iMessage from an AgentPhone number to notify or remind a recipient.',
830+
content:
831+
'# Send SMS Notification\n\nSend a text message to a person from an AgentPhone number.\n\n## Steps\n1. Determine the sending number and the recipient phone number.\n2. Write a clear, concise message (reminder, alert, confirmation, or update).\n3. Send the SMS or iMessage.\n\n## Output\nConfirm the message was sent with the recipient number and a short preview of the text. Note any send failure.',
832+
},
833+
{
834+
name: 'place-outbound-call',
835+
description:
836+
'Place a voice call from an AgentPhone number to deliver a message or run a short scripted interaction.',
837+
content:
838+
'# Place Outbound Call\n\nMake a voice call from an AgentPhone number for reminders, confirmations, or notifications.\n\n## Steps\n1. Determine the AgentPhone number to call from and the destination number.\n2. Prepare the spoken message or script to deliver.\n3. Place the call and deliver the message.\n\n## Output\nConfirm the call was placed with the destination number and the message delivered. Report call status or transcript if available.',
839+
},
840+
{
841+
name: 'provision-and-respond',
842+
description:
843+
'Provision a phone number and handle inbound SMS by reading the message and sending an appropriate reply.',
844+
content:
845+
'# Provision and Respond\n\nSet up a phone number and respond to incoming texts.\n\n## Steps\n1. Provision a US or Canadian phone number if one is not already assigned.\n2. Read inbound SMS messages received on that number.\n3. For each message, determine intent and send a relevant reply.\n\n## Output\nReport the provisioned number, the inbound messages handled, and the replies sent.',
846+
},
847+
],
825848
} as const satisfies BlockMeta

apps/sim/blocks/blocks/agiloft.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,4 +504,27 @@ export const AgiloftBlockMeta = {
504504
alsoIntegrations: ['linear'],
505505
},
506506
],
507+
skills: [
508+
{
509+
name: 'flag-expiring-contracts',
510+
description:
511+
'Query Agiloft for contracts approaching their renewal or expiration date and report the ones at risk.',
512+
content:
513+
'# Flag Expiring Contracts\n\nFind contracts in Agiloft that are nearing expiration or auto-renewal so the team can act in time.\n\n## Steps\n1. Query the contract records for upcoming expiration or renewal dates within the target window.\n2. For each match, read key terms: counterparty, value, renewal type, and notice period.\n3. Identify contracts with auto-renewal clauses that need a decision before the notice deadline.\n\n## Output\nA list of at-risk contracts sorted by date, with counterparty, expiration date, renewal type, and recommended action.',
514+
},
515+
{
516+
name: 'create-contract-record',
517+
description:
518+
'Create a new contract or related record in Agiloft from provided deal or request details.',
519+
content:
520+
'# Create Contract Record\n\nAdd a new contract record to Agiloft from intake details.\n\n## Steps\n1. Map the provided details to the contract record fields (counterparty, type, value, start/end dates, owner).\n2. Set status to the correct initial stage in the lifecycle.\n3. Create the record and capture its ID.\n\n## Output\nConfirm the record was created with its ID and key fields. Note any required fields that were missing.',
521+
},
522+
{
523+
name: 'summarize-contract-terms',
524+
description:
525+
'Read a contract record in Agiloft and produce a plain-language summary of its key obligations and dates.',
526+
content:
527+
'# Summarize Contract Terms\n\nTurn an Agiloft contract record into a concise brief.\n\n## Steps\n1. Read the contract record and its key fields and attached terms.\n2. Identify obligations, payment terms, renewal/termination clauses, and critical dates.\n3. Note any unusual or high-risk terms.\n\n## Output\nA short brief: parties, term, value, key obligations, critical dates, and any risk flags. Keep it readable for non-lawyers.',
528+
},
529+
],
507530
} as const satisfies BlockMeta

apps/sim/blocks/blocks/ahrefs.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,4 +645,27 @@ export const AhrefsBlockMeta = {
645645
alsoIntegrations: ['slack'],
646646
},
647647
],
648+
skills: [
649+
{
650+
name: 'analyze-competitor-backlinks',
651+
description:
652+
"Pull a competitor domain's backlink profile from Ahrefs and surface link-building opportunities.",
653+
content:
654+
"# Analyze Competitor Backlinks\n\nUse Ahrefs to study a competitor's backlinks and find outreach targets.\n\n## Steps\n1. Run a backlink/referring-domains report for the competitor URL or domain.\n2. Identify high-authority referring domains and the anchor texts they use.\n3. Compare against your own domain to find sites linking to them but not you.\n\n## Output\nA prioritized list of link opportunities: referring domain, authority, linked page, and why it is worth pursuing.",
655+
},
656+
{
657+
name: 'keyword-research-report',
658+
description:
659+
'Research keywords in Ahrefs for a topic and report volume, difficulty, and ranking opportunities.',
660+
content:
661+
'# Keyword Research Report\n\nBuild a keyword opportunity report from Ahrefs data.\n\n## Steps\n1. Pull the organic keywords a competitor domain already ranks for to source candidate keywords for the topic.\n2. Run a keyword overview on the most relevant candidates to collect search volume and keyword difficulty.\n3. Highlight keywords with meaningful volume and lower difficulty as quick wins.\n\n## Output\nA table of keywords with volume and difficulty, grouped into quick wins vs long-term targets, with a short recommendation.',
662+
},
663+
{
664+
name: 'track-organic-rankings',
665+
description:
666+
"Pull a domain's organic keyword rankings from Ahrefs and report top movers and lost positions.",
667+
content:
668+
'# Track Organic Rankings\n\nReport how a domain is ranking in organic search using Ahrefs.\n\n## Steps\n1. Pull the organic keywords report for the target domain.\n2. Identify the top-ranking keywords and their positions.\n3. Compare against a prior snapshot if available to find gains and losses.\n\n## Output\nA summary of top organic keywords, notable position gains and drops, and pages that may need attention.',
669+
},
670+
],
648671
} as const satisfies BlockMeta

apps/sim/blocks/blocks/airtable.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,4 +432,27 @@ export const AirtableBlockMeta = {
432432
alsoIntegrations: ['gmail'],
433433
},
434434
],
435+
skills: [
436+
{
437+
name: 'sync-records-to-table',
438+
description:
439+
'Parse incoming emails, forms, or documents and create or update structured Airtable records.',
440+
content:
441+
'# Sync Records to Airtable\n\nTurn unstructured inbound data into clean Airtable records.\n\n## Steps\n1. Read the source content (email body, form payload, or document text).\n2. Extract the fields that map to the target table columns (name, email, company, amount, status, etc.).\n3. Search the table for an existing record matching a unique key (such as email or order ID).\n4. Update the existing record if found; otherwise create a new one.\n5. Set any derived fields (category, priority, owner) based on the content.\n\n## Output\nReport how many records were created vs updated and list the record IDs. Flag any rows skipped for missing required fields.',
442+
},
443+
{
444+
name: 'triage-and-route-records',
445+
description:
446+
'Classify new Airtable records (leads, tickets, requests) and assign owner, priority, and due dates.',
447+
content:
448+
'# Triage and Route Records\n\nAutomatically qualify and route new Airtable records.\n\n## Steps\n1. List recently created records in the target table.\n2. For each record, read the free-text fields (notes, message, transcript) and classify intent, urgency, and category.\n3. Set the owner, priority, and status fields based on the classification.\n4. Compute and set a due date for time-sensitive items.\n\n## Output\nSummarize the records triaged grouped by owner and priority. Note any records that need human review.',
449+
},
450+
{
451+
name: 'generate-status-report',
452+
description:
453+
'Query an Airtable table or view and produce a rolled-up status report of progress, blockers, and trends.',
454+
content:
455+
'# Generate Status Report\n\nBuild a concise report from an Airtable table or view.\n\n## Steps\n1. Read records from the specified table or filtered view.\n2. Group by the relevant dimension (project, status, owner, or stage).\n3. Count totals per group and identify overdue or stalled items.\n4. Highlight notable changes or anomalies in the data.\n\n## Output\nA short report: totals per group, items at risk, and 2-3 takeaways. Keep it scannable with bullet points.',
456+
},
457+
],
435458
} as const satisfies BlockMeta

apps/sim/blocks/blocks/airweave.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,20 @@ export const AirweaveBlockMeta = {
172172
tags: ['research', 'reporting'],
173173
},
174174
],
175+
skills: [
176+
{
177+
name: 'answer-from-collection',
178+
description:
179+
'Search an Airweave collection across synced sources and answer a question with grounded, cited results.',
180+
content:
181+
'# Answer From Collection\n\nUse Airweave to retrieve current context across connected apps and answer a question.\n\n## Steps\n1. Take the user question and search the relevant Airweave collection.\n2. Review the top results, noting which source each came from (docs, tickets, CRM, etc.).\n3. Synthesize an answer grounded only in the retrieved content.\n4. If the collection returns nothing relevant, say so instead of guessing.\n\n## Output\nA concise answer with citations back to the source records. Do not include claims unsupported by the results.',
182+
},
183+
{
184+
name: 'build-context-brief',
185+
description:
186+
'Search an Airweave collection for a person, account, or project and compile a context brief from all sources.',
187+
content:
188+
'# Build Context Brief\n\nGather everything Airweave knows about a subject across synced sources into one brief.\n\n## Steps\n1. Search the collection for the subject (account name, project, customer, or person).\n2. Pull relevant hits from each source type and group them.\n3. Summarize the current state, recent activity, and any open items.\n\n## Output\nA short brief organized by source, highlighting the most recent and relevant facts plus open questions.',
189+
},
190+
],
175191
} as const satisfies BlockMeta

0 commit comments

Comments
 (0)