clawskills-mcp 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  An MCP (Model Context Protocol) server that exposes [ClawSkills](https://github.com/Shanksg/clawskills) API integration skill docs to AI agents.
4
4
 
5
- ClawSkills currently contains 14 structured Markdown skill docs for SaaS APIs such as Salesforce, HubSpot, GitHub, Jira, Stripe, Notion, Slack, and Linear. Each skill tracks its own API version and `Last validated` date, while `clawskills-mcp` has its own npm version for the server itself.
5
+ ClawSkills currently contains 14 structured Markdown skill docs plus workflow playbooks for cross-tool automations. Each skill tracks its own API version and `Last validated` date, while `clawskills-mcp` has its own npm version for the server itself.
6
6
 
7
7
  ---
8
8
 
@@ -46,6 +46,9 @@ docker run --rm -i clawskills-mcp
46
46
  | `list_skills` | List all available skill slugs with descriptions |
47
47
  | `get_skill` | Fetch a full skill doc or a specific section |
48
48
  | `search_skills` | Full-text search across all skill docs |
49
+ | `list_playbooks` | List all available cross-tool workflow playbooks |
50
+ | `get_playbook` | Fetch a full playbook by slug |
51
+ | `search_playbooks` | Full-text search across all workflow playbooks |
49
52
 
50
53
  ### `list_skills`
51
54
 
@@ -71,17 +74,36 @@ Examples:
71
74
 
72
75
  Returns matching excerpts (±3 lines of context) grouped by skill, capped at 5 matches per skill.
73
76
 
77
+ ### `list_playbooks`
78
+
79
+ No arguments. Returns all available playbooks.
80
+
81
+ ### `get_playbook`
82
+
83
+ | Argument | Type | Required | Description |
84
+ |----------|------|----------|-------------|
85
+ | `name` | string | yes | Playbook slug: `zendesk-jira-bug-escalation`, `hubspot-asana-onboarding`, etc. Fuzzy matched. |
86
+
87
+ ### `search_playbooks`
88
+
89
+ | Argument | Type | Required | Description |
90
+ |----------|------|----------|-------------|
91
+ | `query` | string | yes | Search string, e.g. `"idempotency"`, `"rollback"`, `"closed won"` |
92
+
93
+ Returns matching excerpts (±3 lines of context) grouped by playbook, capped at 5 matches per playbook.
94
+
74
95
  ---
75
96
 
76
97
  ## Configuration
77
98
 
78
- Skills are bundled inside the npm package — no `SKILLS_DIR` configuration needed for the standard `npx` setup.
99
+ Skills and playbooks are bundled inside the npm package — no extra configuration is needed for the standard `npx` setup.
79
100
 
80
- `SKILLS_DIR` is available if you want to point the server at a custom or local skills directory (e.g. your own forks of the docs):
101
+ `SKILLS_DIR` and `PLAYBOOKS_DIR` are available if you want to point the server at custom or local content directories:
81
102
 
82
103
  | Env var | Default | Description |
83
104
  |---------|---------|-------------|
84
105
  | `SKILLS_DIR` | bundled `skills/` inside the package | Absolute path to a directory of `{slug}/skill.md` files |
106
+ | `PLAYBOOKS_DIR` | bundled `playbooks/` inside the package | Absolute path to a directory of `{slug}.md` playbook files |
85
107
 
86
108
  ```json
87
109
  {
@@ -90,7 +112,8 @@ Skills are bundled inside the npm package — no `SKILLS_DIR` configuration need
90
112
  "command": "npx",
91
113
  "args": ["-y", "clawskills-mcp"],
92
114
  "env": {
93
- "SKILLS_DIR": "/path/to/your/skills"
115
+ "SKILLS_DIR": "/path/to/your/skills",
116
+ "PLAYBOOKS_DIR": "/path/to/your/playbooks"
94
117
  }
95
118
  }
96
119
  }
package/dist/index.js CHANGED
@@ -25,6 +25,19 @@ function resolveSkillsDir() {
25
25
  }
26
26
  return candidates[0];
27
27
  }
28
+ function resolvePlaybooksDir() {
29
+ if (process.env.PLAYBOOKS_DIR)
30
+ return process.env.PLAYBOOKS_DIR;
31
+ const candidates = [
32
+ path.resolve(__dirname, "../../playbooks"),
33
+ path.resolve(__dirname, "../playbooks"),
34
+ ];
35
+ for (const candidate of candidates) {
36
+ if (fs.existsSync(candidate))
37
+ return candidate;
38
+ }
39
+ return candidates[0];
40
+ }
28
41
  function resolvePackageVersion() {
29
42
  const packageJsonPath = path.resolve(__dirname, "../package.json");
30
43
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
@@ -49,6 +62,23 @@ function loadSkills(skillsDir) {
49
62
  process.stderr.write(`[clawskills-mcp] Loaded ${skills.size} skills: ${[...skills.keys()].join(", ")}\n`);
50
63
  return skills;
51
64
  }
65
+ function loadPlaybooks(playbooksDir) {
66
+ const playbooks = new Map();
67
+ if (!fs.existsSync(playbooksDir)) {
68
+ process.stderr.write(`[clawskills-mcp] Playbooks directory not found: ${playbooksDir}\n`);
69
+ return playbooks;
70
+ }
71
+ const entries = fs.readdirSync(playbooksDir, { withFileTypes: true });
72
+ for (const entry of entries) {
73
+ if (!entry.isFile() || !entry.name.endsWith(".md") || entry.name === "INDEX.md")
74
+ continue;
75
+ const slug = entry.name.replace(/\.md$/, "");
76
+ const content = fs.readFileSync(path.join(playbooksDir, entry.name), "utf-8");
77
+ playbooks.set(slug, content);
78
+ }
79
+ process.stderr.write(`[clawskills-mcp] Loaded ${playbooks.size} playbooks: ${[...playbooks.keys()].join(", ")}\n`);
80
+ return playbooks;
81
+ }
52
82
  // ---------------------------------------------------------------------------
53
83
  // Section extraction
54
84
  // ---------------------------------------------------------------------------
@@ -115,12 +145,15 @@ function findSkill(skills, name) {
115
145
  }
116
146
  return undefined;
117
147
  }
118
- function searchSkills(skills, query) {
148
+ function findPlaybook(playbooks, name) {
149
+ return findSkill(playbooks, name);
150
+ }
151
+ function searchDocuments(documents, query) {
119
152
  const results = [];
120
153
  const lowerQuery = query.toLowerCase();
121
154
  const CONTEXT_LINES = 3;
122
155
  const MAX_PER_SKILL = 5;
123
- for (const [slug, content] of skills.entries()) {
156
+ for (const [name, content] of documents.entries()) {
124
157
  const lines = content.split("\n");
125
158
  const excerpts = [];
126
159
  const usedRanges = [];
@@ -140,11 +173,17 @@ function searchSkills(skills, query) {
140
173
  break;
141
174
  }
142
175
  if (excerpts.length > 0) {
143
- results.push({ skill: slug, excerpts });
176
+ results.push({ name, excerpts });
144
177
  }
145
178
  }
146
179
  return results;
147
180
  }
181
+ function searchSkills(skills, query) {
182
+ return searchDocuments(skills, query);
183
+ }
184
+ function searchPlaybooks(playbooks, query) {
185
+ return searchDocuments(playbooks, query);
186
+ }
148
187
  // ---------------------------------------------------------------------------
149
188
  // Skill summary (first meaningful line)
150
189
  // ---------------------------------------------------------------------------
@@ -159,13 +198,15 @@ function skillSummary(content) {
159
198
  // ---------------------------------------------------------------------------
160
199
  // Exports for testing
161
200
  // ---------------------------------------------------------------------------
162
- export { resolveSkillsDir, resolvePackageVersion, loadSkills, extractSection, findSkill, searchSkills, skillSummary };
201
+ export { resolveSkillsDir, resolvePlaybooksDir, resolvePackageVersion, loadSkills, loadPlaybooks, extractSection, findSkill, findPlaybook, searchSkills, searchPlaybooks, skillSummary, };
163
202
  // ---------------------------------------------------------------------------
164
203
  // MCP Server setup
165
204
  // ---------------------------------------------------------------------------
166
205
  const SKILLS_DIR = resolveSkillsDir();
206
+ const PLAYBOOKS_DIR = resolvePlaybooksDir();
167
207
  const PACKAGE_VERSION = resolvePackageVersion();
168
208
  const skills = loadSkills(SKILLS_DIR);
209
+ const playbooks = loadPlaybooks(PLAYBOOKS_DIR);
169
210
  const server = new Server({ name: "clawskills-mcp", version: PACKAGE_VERSION }, { capabilities: { tools: {} } });
170
211
  // List tools
171
212
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
@@ -201,11 +242,40 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
201
242
  required: ["query"],
202
243
  },
203
244
  },
245
+ {
246
+ name: "list_playbooks",
247
+ description: "List all available ClawSkills workflow playbooks.",
248
+ inputSchema: { type: "object", properties: {}, required: [] },
249
+ },
250
+ {
251
+ name: "get_playbook",
252
+ description: "Retrieve a ClawSkills workflow playbook by name (slug).",
253
+ inputSchema: {
254
+ type: "object",
255
+ properties: {
256
+ name: { type: "string", description: "Playbook slug, e.g. 'zendesk-jira-bug-escalation'" },
257
+ },
258
+ required: ["name"],
259
+ },
260
+ },
261
+ {
262
+ name: "search_playbooks",
263
+ description: "Search across workflow playbooks for a query string and return matching excerpts with context.",
264
+ inputSchema: {
265
+ type: "object",
266
+ properties: {
267
+ query: { type: "string", description: "Search query, e.g. 'idempotency', 'rollback', 'customer impact'" },
268
+ },
269
+ required: ["query"],
270
+ },
271
+ },
204
272
  ],
205
273
  }));
206
274
  // Call tools
207
275
  const GetSkillSchema = z.object({ name: z.string(), section: z.string().optional() });
276
+ const GetPlaybookSchema = z.object({ name: z.string() });
208
277
  const SearchSkillsSchema = z.object({ query: z.string() });
278
+ const SearchPlaybooksSchema = z.object({ query: z.string() });
209
279
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
210
280
  const { name, arguments: args } = request.params;
211
281
  if (name === "list_skills") {
@@ -218,6 +288,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
218
288
  });
219
289
  return { content: [{ type: "text", text: `Available skills (${skills.size}):\n\n${lines.join("\n")}` }] };
220
290
  }
291
+ if (name === "list_playbooks") {
292
+ if (playbooks.size === 0) {
293
+ return { content: [{ type: "text", text: "No playbooks loaded. Check PLAYBOOKS_DIR." }] };
294
+ }
295
+ const lines = [...playbooks.entries()].map(([slug, content]) => `• **${slug}** — ${skillSummary(content)}`);
296
+ return { content: [{ type: "text", text: `Available playbooks (${playbooks.size}):\n\n${lines.join("\n")}` }] };
297
+ }
221
298
  if (name === "get_skill") {
222
299
  const parsed = GetSkillSchema.safeParse(args);
223
300
  if (!parsed.success) {
@@ -256,13 +333,46 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
256
333
  return { content: [{ type: "text", text: `No results found for "${query}".` }] };
257
334
  }
258
335
  const output = results
259
- .map(({ skill, excerpts }) => {
336
+ .map(({ name: skill, excerpts }) => {
260
337
  const blocks = excerpts.map((e, i) => `--- match ${i + 1} ---\n${e}`).join("\n\n");
261
338
  return `### ${skill}\n\n${blocks}`;
262
339
  })
263
340
  .join("\n\n---\n\n");
264
341
  return { content: [{ type: "text", text: `Search results for "${query}":\n\n${output}` }] };
265
342
  }
343
+ if (name === "get_playbook") {
344
+ const parsed = GetPlaybookSchema.safeParse(args);
345
+ if (!parsed.success) {
346
+ return { content: [{ type: "text", text: `Invalid arguments: ${parsed.error.message}` }], isError: true };
347
+ }
348
+ const slug = findPlaybook(playbooks, parsed.data.name);
349
+ if (!slug) {
350
+ const available = [...playbooks.keys()].join(", ");
351
+ return {
352
+ content: [{ type: "text", text: `Playbook "${parsed.data.name}" not found. Available playbooks: ${available}` }],
353
+ isError: true,
354
+ };
355
+ }
356
+ return { content: [{ type: "text", text: playbooks.get(slug) }] };
357
+ }
358
+ if (name === "search_playbooks") {
359
+ const parsed = SearchPlaybooksSchema.safeParse(args);
360
+ if (!parsed.success) {
361
+ return { content: [{ type: "text", text: `Invalid arguments: ${parsed.error.message}` }], isError: true };
362
+ }
363
+ const { query } = parsed.data;
364
+ const results = searchPlaybooks(playbooks, query);
365
+ if (results.length === 0) {
366
+ return { content: [{ type: "text", text: `No playbook results found for "${query}".` }] };
367
+ }
368
+ const output = results
369
+ .map(({ name: playbook, excerpts }) => {
370
+ const blocks = excerpts.map((e, i) => `--- match ${i + 1} ---\n${e}`).join("\n\n");
371
+ return `### ${playbook}\n\n${blocks}`;
372
+ })
373
+ .join("\n\n---\n\n");
374
+ return { content: [{ type: "text", text: `Playbook search results for "${query}":\n\n${output}` }] };
375
+ }
266
376
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
267
377
  });
268
378
  // ---------------------------------------------------------------------------
@@ -3,9 +3,10 @@ import * as fs from "fs";
3
3
  import * as os from "os";
4
4
  import * as path from "path";
5
5
  import { fileURLToPath } from "url";
6
- import { resolveSkillsDir, resolvePackageVersion, loadSkills, extractSection, findSkill, searchSkills, skillSummary } from "./index.js";
6
+ import { resolveSkillsDir, resolvePlaybooksDir, resolvePackageVersion, loadSkills, loadPlaybooks, extractSection, findSkill, findPlaybook, searchSkills, searchPlaybooks, skillSummary, } from "./index.js";
7
7
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
8
  const REAL_SKILLS_DIR = path.resolve(__dirname, "../../skills");
9
+ const REAL_PLAYBOOKS_DIR = path.resolve(__dirname, "../../playbooks");
9
10
  // ---------------------------------------------------------------------------
10
11
  // Helpers
11
12
  // ---------------------------------------------------------------------------
@@ -18,6 +19,13 @@ function makeTempSkillsDir(skills) {
18
19
  }
19
20
  return dir;
20
21
  }
22
+ function makeTempPlaybooksDir(playbooks) {
23
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "clawskills-playbooks-test-"));
24
+ for (const [slug, content] of Object.entries(playbooks)) {
25
+ fs.writeFileSync(path.join(dir, `${slug}.md`), content, "utf-8");
26
+ }
27
+ return dir;
28
+ }
21
29
  const SAMPLE_CONTENT = `# GitHub Skill
22
30
 
23
31
  A skill doc for GitHub API.
@@ -44,11 +52,17 @@ Scopes: repo, read:user, etc.
44
52
  POST /repos/{owner}/{repo}/issues
45
53
  `;
46
54
  let tempDir;
55
+ let tempPlaybooksDir;
47
56
  beforeAll(() => {
48
57
  tempDir = makeTempSkillsDir({ github: SAMPLE_CONTENT, salesforce: "# Salesforce\nCRM platform.\n## Overview\nSalesforce is a CRM." });
58
+ tempPlaybooksDir = makeTempPlaybooksDir({
59
+ "zendesk-jira": "# Zendesk -> Jira\n\nEscalate bugs from support into engineering.\n\n## Idempotency\n\nUse zendesk:{ticket_id}.",
60
+ "hubspot-asana": "# HubSpot -> Asana\n\nCreate onboarding work from closed won deals.",
61
+ });
49
62
  });
50
63
  afterAll(() => {
51
64
  fs.rmSync(tempDir, { recursive: true, force: true });
65
+ fs.rmSync(tempPlaybooksDir, { recursive: true, force: true });
52
66
  });
53
67
  // ---------------------------------------------------------------------------
54
68
  // loadSkills
@@ -86,6 +100,12 @@ describe("resolveSkillsDir", () => {
86
100
  expect(loadSkills(resolveSkillsDir()).has("stripe")).toBe(true);
87
101
  });
88
102
  });
103
+ describe("resolvePlaybooksDir", () => {
104
+ it("prefers the canonical repo-root playbooks directory", () => {
105
+ expect(path.basename(resolvePlaybooksDir())).toBe("playbooks");
106
+ expect(loadPlaybooks(resolvePlaybooksDir()).has("zendesk-jira-bug-escalation")).toBe(true);
107
+ });
108
+ });
89
109
  describe("resolvePackageVersion", () => {
90
110
  it("matches package.json", () => {
91
111
  const packageJsonPath = path.resolve(__dirname, "../package.json");
@@ -94,6 +114,20 @@ describe("resolvePackageVersion", () => {
94
114
  });
95
115
  });
96
116
  // ---------------------------------------------------------------------------
117
+ // loadPlaybooks
118
+ // ---------------------------------------------------------------------------
119
+ describe("loadPlaybooks", () => {
120
+ it("loads markdown files from a directory", () => {
121
+ const playbooks = loadPlaybooks(tempPlaybooksDir);
122
+ expect(playbooks.size).toBe(2);
123
+ expect(playbooks.has("zendesk-jira")).toBe(true);
124
+ });
125
+ it("returns empty map for missing directory", () => {
126
+ const playbooks = loadPlaybooks("/nonexistent/playbooks/path");
127
+ expect(playbooks.size).toBe(0);
128
+ });
129
+ });
130
+ // ---------------------------------------------------------------------------
97
131
  // findSkill
98
132
  // ---------------------------------------------------------------------------
99
133
  describe("findSkill", () => {
@@ -119,6 +153,18 @@ describe("findSkill", () => {
119
153
  expect(findSkill(skills, "zzznomatch")).toBeUndefined();
120
154
  });
121
155
  });
156
+ describe("findPlaybook", () => {
157
+ let playbooks;
158
+ beforeAll(() => {
159
+ playbooks = loadPlaybooks(tempPlaybooksDir);
160
+ });
161
+ it("returns key for exact match", () => {
162
+ expect(findPlaybook(playbooks, "zendesk-jira")).toBe("zendesk-jira");
163
+ });
164
+ it("matches normalized input", () => {
165
+ expect(findPlaybook(playbooks, "zendesk jira")).toBe("zendesk-jira");
166
+ });
167
+ });
122
168
  // ---------------------------------------------------------------------------
123
169
  // extractSection
124
170
  // ---------------------------------------------------------------------------
@@ -153,7 +199,7 @@ describe("searchSkills", () => {
153
199
  it("returns results for a matching query", () => {
154
200
  const results = searchSkills(skills, "Personal Access Token");
155
201
  expect(results.length).toBeGreaterThan(0);
156
- expect(results[0].skill).toBe("github");
202
+ expect(results[0].name).toBe("github");
157
203
  expect(results[0].excerpts.length).toBeGreaterThan(0);
158
204
  });
159
205
  it("is case-insensitive", () => {
@@ -172,6 +218,17 @@ describe("searchSkills", () => {
172
218
  expect(results[0].excerpts.length).toBeLessThanOrEqual(5);
173
219
  });
174
220
  });
221
+ describe("searchPlaybooks", () => {
222
+ let playbooks;
223
+ beforeAll(() => {
224
+ playbooks = loadPlaybooks(tempPlaybooksDir);
225
+ });
226
+ it("returns results for a matching query", () => {
227
+ const results = searchPlaybooks(playbooks, "idempotency");
228
+ expect(results.length).toBeGreaterThan(0);
229
+ expect(results[0].name).toBe("zendesk-jira");
230
+ });
231
+ });
175
232
  // ---------------------------------------------------------------------------
176
233
  // skillSummary
177
234
  // ---------------------------------------------------------------------------
@@ -234,3 +291,25 @@ describe("real skills", () => {
234
291
  }
235
292
  });
236
293
  });
294
+ describe("real playbooks", () => {
295
+ let playbooks;
296
+ beforeAll(() => {
297
+ playbooks = loadPlaybooks(REAL_PLAYBOOKS_DIR);
298
+ });
299
+ it("loads all expected playbooks", () => {
300
+ const knownPlaybooks = [
301
+ "hubspot-asana-onboarding",
302
+ "salesforce-hubspot-lead-sync",
303
+ "zendesk-jira-bug-escalation",
304
+ ];
305
+ for (const slug of knownPlaybooks) {
306
+ expect(playbooks.has(slug), `Missing playbook: ${slug}`).toBe(true);
307
+ }
308
+ });
309
+ it("every playbook has a non-empty summary line", () => {
310
+ for (const [slug, content] of playbooks.entries()) {
311
+ const summary = skillSummary(content);
312
+ expect(summary, `${slug}: empty summary`).not.toBe("");
313
+ }
314
+ });
315
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawskills-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "MCP server exposing ClawSkills API integration skill docs to AI agents",
5
5
  "license": "MIT",
6
6
  "author": "ClawSkills Contributors",
@@ -30,7 +30,8 @@
30
30
  },
31
31
  "files": [
32
32
  "dist/",
33
- "skills/"
33
+ "skills/",
34
+ "playbooks/"
34
35
  ],
35
36
  "scripts": {
36
37
  "sync:skills": "node scripts/sync-skills.mjs",
@@ -0,0 +1,32 @@
1
+ # Playbooks Index
2
+
3
+ Workflow playbooks are end-to-end guides for multi-system automations. They complement the per-tool `skill.md` files by defining the orchestration layer: trigger, source of truth, field mapping, idempotency, failure policy, and operational checks.
4
+
5
+ ## Available playbooks
6
+
7
+ | Playbook | Systems | Primary use case |
8
+ |----------|---------|------------------|
9
+ | [Zendesk Ticket -> Jira Bug escalation](./zendesk-jira-bug-escalation.md) | Zendesk, Jira | Escalate support-confirmed defects into engineering workflow |
10
+ | [HubSpot Deal Won -> Asana onboarding kickoff](./hubspot-asana-onboarding.md) | HubSpot, Asana | Start implementation or onboarding work when a deal closes |
11
+ | [Salesforce Lead -> HubSpot Contact sync](./salesforce-hubspot-lead-sync.md) | Salesforce, HubSpot | Mirror new/updated leads into marketing automation without duplicates |
12
+
13
+ ## Playbook template
14
+
15
+ Each playbook should define:
16
+ - Business trigger
17
+ - Systems involved
18
+ - Source of truth
19
+ - Sequence of operations
20
+ - Field mapping
21
+ - Idempotency and dedup keys
22
+ - Retry / partial-failure policy
23
+ - Reconciliation / rollback guidance
24
+ - Observability checks
25
+
26
+ ## Next candidates
27
+
28
+ - Slack incident -> Jira issue
29
+ - GitHub PR -> Slack notification
30
+ - Zendesk -> Salesforce case sync
31
+ - Stripe payment failed -> HubSpot task
32
+ - Notion request -> Asana task
@@ -0,0 +1,76 @@
1
+ # HubSpot Deal Won -> Asana Onboarding Kickoff
2
+
3
+ ## Goal
4
+
5
+ Create onboarding or implementation work in Asana as soon as a HubSpot Deal is marked closed won.
6
+
7
+ ## Systems involved
8
+
9
+ - HubSpot: sales pipeline, deal state, customer/account context
10
+ - Asana: delivery execution, onboarding tasks, project coordination
11
+
12
+ ## Trigger
13
+
14
+ - HubSpot webhook `deal.propertyChange` where `dealstage` moves into the closed-won stage
15
+
16
+ ## Source of truth
17
+
18
+ - Commercial state and customer/account metadata: HubSpot
19
+ - Delivery execution and task/project state: Asana
20
+ - Workflow linkage: integration database or HubSpot custom property storing the Asana `gid`
21
+
22
+ ## Sequence
23
+
24
+ 1. Receive the HubSpot webhook and verify it corresponds to the target pipeline/stage.
25
+ 2. Fetch the Deal, associated Company, and primary Contact.
26
+ 3. Check whether an Asana task/project already exists for the Deal ID.
27
+ 4. Create the onboarding project or kickoff task in Asana.
28
+ 5. Populate notes and custom fields with customer, owner, ARR, plan, and target launch details.
29
+ 6. Persist the Asana `gid` back to HubSpot or your integration store.
30
+ 7. Notify the implementation owner or CSM if required.
31
+
32
+ ## Field mapping
33
+
34
+ | HubSpot | Asana |
35
+ |---------|-------|
36
+ | `dealId` | external mapping / custom field |
37
+ | `dealname` | task/project name |
38
+ | associated company name | notes / title suffix |
39
+ | deal owner | assignee |
40
+ | close date | kickoff anchor or due date |
41
+ | ARR / amount | custom field |
42
+ | onboarding tier | custom field / template choice |
43
+
44
+ ## Idempotency
45
+
46
+ - Dedup key: HubSpot Deal ID
47
+ - Webhook delivery is at-least-once, so check your mapping store before creation
48
+ - If using project templates, ensure only one project is generated per Deal ID
49
+
50
+ ## Retry and partial failure policy
51
+
52
+ - Retry transient Asana failures with bounded backoff
53
+ - If Asana create succeeds but write-back to HubSpot fails, recover by searching the mapping store on the next run
54
+ - Validation errors should move the record into a manual review queue with the Deal ID and failing field
55
+
56
+ ## Reconciliation
57
+
58
+ - Daily job: compare all closed-won deals in the last 30 days against Asana kickoff artifacts
59
+ - Backfill missing Asana `gid` values into HubSpot
60
+ - Flag onboarding projects with no matching active deal
61
+
62
+ ## Rollback guidance
63
+
64
+ - If a Deal reopens or is marked closed-lost shortly after creation, archive or cancel the Asana project/task rather than deleting it
65
+ - Preserve the linkage so repeated stage churn does not create duplicates
66
+
67
+ ## Observability
68
+
69
+ - Count created onboarding projects/tasks per day
70
+ - Alert on webhook processing failures and duplicate prevention hits
71
+ - Track time from closed won to Asana kickoff creation
72
+
73
+ ## Related skills
74
+
75
+ - [`skills/hubspot/skill.md`](../skills/hubspot/skill.md)
76
+ - [`skills/asana/skill.md`](../skills/asana/skill.md)
@@ -0,0 +1,78 @@
1
+ # Salesforce Lead -> HubSpot Contact Sync
2
+
3
+ ## Goal
4
+
5
+ Mirror new or updated Salesforce Leads into HubSpot Contacts so marketing and lifecycle workflows stay current without creating duplicates.
6
+
7
+ ## Systems involved
8
+
9
+ - Salesforce: CRM of record for lead capture and sales qualification
10
+ - HubSpot: marketing automation, nurture, lifecycle segmentation
11
+
12
+ ## Trigger
13
+
14
+ - Scheduled incremental sync using Salesforce `LastModifiedDate`
15
+ - Optional near-real-time trigger from Salesforce CDC or platform events
16
+
17
+ ## Source of truth
18
+
19
+ - Lead identity and sales-owned fields: Salesforce
20
+ - Marketing engagement and automation state: HubSpot
21
+ - Identity dedup anchor: email, with explicit mapping store as a secondary safeguard
22
+
23
+ ## Sequence
24
+
25
+ 1. Query Salesforce Leads modified since the last sync watermark, with a small overlap window.
26
+ 2. Skip converted Leads and Leads with no email.
27
+ 3. Search HubSpot Contacts by email.
28
+ 4. If a Contact exists, update it with mapped fields.
29
+ 5. If no Contact exists, create one and persist the mapping.
30
+ 6. Record sync outcome per Lead so failed rows can be retried independently.
31
+ 7. Advance the watermark only after all rows are processed or quarantined.
32
+
33
+ ## Field mapping
34
+
35
+ | Salesforce Lead | HubSpot Contact |
36
+ |-----------------|-----------------|
37
+ | `Lead.Id` | custom property `salesforce_lead_id` or integration DB |
38
+ | `Email` | `email` |
39
+ | `FirstName` | `firstname` |
40
+ | `LastName` | `lastname` |
41
+ | `Company` | `company` |
42
+ | `Phone` | `phone` |
43
+ | `LeadSource` | custom source property |
44
+ | `Status` | derived `lifecyclestage` mapping |
45
+
46
+ ## Idempotency
47
+
48
+ - Primary dedup key: normalized email
49
+ - Secondary correlation key: Salesforce Lead ID
50
+ - Do not use HubSpot batch create blindly for unknown contacts; search first
51
+
52
+ ## Retry and partial failure policy
53
+
54
+ - Retry transient HubSpot rate-limit and availability errors per shard
55
+ - Quarantine `400` validation failures with Lead ID, email, and failing field names
56
+ - If one row fails, continue the rest of the batch and surface a remediation report
57
+
58
+ ## Reconciliation
59
+
60
+ - Daily compare: Salesforce Leads updated in the last 24 hours vs HubSpot Contacts carrying `salesforce_lead_id`
61
+ - Repair records where the ID mapping is missing but email matches
62
+ - Flag email collisions where multiple Salesforce Leads compete for one HubSpot Contact
63
+
64
+ ## Rollback guidance
65
+
66
+ - Avoid destructive deletes across systems
67
+ - If a bad sync overwrites HubSpot fields, restore from HubSpot history or your sync audit log and re-run the corrected mapping
68
+
69
+ ## Observability
70
+
71
+ - Track processed, created, updated, quarantined, and retried rows per run
72
+ - Alert on spikes in duplicate resolution or validation failures
73
+ - Monitor watermark lag so the sync does not silently fall behind
74
+
75
+ ## Related skills
76
+
77
+ - [`skills/salesforce/skill.md`](../skills/salesforce/skill.md)
78
+ - [`skills/hubspot/skill.md`](../skills/hubspot/skill.md)
@@ -0,0 +1,79 @@
1
+ # Zendesk Ticket -> Jira Bug Escalation
2
+
3
+ ## Goal
4
+
5
+ Escalate a Zendesk support ticket into Jira when support determines the issue is a product defect that engineering must track and resolve.
6
+
7
+ ## Systems involved
8
+
9
+ - Zendesk: intake, customer communication, support triage
10
+ - Jira: engineering backlog, bug lifecycle, fix tracking
11
+
12
+ ## Trigger
13
+
14
+ Any of:
15
+ - Zendesk ticket gets tag `engineering`
16
+ - Custom field `issue_type` changes to `bug`
17
+ - Ticket priority becomes `urgent` and product area is set
18
+
19
+ ## Source of truth
20
+
21
+ - Customer communication and ticket state: Zendesk
22
+ - Engineering execution state: Jira
23
+ - Escalation linkage: integration database or mirrored external ID field
24
+
25
+ ## Sequence
26
+
27
+ 1. Receive the Zendesk webhook or consume the ticket via Incremental Export.
28
+ 2. Build a deterministic correlation key: `zendesk:{ticket_id}`.
29
+ 3. Search Jira for an existing issue with that correlation key in a custom field or label.
30
+ 4. If no issue exists, create a Jira Bug with summary, reproduction notes, impact, and Zendesk URL.
31
+ 5. If an issue already exists, update priority, labels, assignee, and current reproduction context.
32
+ 6. Write the Jira issue key back to Zendesk as an internal note or custom field.
33
+ 7. Optionally mirror selected Jira status changes back to Zendesk after state mapping is finalized.
34
+
35
+ ## Field mapping
36
+
37
+ | Zendesk | Jira |
38
+ |---------|------|
39
+ | `ticket.id` | custom external ID / label |
40
+ | `subject` | `summary` |
41
+ | latest public comment | `description` |
42
+ | `priority` | `priority` |
43
+ | `tags` | `labels` |
44
+ | requester org / account | custom customer field or component |
45
+ | product area custom field | component / label |
46
+
47
+ ## Idempotency
48
+
49
+ - Dedup key: `zendesk:{ticket_id}`
50
+ - Before create, always search Jira by dedup key
51
+ - Zendesk webhooks are at-least-once delivery, so the create path must be safe to replay
52
+
53
+ ## Retry and partial failure policy
54
+
55
+ - Retry transient Jira failures (`429`, `503`) with bounded exponential backoff
56
+ - Do not retry `400` field validation errors blindly; quarantine and surface them
57
+ - If Jira create succeeds but Zendesk write-back fails, reconcile by searching Jira with the dedup key on the next run
58
+
59
+ ## Reconciliation
60
+
61
+ - Daily job: compare Zendesk tickets tagged `engineering` against Jira bugs with the dedup key
62
+ - Repair missing backlinks in Zendesk
63
+ - Flag Jira issues missing an active Zendesk ticket counterpart
64
+
65
+ ## Rollback guidance
66
+
67
+ - If a Jira issue was created in error, transition it to a closed/cancelled state instead of deleting it
68
+ - Remove the escalation tag or set a custom field in Zendesk to prevent automatic recreation
69
+
70
+ ## Observability
71
+
72
+ - Count escalations per day
73
+ - Alert on create failures, write-back failures, or rising duplicate-detection rates
74
+ - Track mean time from Zendesk escalation to Jira issue creation
75
+
76
+ ## Related skills
77
+
78
+ - [`skills/zendesk/skill.md`](../skills/zendesk/skill.md)
79
+ - [`skills/jira/skill.md`](../skills/jira/skill.md)
package/skills/INDEX.md CHANGED
@@ -3,6 +3,7 @@
3
3
  A curated library of integration skill documents for the most common SaaS platforms used in go-to-market and operations workflows. Each skill doc teaches an agent or automation builder how to reliably use that platform's API for high-value, production-grade workflows.
4
4
 
5
5
  → See [ROADMAP.md](./ROADMAP.md) for the prioritized build plan, phased milestones, and governance model.
6
+ → Cross-tool workflow guides now live in [`../playbooks/INDEX.md`](../playbooks/INDEX.md).
6
7
 
7
8
  ---
8
9
 
package/skills/ROADMAP.md CHANGED
@@ -12,14 +12,14 @@ This skill set enables end-to-end automation and integration across the most com
12
12
 
13
13
  ---
14
14
 
15
- ## Current state (as of 2026-03-10)
15
+ ## Current state (as of 2026-03-12)
16
16
 
17
17
  ### What's shipped
18
18
 
19
19
  | Layer | Status |
20
20
  |-------|--------|
21
21
  | **14 skill docs** — Monday.com, Salesforce, Jira, Dynamics 365, HubSpot, ServiceNow, Zendesk, Asana, GitHub, Figma, Slack, Stripe, Notion, Linear | ✅ Live |
22
- | **MCP server** — `clawskills-mcp` v0.3.6 on npm; `list_skills`, `get_skill`, `search_skills` | ✅ Live |
22
+ | **MCP server** — `clawskills-mcp` v0.4.0 on npm; `list_skills`, `get_skill`, `search_skills` | ✅ Live |
23
23
  | **CI pipeline** — build + test on every push/PR via `ci.yml` | ✅ Live |
24
24
  | **Release automation** — `release.yml` workflow_dispatch → bump, tag, npm publish via OIDC | ✅ Live |
25
25
  | **Test suite** — Vitest unit tests + real-skills validation (required sections, ≥5 KB, all 14 tools) | ✅ Live |
@@ -83,7 +83,7 @@ The repo now needs tighter alignment between the canonical `skills/` directory,
83
83
  | Channel | Status | Next action |
84
84
  |---------|--------|-------------|
85
85
  | GitHub (public) | Ready to flip | Make repo public |
86
- | npm (`clawskills-mcp`) | ✅ Live — v0.3.6 | Maintain via `release.yml` |
86
+ | npm (`clawskills-mcp`) | ✅ Live — v0.4.0 | Maintain via `release.yml` |
87
87
  | MCP registry listings | Not submitted | Submit to glama.ai, mcp.so, smithery.ai after public launch |
88
88
  | Docs website | Planned | Mintlify — fastest path to SEO-friendly browsable docs |
89
89
  | Lane A vs B | TBD | Revisit after first traction signals from public launch |
@@ -108,6 +108,43 @@ Each recipe lives as a `## Cross-tool recipe` appendix section in the relevant p
108
108
 
109
109
  ---
110
110
 
111
+ ### Priority 5 — Workflow playbooks and retrieval UX
112
+
113
+ The next product layer is workflow-first guidance, not only tool-by-tool references. This means shipping opinionated playbooks that show how multiple systems fit together operationally, then exposing them cleanly through MCP.
114
+
115
+ **Backlog:**
116
+ - Add a `playbooks/` directory with high-value cross-tool workflows
117
+ - Ship 3 flagship playbooks first:
118
+ - Zendesk Ticket -> Jira Bug escalation
119
+ - HubSpot Deal Won -> Asana onboarding kickoff
120
+ - Salesforce Lead -> HubSpot Contact sync
121
+ - Expand to additional high-frequency workflows:
122
+ - Slack incident -> Jira issue
123
+ - GitHub PR -> Slack notification
124
+ - Zendesk -> Salesforce case sync
125
+ - Stripe payment failed -> HubSpot task
126
+ - Notion request -> Asana task
127
+ - Standardize each playbook format:
128
+ - business trigger
129
+ - systems involved
130
+ - source-of-truth decision
131
+ - field mapping
132
+ - idempotency strategy
133
+ - retry and partial-failure policy
134
+ - reconciliation and rollback guidance
135
+ - observability / alerting checks
136
+ - Expose playbooks through MCP after the doc layer stabilizes:
137
+ - `list_playbooks`
138
+ - `get_playbook`
139
+ - search/ranking that prefers recipes and playbooks over generic mentions
140
+
141
+ **Acceptance criteria:**
142
+ - [ ] At least 3 production-grade playbooks checked into the repo
143
+ - [ ] README and INDEX reference the playbooks layer
144
+ - [ ] MCP server plan defined for playbook retrieval and discovery
145
+
146
+ ---
147
+
111
148
  ## Prioritization framework
112
149
 
113
150
  Each skill/recipe is scored on five dimensions (1–5 each):