diff --git a/apps/docs/content/docs/en/meta.json b/apps/docs/content/docs/en/meta.json index b934947a32..2f38998574 100644 --- a/apps/docs/content/docs/en/meta.json +++ b/apps/docs/content/docs/en/meta.json @@ -10,6 +10,7 @@ "connections", "mcp", "copilot", + "skills", "knowledgebase", "variables", "execution", diff --git a/apps/docs/content/docs/en/skills/index.mdx b/apps/docs/content/docs/en/skills/index.mdx new file mode 100644 index 0000000000..1af685ceb5 --- /dev/null +++ b/apps/docs/content/docs/en/skills/index.mdx @@ -0,0 +1,83 @@ +--- +title: Agent Skills +--- + +import { Callout } from 'fumadocs-ui/components/callout' + +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. + +## How Skills Work + +Skills use **progressive disclosure** to keep agent context lean: + +1. **Discovery** — Only skill names and descriptions are included in the agent's system prompt (~50-100 tokens each) +2. **Activation** — When the agent decides a skill is relevant, it calls the `load_skill` tool to load the full instructions into context +3. **Execution** — The agent follows the loaded instructions to complete the task + +This means you can attach many skills to an agent without bloating its context window. The agent only loads what it needs. + +## Creating Skills + +Go to **Settings** (gear icon) and select **Skills** under the Tools section. + +Click **Add** to create a new skill with three fields: + +| Field | Description | +|-------|-------------| +| **Name** | A kebab-case identifier (e.g. `sql-expert`, `code-reviewer`). Max 64 characters. | +| **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. | +| **Content** | The full skill instructions in markdown. This is loaded when the agent activates the skill. | + + + 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. + + +### Writing Good Skill Content + +Skill content follows the same conventions as [SKILL.md files](https://agentskills.io/specification): + +```markdown +# SQL Expert + +## When to use this skill +Use when the user asks you to write, optimize, or debug SQL queries. + +## Instructions +1. Always ask which database engine (PostgreSQL, MySQL, SQLite) +2. Use CTEs over subqueries for readability +3. Add index recommendations when relevant +4. Explain query plans for optimization requests + +## Common Patterns +... +``` + +## Adding Skills to an Agent + +Open any **Agent** block and find the **Skills** dropdown below the tools section. Select the skills you want the agent to have access to. + +Selected skills appear as chips that you can click to edit or remove. + +### What Happens at Runtime + +When the workflow runs: + +1. The agent's system prompt includes an `` section listing each skill's name and description +2. A `load_skill` tool is automatically added to the agent's available tools +3. When the agent determines a skill is relevant to the current task, it calls `load_skill` with the skill name +4. The full skill content is returned as a tool response, giving the agent detailed instructions + +This works across all supported LLM providers — the `load_skill` tool uses standard tool-calling, so no provider-specific configuration is needed. + +## Tips + +- **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" +- **One skill per domain** — A focused `sql-expert` skill works better than a broad `database-everything` skill +- **Use markdown structure** — Headers, lists, and code blocks help the agent parse and follow instructions +- **Test iteratively** — Run your workflow and check if the agent activates the skill when expected + +## Learn More + +- [Agent Skills specification](https://agentskills.io) — The open format for portable agent skills +- [Example skills](https://github.com/anthropics/skills) — Browse community skill examples +- [Best practices](https://agentskills.io/what-are-skills) — Writing effective skills diff --git a/apps/docs/content/docs/en/tools/linear.mdx b/apps/docs/content/docs/en/tools/linear.mdx index b64d9a9153..7d472afce0 100644 --- a/apps/docs/content/docs/en/tools/linear.mdx +++ b/apps/docs/content/docs/en/tools/linear.mdx @@ -320,6 +320,7 @@ Search for issues in Linear using full-text search | `teamId` | string | No | Filter by team ID | | `includeArchived` | boolean | No | Include archived issues in search results | | `first` | number | No | Number of results to return \(default: 50\) | +| `after` | string | No | Cursor for pagination | #### Output @@ -754,6 +755,10 @@ List all labels in Linear workspace or team | ↳ `name` | string | Label name | | ↳ `color` | string | Label color \(hex\) | | ↳ `description` | string | Label description | +| ↳ `isGroup` | boolean | Whether this label is a group | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | | ↳ `team` | object | Team object | | ↳ `id` | string | Team ID | | ↳ `name` | string | Team name | @@ -780,6 +785,10 @@ Create a new label in Linear | ↳ `name` | string | Label name | | ↳ `color` | string | Label color \(hex\) | | ↳ `description` | string | Label description | +| ↳ `isGroup` | boolean | Whether this label is a group | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | | ↳ `team` | object | Team object | | ↳ `id` | string | Team ID | | ↳ `name` | string | Team name | @@ -806,6 +815,10 @@ Update an existing label in Linear | ↳ `name` | string | Label name | | ↳ `color` | string | Label color \(hex\) | | ↳ `description` | string | Label description | +| ↳ `isGroup` | boolean | Whether this label is a group | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | | ↳ `team` | object | Team object | | ↳ `id` | string | Team ID | | ↳ `name` | string | Team name | @@ -849,9 +862,13 @@ List all workflow states (statuses) in Linear | `states` | array | Array of workflow states | | ↳ `id` | string | State ID | | ↳ `name` | string | State name \(e.g., "Todo", "In Progress"\) | -| ↳ `type` | string | State type \(unstarted, started, completed, canceled\) | +| ↳ `description` | string | State description | +| ↳ `type` | string | State type \(triage, backlog, unstarted, started, completed, canceled\) | | ↳ `color` | string | State color \(hex\) | | ↳ `position` | number | State position in workflow | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | | ↳ `team` | object | Team object | | ↳ `id` | string | Team ID | | ↳ `name` | string | Team name | @@ -877,11 +894,17 @@ Create a new workflow state (status) in Linear | --------- | ---- | ----------- | | `state` | object | The created workflow state | | ↳ `id` | string | State ID | -| ↳ `name` | string | State name | -| ↳ `type` | string | State type | -| ↳ `color` | string | State color | -| ↳ `position` | number | State position | -| ↳ `team` | object | Team this state belongs to | +| ↳ `name` | string | State name \(e.g., "Todo", "In Progress"\) | +| ↳ `description` | string | State description | +| ↳ `type` | string | State type \(triage, backlog, unstarted, started, completed, canceled\) | +| ↳ `color` | string | State color \(hex\) | +| ↳ `position` | number | State position in workflow | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | +| ↳ `team` | object | Team object | +| ↳ `id` | string | Team ID | +| ↳ `name` | string | Team name | ### `linear_update_workflow_state` @@ -903,10 +926,17 @@ Update an existing workflow state in Linear | --------- | ---- | ----------- | | `state` | object | The updated workflow state | | ↳ `id` | string | State ID | -| ↳ `name` | string | State name | -| ↳ `type` | string | State type | -| ↳ `color` | string | State color | -| ↳ `position` | number | State position | +| ↳ `name` | string | State name \(e.g., "Todo", "In Progress"\) | +| ↳ `description` | string | State description | +| ↳ `type` | string | State type \(triage, backlog, unstarted, started, completed, canceled\) | +| ↳ `color` | string | State color \(hex\) | +| ↳ `position` | number | State position in workflow | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | +| ↳ `team` | object | Team object | +| ↳ `id` | string | Team ID | +| ↳ `name` | string | Team name | ### `linear_list_cycles` @@ -935,6 +965,7 @@ List cycles (sprints/iterations) in Linear | ↳ `endsAt` | string | End date \(ISO 8601\) | | ↳ `completedAt` | string | Completion date \(ISO 8601\) | | ↳ `progress` | number | Progress percentage \(0-1\) | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | | ↳ `team` | object | Team object | | ↳ `id` | string | Team ID | | ↳ `name` | string | Team name | @@ -961,6 +992,7 @@ Get a single cycle by ID from Linear | ↳ `endsAt` | string | End date \(ISO 8601\) | | ↳ `completedAt` | string | Completion date \(ISO 8601\) | | ↳ `progress` | number | Progress percentage \(0-1\) | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | | ↳ `team` | object | Team object | | ↳ `id` | string | Team ID | | ↳ `name` | string | Team name | @@ -986,9 +1018,14 @@ Create a new cycle (sprint/iteration) in Linear | ↳ `id` | string | Cycle ID | | ↳ `number` | number | Cycle number | | ↳ `name` | string | Cycle name | -| ↳ `startsAt` | string | Start date | -| ↳ `endsAt` | string | End date | -| ↳ `team` | object | Team this cycle belongs to | +| ↳ `startsAt` | string | Start date \(ISO 8601\) | +| ↳ `endsAt` | string | End date \(ISO 8601\) | +| ↳ `completedAt` | string | Completion date \(ISO 8601\) | +| ↳ `progress` | number | Progress percentage \(0-1\) | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `team` | object | Team object | +| ↳ `id` | string | Team ID | +| ↳ `name` | string | Team name | ### `linear_get_active_cycle` @@ -1008,10 +1045,14 @@ Get the currently active cycle for a team | ↳ `id` | string | Cycle ID | | ↳ `number` | number | Cycle number | | ↳ `name` | string | Cycle name | -| ↳ `startsAt` | string | Start date | -| ↳ `endsAt` | string | End date | -| ↳ `progress` | number | Progress percentage | -| ↳ `team` | object | Team this cycle belongs to | +| ↳ `startsAt` | string | Start date \(ISO 8601\) | +| ↳ `endsAt` | string | End date \(ISO 8601\) | +| ↳ `completedAt` | string | Completion date \(ISO 8601\) | +| ↳ `progress` | number | Progress percentage \(0-1\) | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `team` | object | Team object | +| ↳ `id` | string | Team ID | +| ↳ `name` | string | Team name | ### `linear_create_attachment` @@ -1334,8 +1375,12 @@ Create a new customer in Linear | ↳ `domains` | array | Associated domains | | ↳ `externalIds` | array | External IDs from other systems | | ↳ `logoUrl` | string | Logo URL | +| ↳ `slugId` | string | Unique URL slug | | ↳ `approximateNeedCount` | number | Number of customer needs | +| ↳ `revenue` | number | Annual revenue | +| ↳ `size` | number | Organization size | | ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) | | ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | ### `linear_list_customers` @@ -1363,8 +1408,12 @@ List all customers in Linear | ↳ `domains` | array | Associated domains | | ↳ `externalIds` | array | External IDs from other systems | | ↳ `logoUrl` | string | Logo URL | +| ↳ `slugId` | string | Unique URL slug | | ↳ `approximateNeedCount` | number | Number of customer needs | +| ↳ `revenue` | number | Annual revenue | +| ↳ `size` | number | Organization size | | ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) | | ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | ### `linear_create_customer_request` @@ -1480,8 +1529,12 @@ Get a single customer by ID in Linear | ↳ `domains` | array | Associated domains | | ↳ `externalIds` | array | External IDs from other systems | | ↳ `logoUrl` | string | Logo URL | +| ↳ `slugId` | string | Unique URL slug | | ↳ `approximateNeedCount` | number | Number of customer needs | +| ↳ `revenue` | number | Annual revenue | +| ↳ `size` | number | Organization size | | ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) | | ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | ### `linear_update_customer` @@ -1513,8 +1566,12 @@ Update a customer in Linear | ↳ `domains` | array | Associated domains | | ↳ `externalIds` | array | External IDs from other systems | | ↳ `logoUrl` | string | Logo URL | +| ↳ `slugId` | string | Unique URL slug | | ↳ `approximateNeedCount` | number | Number of customer needs | +| ↳ `revenue` | number | Annual revenue | +| ↳ `size` | number | Organization size | | ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) | | ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | ### `linear_delete_customer` @@ -1560,8 +1617,8 @@ Create a new customer status in Linear | --------- | ---- | -------- | ----------- | | `name` | string | Yes | Customer status name | | `color` | string | Yes | Status color \(hex code\) | -| `displayName` | string | No | Display name for the status | | `description` | string | No | Status description | +| `displayName` | string | No | Display name for the status | | `position` | number | No | Position in status list | #### Output @@ -1571,11 +1628,12 @@ Create a new customer status in Linear | `customerStatus` | object | The created customer status | | ↳ `id` | string | Customer status ID | | ↳ `name` | string | Status name | -| ↳ `displayName` | string | Display name | | ↳ `description` | string | Status description | | ↳ `color` | string | Status color \(hex\) | | ↳ `position` | number | Position in list | +| ↳ `type` | string | Status type \(active, inactive\) | | ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last updated timestamp \(ISO 8601\) | | ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | ### `linear_update_customer_status` @@ -1589,8 +1647,8 @@ Update a customer status in Linear | `statusId` | string | Yes | Customer status ID to update | | `name` | string | No | Updated status name | | `color` | string | No | Updated status color | -| `displayName` | string | No | Updated display name | | `description` | string | No | Updated description | +| `displayName` | string | No | Updated display name | | `position` | number | No | Updated position | #### Output @@ -1598,6 +1656,15 @@ Update a customer status in Linear | Parameter | Type | Description | | --------- | ---- | ----------- | | `customerStatus` | object | The updated customer status | +| ↳ `id` | string | Customer status ID | +| ↳ `name` | string | Status name | +| ↳ `description` | string | Status description | +| ↳ `color` | string | Status color \(hex\) | +| ↳ `position` | number | Position in list | +| ↳ `type` | string | Status type \(active, inactive\) | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last updated timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | ### `linear_delete_customer_status` @@ -1623,19 +1690,25 @@ List all customer statuses in Linear | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `first` | number | No | Number of statuses to return \(default: 50\) | +| `after` | string | No | Cursor for pagination | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | +| `pageInfo` | object | Pagination information | +| ↳ `hasNextPage` | boolean | Whether there are more results | +| ↳ `endCursor` | string | Cursor for the next page | | `customerStatuses` | array | List of customer statuses | | ↳ `id` | string | Customer status ID | | ↳ `name` | string | Status name | -| ↳ `displayName` | string | Display name | | ↳ `description` | string | Status description | | ↳ `color` | string | Status color \(hex\) | | ↳ `position` | number | Position in list | +| ↳ `type` | string | Status type \(active, inactive\) | | ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last updated timestamp \(ISO 8601\) | | ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | ### `linear_create_customer_tier` @@ -1711,11 +1784,16 @@ List all customer tiers in Linear | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `first` | number | No | Number of tiers to return \(default: 50\) | +| `after` | string | No | Cursor for pagination | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | +| `pageInfo` | object | Pagination information | +| ↳ `hasNextPage` | boolean | Whether there are more results | +| ↳ `endCursor` | string | Cursor for the next page | | `customerTiers` | array | List of customer tiers | | ↳ `id` | string | Customer tier ID | | ↳ `name` | string | Tier name | @@ -1761,6 +1839,14 @@ Create a new project label in Linear | Parameter | Type | Description | | --------- | ---- | ----------- | | `projectLabel` | object | The created project label | +| ↳ `id` | string | Project label ID | +| ↳ `name` | string | Label name | +| ↳ `description` | string | Label description | +| ↳ `color` | string | Label color \(hex\) | +| ↳ `isGroup` | boolean | Whether this label is a group | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | ### `linear_update_project_label` @@ -1780,6 +1866,14 @@ Update a project label in Linear | Parameter | Type | Description | | --------- | ---- | ----------- | | `projectLabel` | object | The updated project label | +| ↳ `id` | string | Project label ID | +| ↳ `name` | string | Label name | +| ↳ `description` | string | Label description | +| ↳ `color` | string | Label color \(hex\) | +| ↳ `isGroup` | boolean | Whether this label is a group | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | ### `linear_delete_project_label` @@ -1806,12 +1900,25 @@ List all project labels in Linear | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `projectId` | string | No | Optional project ID to filter labels for a specific project | +| `first` | number | No | Number of labels to return \(default: 50\) | +| `after` | string | No | Cursor for pagination | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | +| `pageInfo` | object | Pagination information | +| ↳ `hasNextPage` | boolean | Whether there are more results | +| ↳ `endCursor` | string | Cursor for the next page | | `projectLabels` | array | List of project labels | +| ↳ `id` | string | Project label ID | +| ↳ `name` | string | Label name | +| ↳ `description` | string | Label description | +| ↳ `color` | string | Label color \(hex\) | +| ↳ `isGroup` | boolean | Whether this label is a group | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | ### `linear_add_label_to_project` @@ -1867,6 +1974,16 @@ Create a new project milestone in Linear | Parameter | Type | Description | | --------- | ---- | ----------- | | `projectMilestone` | object | The created project milestone | +| ↳ `id` | string | Project milestone ID | +| ↳ `name` | string | Milestone name | +| ↳ `description` | string | Milestone description | +| ↳ `projectId` | string | Project ID | +| ↳ `targetDate` | string | Target date \(YYYY-MM-DD\) | +| ↳ `progress` | number | Progress percentage \(0-1\) | +| ↳ `sortOrder` | number | Sort order within the project | +| ↳ `status` | string | Milestone status \(done, next, overdue, unstarted\) | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | ### `linear_update_project_milestone` @@ -1886,6 +2003,16 @@ Update a project milestone in Linear | Parameter | Type | Description | | --------- | ---- | ----------- | | `projectMilestone` | object | The updated project milestone | +| ↳ `id` | string | Project milestone ID | +| ↳ `name` | string | Milestone name | +| ↳ `description` | string | Milestone description | +| ↳ `projectId` | string | Project ID | +| ↳ `targetDate` | string | Target date \(YYYY-MM-DD\) | +| ↳ `progress` | number | Progress percentage \(0-1\) | +| ↳ `sortOrder` | number | Sort order within the project | +| ↳ `status` | string | Milestone status \(done, next, overdue, unstarted\) | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | ### `linear_delete_project_milestone` @@ -1912,12 +2039,27 @@ List all milestones for a project in Linear | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `projectId` | string | Yes | Project ID to list milestones for | +| `first` | number | No | Number of milestones to return \(default: 50\) | +| `after` | string | No | Cursor for pagination | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | +| `pageInfo` | object | Pagination information | +| ↳ `hasNextPage` | boolean | Whether there are more results | +| ↳ `endCursor` | string | Cursor for the next page | | `projectMilestones` | array | List of project milestones | +| ↳ `id` | string | Project milestone ID | +| ↳ `name` | string | Milestone name | +| ↳ `description` | string | Milestone description | +| ↳ `projectId` | string | Project ID | +| ↳ `targetDate` | string | Target date \(YYYY-MM-DD\) | +| ↳ `progress` | number | Progress percentage \(0-1\) | +| ↳ `sortOrder` | number | Sort order within the project | +| ↳ `status` | string | Milestone status \(done, next, overdue, unstarted\) | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | ### `linear_create_project_status` @@ -1939,6 +2081,16 @@ Create a new project status in Linear | Parameter | Type | Description | | --------- | ---- | ----------- | | `projectStatus` | object | The created project status | +| ↳ `id` | string | Project status ID | +| ↳ `name` | string | Status name | +| ↳ `description` | string | Status description | +| ↳ `color` | string | Status color \(hex\) | +| ↳ `indefinite` | boolean | Whether this status is indefinite | +| ↳ `position` | number | Position in list | +| ↳ `type` | string | Status type \(backlog, planned, started, paused, completed, canceled\) | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last updated timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | ### `linear_update_project_status` @@ -1960,6 +2112,16 @@ Update a project status in Linear | Parameter | Type | Description | | --------- | ---- | ----------- | | `projectStatus` | object | The updated project status | +| ↳ `id` | string | Project status ID | +| ↳ `name` | string | Status name | +| ↳ `description` | string | Status description | +| ↳ `color` | string | Status color \(hex\) | +| ↳ `indefinite` | boolean | Whether this status is indefinite | +| ↳ `position` | number | Position in list | +| ↳ `type` | string | Status type \(backlog, planned, started, paused, completed, canceled\) | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last updated timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | ### `linear_delete_project_status` @@ -1985,11 +2147,26 @@ List all project statuses in Linear | Parameter | Type | Required | Description | | --------- | ---- | -------- | ----------- | +| `first` | number | No | Number of statuses to return \(default: 50\) | +| `after` | string | No | Cursor for pagination | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | +| `pageInfo` | object | Pagination information | +| ↳ `hasNextPage` | boolean | Whether there are more results | +| ↳ `endCursor` | string | Cursor for the next page | | `projectStatuses` | array | List of project statuses | +| ↳ `id` | string | Project status ID | +| ↳ `name` | string | Status name | +| ↳ `description` | string | Status description | +| ↳ `color` | string | Status color \(hex\) | +| ↳ `indefinite` | boolean | Whether this status is indefinite | +| ↳ `position` | number | Position in list | +| ↳ `type` | string | Status type \(backlog, planned, started, paused, completed, canceled\) | +| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) | +| ↳ `updatedAt` | string | Last updated timestamp \(ISO 8601\) | +| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) | diff --git a/apps/sim/app/api/permission-groups/[id]/route.ts b/apps/sim/app/api/permission-groups/[id]/route.ts index 977cb1bbfe..bdad32bdb9 100644 --- a/apps/sim/app/api/permission-groups/[id]/route.ts +++ b/apps/sim/app/api/permission-groups/[id]/route.ts @@ -24,6 +24,7 @@ const configSchema = z.object({ hideFilesTab: z.boolean().optional(), disableMcpTools: z.boolean().optional(), disableCustomTools: z.boolean().optional(), + disableSkills: z.boolean().optional(), hideTemplates: z.boolean().optional(), disableInvitations: z.boolean().optional(), hideDeployApi: z.boolean().optional(), diff --git a/apps/sim/app/api/permission-groups/route.ts b/apps/sim/app/api/permission-groups/route.ts index a72726c5a9..003c3131bf 100644 --- a/apps/sim/app/api/permission-groups/route.ts +++ b/apps/sim/app/api/permission-groups/route.ts @@ -25,6 +25,7 @@ const configSchema = z.object({ hideFilesTab: z.boolean().optional(), disableMcpTools: z.boolean().optional(), disableCustomTools: z.boolean().optional(), + disableSkills: z.boolean().optional(), hideTemplates: z.boolean().optional(), disableInvitations: z.boolean().optional(), hideDeployApi: z.boolean().optional(), diff --git a/apps/sim/app/api/skills/route.ts b/apps/sim/app/api/skills/route.ts new file mode 100644 index 0000000000..cf0b76c84d --- /dev/null +++ b/apps/sim/app/api/skills/route.ts @@ -0,0 +1,182 @@ +import { db } from '@sim/db' +import { skill } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, desc, eq } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid' +import { generateRequestId } from '@/lib/core/utils/request' +import { upsertSkills } from '@/lib/workflows/skills/operations' +import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' + +const logger = createLogger('SkillsAPI') + +const SkillSchema = z.object({ + skills: z.array( + z.object({ + id: z.string().optional(), + name: z + .string() + .min(1, 'Skill name is required') + .max(64) + .regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, 'Name must be kebab-case (e.g. my-skill)'), + description: z.string().min(1, 'Description is required').max(1024), + content: z.string().min(1, 'Content is required').max(50000, 'Content is too large'), + }) + ), + workspaceId: z.string().optional(), +}) + +/** GET - Fetch all skills for a workspace */ +export async function GET(request: NextRequest) { + const requestId = generateRequestId() + const searchParams = request.nextUrl.searchParams + const workspaceId = searchParams.get('workspaceId') + + try { + const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success || !authResult.userId) { + logger.warn(`[${requestId}] Unauthorized skills access attempt`) + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const userId = authResult.userId + + if (!workspaceId) { + logger.warn(`[${requestId}] Missing workspaceId`) + return NextResponse.json({ error: 'workspaceId is required' }, { status: 400 }) + } + + const userPermission = await getUserEntityPermissions(userId, 'workspace', workspaceId) + if (!userPermission) { + logger.warn(`[${requestId}] User ${userId} does not have access to workspace ${workspaceId}`) + return NextResponse.json({ error: 'Access denied' }, { status: 403 }) + } + + const result = await db + .select() + .from(skill) + .where(eq(skill.workspaceId, workspaceId)) + .orderBy(desc(skill.createdAt)) + + return NextResponse.json({ data: result }, { status: 200 }) + } catch (error) { + logger.error(`[${requestId}] Error fetching skills:`, error) + return NextResponse.json({ error: 'Failed to fetch skills' }, { status: 500 }) + } +} + +/** POST - Create or update skills */ +export async function POST(req: NextRequest) { + const requestId = generateRequestId() + + try { + const authResult = await checkSessionOrInternalAuth(req, { requireWorkflowId: false }) + if (!authResult.success || !authResult.userId) { + logger.warn(`[${requestId}] Unauthorized skills update attempt`) + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const userId = authResult.userId + const body = await req.json() + + try { + const { skills, workspaceId } = SkillSchema.parse(body) + + if (!workspaceId) { + logger.warn(`[${requestId}] Missing workspaceId in request body`) + return NextResponse.json({ error: 'workspaceId is required' }, { status: 400 }) + } + + const userPermission = await getUserEntityPermissions(userId, 'workspace', workspaceId) + if (!userPermission || (userPermission !== 'admin' && userPermission !== 'write')) { + logger.warn( + `[${requestId}] User ${userId} does not have write permission for workspace ${workspaceId}` + ) + return NextResponse.json({ error: 'Write permission required' }, { status: 403 }) + } + + const resultSkills = await upsertSkills({ + skills, + workspaceId, + userId, + requestId, + }) + + return NextResponse.json({ success: true, data: resultSkills }) + } catch (validationError) { + if (validationError instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid skills data`, { + errors: validationError.errors, + }) + return NextResponse.json( + { error: 'Invalid request data', details: validationError.errors }, + { status: 400 } + ) + } + if (validationError instanceof Error && validationError.message.includes('already exists')) { + return NextResponse.json({ error: validationError.message }, { status: 409 }) + } + throw validationError + } + } catch (error) { + logger.error(`[${requestId}] Error updating skills`, error) + return NextResponse.json({ error: 'Failed to update skills' }, { status: 500 }) + } +} + +/** DELETE - Delete a skill by ID */ +export async function DELETE(request: NextRequest) { + const requestId = generateRequestId() + const searchParams = request.nextUrl.searchParams + const skillId = searchParams.get('id') + const workspaceId = searchParams.get('workspaceId') + + try { + const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false }) + if (!authResult.success || !authResult.userId) { + logger.warn(`[${requestId}] Unauthorized skill deletion attempt`) + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const userId = authResult.userId + + if (!skillId) { + logger.warn(`[${requestId}] Missing skill ID for deletion`) + return NextResponse.json({ error: 'Skill ID is required' }, { status: 400 }) + } + + if (!workspaceId) { + logger.warn(`[${requestId}] Missing workspaceId for deletion`) + return NextResponse.json({ error: 'workspaceId is required' }, { status: 400 }) + } + + const userPermission = await getUserEntityPermissions(userId, 'workspace', workspaceId) + if (!userPermission || (userPermission !== 'admin' && userPermission !== 'write')) { + logger.warn( + `[${requestId}] User ${userId} does not have write permission for workspace ${workspaceId}` + ) + return NextResponse.json({ error: 'Write permission required' }, { status: 403 }) + } + + const existingSkill = await db.select().from(skill).where(eq(skill.id, skillId)).limit(1) + + if (existingSkill.length === 0) { + logger.warn(`[${requestId}] Skill not found: ${skillId}`) + return NextResponse.json({ error: 'Skill not found' }, { status: 404 }) + } + + if (existingSkill[0].workspaceId !== workspaceId) { + logger.warn(`[${requestId}] Skill ${skillId} does not belong to workspace ${workspaceId}`) + return NextResponse.json({ error: 'Skill not found' }, { status: 404 }) + } + + await db.delete(skill).where(and(eq(skill.id, skillId), eq(skill.workspaceId, workspaceId))) + + logger.info(`[${requestId}] Deleted skill: ${skillId}`) + return NextResponse.json({ success: true }) + } catch (error) { + logger.error(`[${requestId}] Error deleting skill:`, error) + return NextResponse.json({ error: 'Failed to delete skill' }, { status: 500 }) + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/index.ts index 0cfc45369b..a66b4ac04d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/index.ts @@ -24,6 +24,7 @@ export { ResponseFormat } from './response/response-format' export { ScheduleInfo } from './schedule-info/schedule-info' export { SheetSelectorInput } from './sheet-selector/sheet-selector-input' export { ShortInput } from './short-input/short-input' +export { SkillInput } from './skill-input/skill-input' export { SlackSelectorInput } from './slack-selector/slack-selector-input' export { SliderInput } from './slider-input/slider-input' export { InputFormat } from './starter/input-format' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/skill-input/skill-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/skill-input/skill-input.tsx new file mode 100644 index 0000000000..b61a964145 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/skill-input/skill-input.tsx @@ -0,0 +1,181 @@ +'use client' + +import { useCallback, useMemo, useState } from 'react' +import { Plus, XIcon } from 'lucide-react' +import { useParams } from 'next/navigation' +import { Combobox, type ComboboxOptionGroup } from '@/components/emcn' +import { AgentSkillsIcon } from '@/components/icons' +import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value' +import { SkillModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/skills/components/skill-modal' +import type { SkillDefinition } from '@/hooks/queries/skills' +import { useSkills } from '@/hooks/queries/skills' +import { usePermissionConfig } from '@/hooks/use-permission-config' + +interface StoredSkill { + skillId: string + name?: string +} + +interface SkillInputProps { + blockId: string + subBlockId: string + isPreview?: boolean + previewValue?: unknown + disabled?: boolean +} + +export function SkillInput({ + blockId, + subBlockId, + isPreview, + previewValue, + disabled, +}: SkillInputProps) { + const params = useParams() + const workspaceId = params.workspaceId as string + + const { config: permissionConfig } = usePermissionConfig() + const { data: workspaceSkills = [] } = useSkills(workspaceId) + const [value, setValue] = useSubBlockValue(blockId, subBlockId) + const [showCreateModal, setShowCreateModal] = useState(false) + const [editingSkill, setEditingSkill] = useState(null) + const [open, setOpen] = useState(false) + + const selectedSkills: StoredSkill[] = useMemo(() => { + if (isPreview && previewValue) { + return Array.isArray(previewValue) ? previewValue : [] + } + return Array.isArray(value) ? value : [] + }, [isPreview, previewValue, value]) + + const selectedIds = useMemo(() => new Set(selectedSkills.map((s) => s.skillId)), [selectedSkills]) + + const skillsDisabled = permissionConfig.disableSkills + + const skillGroups = useMemo((): ComboboxOptionGroup[] => { + const groups: ComboboxOptionGroup[] = [] + + if (!skillsDisabled) { + groups.push({ + items: [ + { + label: 'Create Skill', + value: 'action-create-skill', + icon: Plus, + onSelect: () => { + setShowCreateModal(true) + setOpen(false) + }, + disabled: isPreview, + }, + ], + }) + } + + const availableSkills = workspaceSkills.filter((s) => !selectedIds.has(s.id)) + if (!skillsDisabled && availableSkills.length > 0) { + groups.push({ + section: 'Skills', + items: availableSkills.map((s) => { + return { + label: s.name, + value: `skill-${s.id}`, + icon: AgentSkillsIcon, + onSelect: () => { + const newSkills: StoredSkill[] = [...selectedSkills, { skillId: s.id, name: s.name }] + setValue(newSkills) + setOpen(false) + }, + } + }), + }) + } + + return groups + }, [workspaceSkills, selectedIds, selectedSkills, setValue, isPreview, skillsDisabled]) + + const handleRemove = useCallback( + (skillId: string) => { + const newSkills = selectedSkills.filter((s) => s.skillId !== skillId) + setValue(newSkills) + }, + [selectedSkills, setValue] + ) + + const handleSkillSaved = useCallback(() => { + setShowCreateModal(false) + setEditingSkill(null) + }, []) + + const resolveSkillName = useCallback( + (stored: StoredSkill): string => { + const found = workspaceSkills.find((s) => s.id === stored.skillId) + return found?.name ?? stored.name ?? stored.skillId + }, + [workspaceSkills] + ) + + return ( + <> +
+ + + {selectedSkills.length > 0 && ( +
+ {selectedSkills.map((stored) => { + const fullSkill = workspaceSkills.find((s) => s.id === stored.skillId) + return ( +
{ + if (fullSkill && !disabled && !isPreview) { + setEditingSkill(fullSkill) + } + }} + > + + {resolveSkillName(stored)} + {!disabled && !isPreview && ( + + )} +
+ ) + })} +
+ )} +
+ + { + if (!isOpen) { + setShowCreateModal(false) + setEditingSkill(null) + } + }} + onSave={handleSkillSaved} + initialValues={editingSkill ?? undefined} + /> + + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx index cd1e9168e8..800ed5f932 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx @@ -32,6 +32,7 @@ import { ScheduleInfo, SheetSelectorInput, ShortInput, + SkillInput, SlackSelectorInput, SliderInput, Switch, @@ -687,6 +688,17 @@ function SubBlockComponent({ /> ) + case 'skill-input': + return ( + + ) + case 'checkbox-list': return ( void + onSave: () => void + onDelete?: (skillId: string) => void + initialValues?: SkillDefinition +} + +const KEBAB_CASE_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/ + +export function SkillModal({ + open, + onOpenChange, + onSave, + onDelete, + initialValues, +}: SkillModalProps) { + const params = useParams() + const workspaceId = params.workspaceId as string + + const createSkill = useCreateSkill() + const updateSkill = useUpdateSkill() + + const [name, setName] = useState('') + const [description, setDescription] = useState('') + const [content, setContent] = useState('') + const [formError, setFormError] = useState('') + const [saving, setSaving] = useState(false) + + useEffect(() => { + if (open) { + if (initialValues) { + setName(initialValues.name) + setDescription(initialValues.description) + setContent(initialValues.content) + } else { + setName('') + setDescription('') + setContent('') + } + setFormError('') + } + }, [open, initialValues]) + + const hasChanges = useMemo(() => { + if (!initialValues) return true + return ( + name !== initialValues.name || + description !== initialValues.description || + content !== initialValues.content + ) + }, [name, description, content, initialValues]) + + const handleSave = async () => { + if (!name.trim()) { + setFormError('Name is required') + return + } + if (name.length > 64) { + setFormError('Name must be 64 characters or less') + return + } + if (!KEBAB_CASE_REGEX.test(name)) { + setFormError('Name must be kebab-case (e.g. my-skill)') + return + } + if (!description.trim()) { + setFormError('Description is required') + return + } + if (!content.trim()) { + setFormError('Content is required') + return + } + + setSaving(true) + + try { + if (initialValues) { + await updateSkill.mutateAsync({ + workspaceId, + skillId: initialValues.id, + updates: { name, description, content }, + }) + } else { + await createSkill.mutateAsync({ + workspaceId, + skill: { name, description, content }, + }) + } + onSave() + } catch (error) { + const message = + error instanceof Error && error.message.includes('already exists') + ? error.message + : 'Failed to save skill. Please try again.' + setFormError(message) + } finally { + setSaving(false) + } + } + + return ( + + + {initialValues ? 'Edit Skill' : 'Create Skill'} + +
+
+ + { + setName(e.target.value) + if (formError) setFormError('') + }} + /> + + Lowercase letters, numbers, and hyphens (e.g. my-skill) + +
+ +
+ + { + setDescription(e.target.value) + if (formError) setFormError('') + }} + maxLength={1024} + /> +
+ +
+ +