@codemation/agent-skills 0.4.0 → 0.5.1

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +165 -0
  2. package/dist/metadata.json +358 -48
  3. package/package.json +3 -1
  4. package/skills/builder/ai-agent/SKILL.md +314 -0
  5. package/skills/builder/ai-agent/references/anti-patterns.md +24 -0
  6. package/skills/{codemation-cli → builder/cli}/SKILL.md +1 -8
  7. package/skills/builder/connect-external-systems/SKILL.md +191 -0
  8. package/skills/builder/credential-development/SKILL.md +86 -0
  9. package/skills/{codemation-credential-development → builder/credential-development}/references/credential-patterns.md +3 -3
  10. package/skills/builder/custom-node-development/SKILL.md +61 -0
  11. package/skills/builder/custom-node-development/references/credential-aware-nodes.md +52 -0
  12. package/skills/builder/custom-node-development/references/define-batch-node.md +54 -0
  13. package/skills/{codemation-custom-node-development → builder/custom-node-development}/references/define-node-per-item.md +14 -14
  14. package/skills/{codemation-custom-node-development → builder/custom-node-development}/references/node-patterns.md +33 -49
  15. package/skills/builder/document-ai/SKILL.md +167 -0
  16. package/skills/builder/execution-context/SKILL.md +436 -0
  17. package/skills/{codemation-framework-concepts → builder/framework-concepts}/SKILL.md +10 -18
  18. package/skills/builder/gmail/SKILL.md +327 -0
  19. package/skills/builder/human-in-the-loop/SKILL.md +82 -0
  20. package/skills/{codemation-mcp-capabilities → builder/mcp-capabilities}/SKILL.md +4 -11
  21. package/skills/{codemation-mcp-capabilities → builder/mcp-capabilities}/references/agent-with-mcp.ts +1 -1
  22. package/skills/builder/msgraph/SKILL.md +338 -0
  23. package/skills/builder/odoo/SKILL.md +498 -0
  24. package/skills/{codemation-plugin-development → builder/plugin-development}/SKILL.md +4 -7
  25. package/skills/{codemation-plugin-development → builder/plugin-development}/references/plugin-anatomy.md +36 -15
  26. package/skills/{codemation-plugin-development → builder/plugin-development}/references/plugin-structure.md +2 -2
  27. package/skills/builder/rest-node/SKILL.md +148 -0
  28. package/skills/builder/testing/SKILL.md +142 -0
  29. package/skills/builder/workflow-dsl/SKILL.md +493 -0
  30. package/skills/builder/workspace-files/SKILL.md +191 -0
  31. package/skills/concierge/credentials/SKILL.md +91 -0
  32. package/skills/concierge/intake-automation-playbook/SKILL.md +78 -0
  33. package/skills/concierge/scenario-invoice-to-accounting/SKILL.md +48 -0
  34. package/skills/concierge/scenario-procurement-intake/SKILL.md +58 -0
  35. package/skills/codemation-ai-agent-node/SKILL.md +0 -66
  36. package/skills/codemation-ai-agent-node/references/anti-patterns.md +0 -11
  37. package/skills/codemation-credential-development/SKILL.md +0 -57
  38. package/skills/codemation-custom-node-development/SKILL.md +0 -61
  39. package/skills/codemation-custom-node-development/references/credential-aware-nodes.md +0 -38
  40. package/skills/codemation-custom-node-development/references/define-batch-node.md +0 -38
  41. package/skills/codemation-document-scanner/SKILL.md +0 -136
  42. package/skills/codemation-workflow-dsl/SKILL.md +0 -78
  43. package/skills/codemation-workflow-dsl/references/builder-patterns.md +0 -120
  44. package/skills/codemation-workflow-dsl/references/complete-example.md +0 -263
  45. package/skills/codemation-workflow-dsl/references/workflow-testing.md +0 -194
  46. package/skills/codemation-workspace-files/SKILL.md +0 -142
  47. /package/skills/{codemation-cli → builder/cli}/references/command-map.md +0 -0
  48. /package/skills/{codemation-framework-concepts → builder/framework-concepts}/references/architecture-map.md +0 -0
@@ -0,0 +1,327 @@
1
+ ---
2
+ name: gmail
3
+ description: Builds a Codemation workflow that reacts to Gmail and acts on it — trigger on a label, reply, modify labels, and hand attachment bytes to the document scanner. Use whenever a workflow reads from or writes to a Gmail mailbox.
4
+ compatibility: Requires @codemation/core-nodes-gmail. A Gmail OAuth credential binds on slot "auth".
5
+ tags: gmail, email, label, reply, attachment, trigger, integration
6
+ uses: "@codemation/core-nodes-gmail, @codemation/core-nodes, @codemation/core"
7
+ ---
8
+
9
+ # Codemation Gmail
10
+
11
+ Four building blocks, imported from `@codemation/core-nodes-gmail`:
12
+
13
+ - **`OnNewGmailTrigger`** — fires one item per new message (optionally on a label).
14
+ - **`ReplyToGmailMessage`** — reply to a message by id.
15
+ - **`ModifyGmailLabels`** — add/remove labels on a message or thread.
16
+ - **`SendGmailMessage`** — send a fresh message (not a reply).
17
+
18
+ Every node binds a Gmail OAuth credential on slot **`"auth"`** (accepted type `oauth.google.gmail`). You
19
+ declare the slot by adding the node; the concierge binds the credential (separate skill).
20
+
21
+ **Discipline:** author straight from this file, then run `verify_workflow` and fix only what it flags.
22
+ Use `workflow-dsl` for the surrounding builder, trigger, and flow-control surface.
23
+
24
+ ## The default flow: trigger on a label → reply → mark processed
25
+
26
+ The trigger's `labelIds` filters to messages carrying a Gmail label. `messageId` from each item targets
27
+ the reply and the label change. Both action nodes take **config expressions** resolved per item — read
28
+ the trigger payload with `item.json` cast to `OnNewGmailTriggerItemJson` (the class nodes' expression
29
+ slots are item-untyped, so cast inside the `itemExpr`).
30
+
31
+ ```typescript
32
+ import { createWorkflowBuilder } from "@codemation/core-nodes";
33
+ import { itemExpr } from "@codemation/core";
34
+ import { OnNewGmailTrigger, ReplyToGmailMessage, ModifyGmailLabels } from "@codemation/core-nodes-gmail";
35
+ import type { OnNewGmailTriggerItemJson } from "@codemation/core-nodes-gmail";
36
+
37
+ export default createWorkflowBuilder({ id: "wf.gmail.acknowledge", name: "Acknowledge inbound mail" })
38
+ .trigger(
39
+ // One item per new message carrying the "to-process" label. Slot "auth" must be bound.
40
+ new OnNewGmailTrigger("New email", { mailbox: "me", labelIds: ["to-process"] }),
41
+ )
42
+ .then(
43
+ new ReplyToGmailMessage("Acknowledge", {
44
+ messageId: itemExpr(({ item }) => (item.json as OnNewGmailTriggerItemJson).messageId),
45
+ text: itemExpr(
46
+ ({ item }) => `Thanks — we received "${(item.json as OnNewGmailTriggerItemJson).subject ?? "your email"}".`,
47
+ ),
48
+ }),
49
+ )
50
+ .then(
51
+ new ModifyGmailLabels(
52
+ "Mark processed",
53
+ {
54
+ target: "message", // or "thread" to label the whole conversation
55
+ messageId: itemExpr(({ item }) => (item.json as OnNewGmailTriggerItemJson).messageId),
56
+ addLabels: ["Processed"], // display name — resolved to the real label ID at runtime
57
+ removeLabelIds: ["UNREAD"], // system label IDs (UNREAD, INBOX, SENT, …) use addLabelIds/removeLabelIds
58
+ },
59
+ "mark-processed",
60
+ ),
61
+ )
62
+ .build();
63
+ ```
64
+
65
+ ## Trigger item shape
66
+
67
+ `OnNewGmailTrigger` emits `OnNewGmailTriggerItemJson` — import the type, do not redefine it:
68
+
69
+ ```text
70
+ OnNewGmailTriggerItemJson = {
71
+ mailbox: string;
72
+ historyId: string;
73
+ messageId: string; // target for ReplyToGmailMessage / ModifyGmailLabels (target: "message")
74
+ threadId?: string;
75
+ subject?: string;
76
+ from?: string;
77
+ to?: string;
78
+ deliveredTo?: string;
79
+ snippet?: string;
80
+ internalDate?: string;
81
+ labelIds: readonly string[];
82
+ headers: Record<string, string>;
83
+ textPlain?: string; // inline plain-text body — prefer this for an LLM step
84
+ textHtml?: string; // inline HTML body
85
+ attachments: readonly GmailMessageAttachmentRecord[]; // metadata only — see below
86
+ }
87
+ ```
88
+
89
+ Trigger options: `{ mailbox: string; labelIds?: string[]; query?: string; downloadAttachments?: boolean }`.
90
+ Set `query` for a raw Gmail search; set `downloadAttachments: true` to fetch attachment bytes.
91
+
92
+ ## Attachments → binary → document scanner (the important one)
93
+
94
+ `item.json.attachments` is **metadata only** — bytes never ride on `item.json` (binary always goes
95
+ through `ctx.binary`). Each record:
96
+
97
+ ```text
98
+ GmailMessageAttachmentRecord = {
99
+ attachmentId: string;
100
+ filename?: string;
101
+ mimeType: string; // e.g. "application/pdf"
102
+ size?: number;
103
+ binaryName: string; // ← the KEY the bytes live under in ctx.binary
104
+ }
105
+ ```
106
+
107
+ Construct the trigger with `downloadAttachments: true` so the bytes are fetched. Then feed the **record's
108
+ `binaryName`** to the scanner's `binaryField` — never hardcode `"data"`. The scanner node is a
109
+ `defineNode` (`.create(config, label, id)`), and its config is item-typed, so use a typed `itemExpr` to
110
+ pick the first PDF's `binaryName`:
111
+
112
+ ```typescript
113
+ import { createWorkflowBuilder, codemationDocumentScannerNode } from "@codemation/core-nodes";
114
+ import { itemExpr } from "@codemation/core";
115
+ import { OnNewGmailTrigger } from "@codemation/core-nodes-gmail";
116
+ import type { OnNewGmailTriggerItemJson } from "@codemation/core-nodes-gmail";
117
+
118
+ export default createWorkflowBuilder({ id: "wf.gmail.scan-invoice", name: "Scan emailed invoice" })
119
+ .trigger(
120
+ new OnNewGmailTrigger("New invoice email", {
121
+ mailbox: "me",
122
+ labelIds: ["invoices"],
123
+ downloadAttachments: true, // required — fetches the bytes into ctx.binary
124
+ }),
125
+ )
126
+ .then(
127
+ codemationDocumentScannerNode.create<OnNewGmailTriggerItemJson>(
128
+ {
129
+ // Pick the first PDF attachment's binary key; fall back to "data" when none.
130
+ binaryField: itemExpr<string, OnNewGmailTriggerItemJson>(
131
+ ({ item }) => item.json.attachments.find((a) => a.mimeType === "application/pdf")?.binaryName ?? "data",
132
+ ),
133
+ analyzerType: "invoice",
134
+ },
135
+ "Scan invoice",
136
+ "scan-invoice",
137
+ ),
138
+ )
139
+ .build();
140
+ ```
141
+
142
+ When there is no attachment, `attachments` is empty — branch on that (an `If` on
143
+ `item.json.attachments.length`) rather than assuming a PDF. The scanner replaces `item.json` with
144
+ `{ markdown, fields }`; see `document-ai`.
145
+
146
+ ## Gotchas
147
+
148
+ - **Bind slot `"auth"`.** Every Gmail node requires a `oauth.google.gmail` credential on `"auth"`. The
149
+ trigger and each action node each declare their own slot — bind them all before activation.
150
+ - **Use display names; don't fabricate IDs.** `addLabels`/`removeLabels` (on `ModifyGmailLabels`)
151
+ and `labelIds` (on `OnNewGmailTrigger`) accept display name strings — the plugin resolves them to
152
+ real Gmail label IDs at runtime via `GmailConfiguredLabelService`. **Prefer these over raw IDs**:
153
+ display names work across accounts; a hard-coded `"Label_123"` only works on one specific account.
154
+ Use `addLabelIds`/`removeLabelIds` only for system labels that have stable IDs: `"UNREAD"`,
155
+ `"INBOX"`, `"SENT"`, `"IMPORTANT"`, etc.
156
+ Never invent a Gmail label ID (`"Label_orders"`, `"Label_processed"`) — those will not exist in a
157
+ real account and will throw at runtime. If you don't know the real label name, use a named
158
+ `// TODO(setup): <description>` placeholder and call `record_decision` to surface the gap
159
+ (see `connect-external-systems` → "Build what works; report what's missing").
160
+ - **Cast inside the `itemExpr` for class nodes.** `ReplyToGmailMessage` / `ModifyGmailLabels` expression
161
+ slots are item-untyped, so cast inside: `itemExpr(({ item }) => (item.json as OnNewGmailTriggerItemJson).messageId)`.
162
+ - **`ItemExpr<T>` is invariant — return type must match `T` exactly.** Prefer bare `itemExpr(...)` (no explicit generics) and let TypeScript infer. When the field is typed `ItemExpr<string>` but your access is `string | undefined`, narrow it: add `?? ""` so the return is `string`. Do NOT annotate `itemExpr<string, SomeType>` and then return `string | undefined` — it looks like it should work but `ItemExpr<T>` is invariant in `T` and TypeScript will reject it. Example: `messageId: itemExpr(({ item }) => (item.json as OnNewGmailTriggerItemJson).messageId ?? "")`. For the scanner's `binaryField` (also `ItemExpr<string>`), the `?.binaryName ?? "data"` fallback already returns `string` — the explicit `itemExpr<string, T>` annotation there is fine because the return is concrete.
163
+ - **Attachment bytes need `downloadAttachments: true`.** Without it `attachments` carries metadata but
164
+ `ctx.binary[binaryName]` is empty and the scanner throws.
165
+ - **Remove `UNREAD` (or add a processed label) to avoid retriggering.** The trigger re-emits matching
166
+ unprocessed mail each poll.
167
+
168
+ ## Testing a Gmail workflow on real mail (not fabricated fixtures)
169
+
170
+ Use `GmailLabelTestSource` (a `TestTriggerNodeConfig`) to run the persistent Tests-tab suite on
171
+ real emails from a **designated test label** in the same mailbox. Each message becomes one test
172
+ case and yields items in the **identical `OnNewGmailTriggerItemJson` shape** the live trigger emits
173
+ — so OCR, extraction, and matching all run downstream in tests too.
174
+
175
+ **The designated test label is communicated to the builder by the concierge as part of the build
176
+ task.** If the label name is absent from the task, call `report_flag({ kind: "gap" })` and note
177
+ that the test label must be provided. Prefer declaring the label name as a named constant or config
178
+ the owner fills (e.g. `const TEST_LABEL = "TODO(setup): paste the test label name here"`) rather
179
+ than leaving an unexplained placeholder string buried inside the node config. NEVER fabricate a
180
+ label name or fall back to hard-coded fixture data.
181
+
182
+ The topology that matters:
183
+
184
+ ```
185
+ trigger → [shared processing: OCR / extraction / matching] → IsTestRun → {
186
+ true (test): Assertion — checks the EXTRACTION OUTCOME, not the raw email
187
+ false (live): ALL side-effects — reply + label change + ERP write, all gated together
188
+ }
189
+ ```
190
+
191
+ `IsTestRun` must go **after** the shared processing and **just before** the side-effects. If it
192
+ forks before the extraction, the test path asserts raw trigger bytes and proves nothing.
193
+
194
+ ```typescript
195
+ import { z } from "zod";
196
+ import {
197
+ createWorkflowBuilder,
198
+ AIAgent,
199
+ IsTestRun,
200
+ Assertion,
201
+ CodemationChatModelConfig,
202
+ } from "@codemation/core-nodes";
203
+ import { itemExpr } from "@codemation/core";
204
+ import {
205
+ OnNewGmailTrigger,
206
+ GmailLabelTestSource,
207
+ ModifyGmailLabels,
208
+ ReplyToGmailMessage,
209
+ } from "@codemation/core-nodes-gmail";
210
+ import type { OnNewGmailTriggerItemJson } from "@codemation/core-nodes-gmail";
211
+
212
+ // ── output schema for the extraction step ─────────────────────────────────
213
+ // messageId is passed through so the side-effect nodes can access it after extraction.
214
+ const ExtractionSchema = z.object({
215
+ messageId: z.string(), // pass-through from trigger — needed by reply/label nodes
216
+ customer: z.string(), // recognised company/customer name; empty string when unrecognised
217
+ lineItems: z.array(z.object({ description: z.string(), amount: z.number() })),
218
+ });
219
+ type ExtractionOutput = z.infer<typeof ExtractionSchema>;
220
+
221
+ // ── test label ────────────────────────────────────────────────────────────
222
+ // Declare the label as a named constant the owner fills.
223
+ // If the concierge did not provide it → call report_flag({ kind: "gap" }) instead.
224
+ const TEST_LABEL = "TODO(setup): paste the test label name here";
225
+
226
+ // SEPARATE top-level export — the Tests tab discovers it. NOT a second .trigger().
227
+ export const testSource = new GmailLabelTestSource(
228
+ "Gmail test cases",
229
+ { mailbox: "me", labelIds: [TEST_LABEL], maxResults: 20 },
230
+ "gmail-test-source",
231
+ );
232
+
233
+ export default createWorkflowBuilder({ id: "wf.gmail.procurement", name: "Procurement intake" })
234
+ .trigger(new OnNewGmailTrigger("New email", { mailbox: "me", labelIds: ["procurement/inbox"] }))
235
+ // ── shared processing (runs in BOTH test and live paths) ────────────────
236
+ // Extract structured fields from the email body. Include messageId so
237
+ // downstream side-effect nodes (reply, label, ERP) can address the message.
238
+ .then(
239
+ new AIAgent<OnNewGmailTriggerItemJson, ExtractionOutput>({
240
+ name: "Extract procurement data",
241
+ id: "extract-procurement-data",
242
+ messages: [
243
+ {
244
+ role: "system",
245
+ content:
246
+ "Extract the supplier/customer name and line items from the email. " +
247
+ "Include the messageId field verbatim from the input. " +
248
+ 'Reply with strict JSON matching {"messageId","customer","lineItems":[{"description","amount"}]}.',
249
+ },
250
+ {
251
+ role: "user",
252
+ content: ({ item }) =>
253
+ `messageId: ${item.json.messageId}\n\n${item.json.textPlain ?? item.json.snippet ?? ""}`,
254
+ },
255
+ ],
256
+ chatModel: new CodemationChatModelConfig("Managed AI", "low"),
257
+ outputSchema: ExtractionSchema,
258
+ guardrails: { maxTurns: 1 },
259
+ }),
260
+ )
261
+ // ── IsTestRun: AFTER all shared processing, JUST BEFORE side-effects ───
262
+ .then(new IsTestRun<ExtractionOutput>("Is this a test run?", "is-test-run"))
263
+ .when({
264
+ // true = test path: assert the EXTRACTION OUTCOME, not the raw email fields
265
+ true: [
266
+ new Assertion<ExtractionOutput>({
267
+ name: "Extraction outcome",
268
+ id: "assert-extraction-outcome",
269
+ assertions: (item) => [
270
+ {
271
+ // Did the model recognise a company/customer?
272
+ name: "customer recognised",
273
+ score: item.json.customer.trim().length > 0 ? 1 : 0,
274
+ actual: item.json.customer,
275
+ },
276
+ {
277
+ // Did the model extract at least one line item?
278
+ name: "line items extracted",
279
+ score: item.json.lineItems.length > 0 ? 1 : 0,
280
+ actual: item.json.lineItems.length,
281
+ },
282
+ ],
283
+ }),
284
+ ],
285
+ // false = live path: EVERY side-effect is gated here together
286
+ // (ERP write / create-order call belongs here too — never outside this branch)
287
+ false: [
288
+ new ReplyToGmailMessage("Acknowledge receipt", {
289
+ messageId: itemExpr(({ item }) => (item.json as ExtractionOutput).messageId ?? ""),
290
+ text: itemExpr(
291
+ ({ item }) =>
292
+ `Thank you — we received your procurement request and are processing it.` +
293
+ ((item.json as ExtractionOutput).customer ? ` Customer: ${(item.json as ExtractionOutput).customer}.` : ""),
294
+ ),
295
+ }),
296
+ new ModifyGmailLabels(
297
+ "Mark processed",
298
+ {
299
+ target: "message",
300
+ messageId: itemExpr(({ item }) => (item.json as ExtractionOutput).messageId ?? ""),
301
+ addLabels: ["Processed"],
302
+ removeLabelIds: ["UNREAD"],
303
+ },
304
+ "mark-processed",
305
+ ),
306
+ // → Add the ERP write node here (e.g. odooCreateSaleOrderNode) before marking processed.
307
+ ],
308
+ })
309
+ .build();
310
+ ```
311
+
312
+ **Key rules:**
313
+
314
+ - `GmailLabelTestSource` is a **separate `export const`**, never a second `.trigger(...)` on the chain.
315
+ - **Place `IsTestRun` after all shared processing and just before the side-effects.** Processing steps (OCR, extraction, matching) must be upstream of `IsTestRun` so both the test path and the live path exercise the same logic. An `IsTestRun` placed right after the trigger asserts nothing useful.
316
+ - **Assert the processing outcome, not the raw email.** The `Assertion` on the `true` branch must check the extractor's structured output (recognised customer, extracted line items) — not `subject`, `from`, or other raw trigger fields. Asserting `subject.length > 0` proves only that an email arrived; asserting `lineItems.length > 0` proves the extraction worked.
317
+ - **Gate EVERY side-effect on the `false` branch.** Reply, label change, and ERP writes all belong on the `false` branch — together. A single gated label change while the reply or ERP write runs unconditionally defeats the purpose. If you add a node that writes to an external system, it must live on the `false` branch.
318
+ - **Absent test label → `report_flag` + declare a required input, never a silent placeholder.** If the concierge did not provide the test label, call `report_flag({ kind: "gap" })`. When a placeholder is unavoidable, declare it as a named constant at the top of the file (as `TEST_LABEL` above) so the owner knows exactly what to fill in — never bury an opaque string inside a node config.
319
+ - `GmailLabelTestSource` yields items in the same raw `OnNewGmailTriggerItemJson` shape as the live `OnNewGmailTrigger` — no pre-processing, no canonicalization. That's what makes the test meaningful.
320
+ - The `caseLabel` on `GmailLabelTestSource` defaults to the email subject so each test-case row in the Tests tab is human-readable.
321
+
322
+ ## Read next when needed
323
+
324
+ - `workflow-dsl` — builder, triggers, flow control, the per-item contract.
325
+ - `document-ai` — the attachment → `{ markdown, fields }` handoff in full.
326
+ - `ai-agent` — summarize or triage `item.json.textPlain` with an LLM before replying.
327
+ - `testing` — the full `IsTestRun` + `Assertion` + `TestTrigger` pattern (trigger-agnostic).
@@ -0,0 +1,82 @@
1
+ ---
2
+ name: human-in-the-loop
3
+ description: Pause a workflow for a person to approve, reject, or note before it continues, using the inboxApproval node and the builder's .humanApproval() step. The gate sits ON the live path and the run resumes on the human decision. Read this to gate any step that is expensive or irreversible to undo.
4
+ compatibility: Designed for Codemation workflows authored with @codemation/core-nodes.
5
+ tags: hitl, approval, inbox, review, gate
6
+ uses: "@codemation/core-nodes, @codemation/core"
7
+ ---
8
+
9
+ # Codemation Human-in-the-loop
10
+
11
+ Gate a step that's expensive to undo — a payment, an external write, a customer reply — by suspending the run and waiting for a person to decide (approve / reject). Use `inboxApproval` with the builder's `.humanApproval(...)` step so the gate sits **on the live path**: a real node the items flow through, not a dangling node next to a `NoOp`. The run resumes when the reviewer decides; **rejected items never reach the downstream nodes** (the engine discards them at the gate). The same node auto-routes to the managed control-plane inbox in managed mode and the local `/dev/inbox` otherwise — no per-deployment wiring.
12
+
13
+ ## The gate — one step on the chain
14
+
15
+ `inboxApproval` is a `defineHumanApprovalNode` node imported from `@codemation/core-nodes`. Place it with `.humanApproval(inboxApproval, config)`. The `title`/`body` fields are a static string or a callback `({ item }) => string`; the callback receives an **untyped `Item`**, so cast `item.json` to your type inside it. After the gate, the item json is `Input & { decision }` — downstream reads `item.json.decision.status`.
16
+
17
+ ```typescript
18
+ import { inboxApproval } from "@codemation/core-nodes";
19
+
20
+ type Invoice = { vendor: string; amount: number; currency: string };
21
+
22
+ const approvalConfig = {
23
+ title: ({ item }: { item: { json: unknown } }) => `Approve invoice from ${(item.json as Invoice).vendor}`,
24
+ body: ({ item }: { item: { json: unknown } }) => {
25
+ const inv = item.json as Invoice;
26
+ return `Amount: ${inv.amount} ${inv.currency}`;
27
+ },
28
+ priority: "normal" as const, // "low" | "normal" | "high"
29
+ timeout: "24h",
30
+ onTimeout: "halt" as const, // "halt" stops the run on timeout; "auto-accept" continues
31
+ };
32
+ ```
33
+
34
+ The decision shape merged into the item: `decision.status` is `"approved" | "rejected" | "timed-out" | "auto-accepted"`, plus optional `decision.actor`, `decision.decidedAt`, and `decision.note`.
35
+
36
+ ## One realistic complete example
37
+
38
+ Webhook receives an invoice → `.humanApproval` suspends for a reviewer → on approval the run continues and posts to accounting. The gate is the live path; rejected items stop here. This folds in the former `hitl-cp-inbox-approval` example.
39
+
40
+ ```typescript
41
+ import { createWorkflowBuilder, WebhookTrigger, inboxApproval, HttpRequest } from "@codemation/core-nodes";
42
+ import type { Item } from "@codemation/core";
43
+
44
+ type Invoice = { messageId: string; vendor: string; amount: number; currency: string };
45
+
46
+ export default createWorkflowBuilder({ id: "wf.invoice-approval", name: "Invoice approval (HITL)" })
47
+ .trigger(new WebhookTrigger("Receive invoice", { endpointKey: "invoices", methods: ["POST"] }))
48
+ // Suspend for a human. inboxApproval auto-routes to the CP inbox (managed) or /dev/inbox (local).
49
+ // title/body callbacks read the item at runtime — cast item.json to your type.
50
+ .humanApproval(inboxApproval, {
51
+ title: ({ item }: { item: Item }) => `Approve invoice from ${(item.json as Invoice).vendor}`,
52
+ body: ({ item }: { item: Item }) => {
53
+ const inv = item.json as Invoice;
54
+ return `Amount: ${inv.amount} ${inv.currency}\nMessage ID: ${inv.messageId}`;
55
+ },
56
+ priority: "normal",
57
+ timeout: "24h",
58
+ onTimeout: "halt",
59
+ })
60
+ // Reached only on approval — rejected items were discarded at the gate.
61
+ // After .humanApproval the json is Invoice & { decision }, so item.json.decision.status is typed.
62
+ .then(
63
+ new HttpRequest<Invoice & { decision: { status: string } }>("Post to accounting", {
64
+ method: "POST",
65
+ url: "https://accounting.example.com/api/invoices",
66
+ body: { kind: "json", data: { vendor: "${item.json.vendor}", amount: "${item.json.amount}" } },
67
+ id: "post-to-accounting",
68
+ }),
69
+ )
70
+ .build();
71
+ ```
72
+
73
+ ## Gate inside an agent (one line)
74
+
75
+ To let an `AIAgent` request approval as a tool instead of as a fixed step, bind the same node config with `AgentToolFactory.asTool(inboxApproval.create(config, "Inbox approval"), { name, onRejected, inputSchema, outputSchema, mapInput, mapOutput })`. `onRejected: "halt"` stops the run on rejection; `onRejected: "return"` feeds the rejection back to the agent so it can adapt. Use the fixed `.humanApproval` step for a single mandatory gate; use the tool form when the agent decides whether to escalate.
76
+
77
+ ## Gotchas
78
+
79
+ - **Gate the irreversible step.** Put `.humanApproval` immediately before the expensive write, not after it.
80
+ - **On the live path, not beside it.** `.humanApproval` returns the cursor for the next step — chain the downstream node off it. Don't approve next to a `NoOp` and continue regardless.
81
+ - **Cast `item.json` in title/body callbacks.** They receive an untyped `Item`; `item.json as YourType` keeps the gate compiling.
82
+ - **Rejected items stop at the gate.** Downstream nodes only see approved (and, with `onTimeout: "auto-accept"`, auto-accepted) items — read `item.json.decision.status` if you need to branch on the outcome.
@@ -1,20 +1,13 @@
1
1
  ---
2
- name: codemation-mcp-capabilities
2
+ name: mcp-capabilities
3
3
  description: Discover MCP servers registered on the Codemation control plane. Use before authoring agent workflows that reference mcpServers to find available server ids and their credential requirements.
4
- compatibility: Requires an installation paired with a connected control plane (Sprint 2+).
4
+ compatibility: Requires an installation paired with a connected control plane.
5
5
  tags: mcp, agent, tool
6
6
  ---
7
7
 
8
8
  # Codemation MCP Capabilities
9
9
 
10
- ## Mental model
11
-
12
- MCP servers extend `AIAgent` with tool access to external services (Gmail, Sheets, etc.). Server ids and credential requirements come from the control-plane registry — they are not hard-coded in framework code. The agent's `mcpServers` array contains stable server id slugs; each declared server surfaces a credential slot the operator must bind in the canvas before activation.
13
-
14
- ## When to use / when NOT
15
-
16
- Use this skill before writing `agent({ mcpServers: ["..."] })` to discover available server ids and their credential types.
17
- Do not use for general AIAgent authoring — read `codemation-ai-agent-node` for that.
10
+ MCP servers extend `AIAgent` with tool access to external services (Gmail, Sheets, etc.). Server ids and credential requirements come from the control-plane registry — they are not hard-coded in framework code. The agent's `mcpServers` array contains stable server id slugs; each declared server surfaces a credential slot the operator must bind in the canvas before activation. Use this skill before writing `agent({ mcpServers: ["..."] })` to discover available server ids and their credential types.
18
11
 
19
12
  ## Managed mode: CP-loaded MCP servers (default path)
20
13
 
@@ -30,7 +23,7 @@ For a full wired example — cron workflow + AIAgent + mcpServers — use your h
30
23
 
31
24
  ## Non-managed: plugin-declared MCP servers
32
25
 
33
- In self-hosted / non-managed deployments, MCP servers can also be declared via `mcpServers: [...]` in a `definePlugin(...)` call. This is a framework-author pattern — do not use it in managed-mode workflows. See `references/plugin-anatomy.md` in the `codemation-plugin-development` skill for the plugin declaration syntax.
26
+ In self-hosted / non-managed deployments, MCP servers can also be declared via `mcpServers: [...]` in a `definePlugin(...)` call. This is a framework-author pattern — do not use it in managed-mode workflows. See `references/plugin-anatomy.md` in the `plugin-development` skill for the plugin declaration syntax.
34
27
 
35
28
  ## Decision branches & gotchas
36
29
 
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * Cron / webhook workflows use createWorkflowBuilder({id, name}).trigger(new XxxTrigger(...))
8
8
  * and chain with .then(new SomeNodeConfig(...)). The fluent .map/.if/.agent helpers are
9
- * only available via workflow("id").manualTrigger(...). See codemation-workflow-dsl skill.
9
+ * only available via workflow("id").manualTrigger(...). See workflow-dsl skill.
10
10
  *
11
11
  * `mcpServers` is a plain array of server ids. Each declared server surfaces a credential
12
12
  * slot on the materialized MCP connection node (same shape as ChatModel/Tool connection