@codemation/agent-skills 0.3.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.
- package/CHANGELOG.md +182 -0
- package/dist/metadata.json +383 -36
- package/package.json +3 -1
- package/skills/builder/ai-agent/SKILL.md +314 -0
- package/skills/builder/ai-agent/references/anti-patterns.md +24 -0
- package/skills/{codemation-cli → builder/cli}/SKILL.md +1 -8
- package/skills/builder/connect-external-systems/SKILL.md +191 -0
- package/skills/builder/credential-development/SKILL.md +86 -0
- package/skills/{codemation-credential-development → builder/credential-development}/references/credential-patterns.md +3 -3
- package/skills/builder/custom-node-development/SKILL.md +61 -0
- package/skills/builder/custom-node-development/references/credential-aware-nodes.md +52 -0
- package/skills/builder/custom-node-development/references/define-batch-node.md +54 -0
- package/skills/{codemation-custom-node-development → builder/custom-node-development}/references/define-node-per-item.md +14 -14
- package/skills/{codemation-custom-node-development → builder/custom-node-development}/references/node-patterns.md +33 -49
- package/skills/builder/document-ai/SKILL.md +167 -0
- package/skills/builder/execution-context/SKILL.md +436 -0
- package/skills/{codemation-framework-concepts → builder/framework-concepts}/SKILL.md +10 -18
- package/skills/builder/gmail/SKILL.md +327 -0
- package/skills/builder/human-in-the-loop/SKILL.md +82 -0
- package/skills/{codemation-mcp-capabilities → builder/mcp-capabilities}/SKILL.md +4 -11
- package/skills/{codemation-mcp-capabilities → builder/mcp-capabilities}/references/agent-with-mcp.ts +1 -1
- package/skills/builder/msgraph/SKILL.md +338 -0
- package/skills/builder/odoo/SKILL.md +498 -0
- package/skills/{codemation-plugin-development → builder/plugin-development}/SKILL.md +4 -7
- package/skills/{codemation-plugin-development → builder/plugin-development}/references/plugin-anatomy.md +36 -15
- package/skills/{codemation-plugin-development → builder/plugin-development}/references/plugin-structure.md +2 -2
- package/skills/builder/rest-node/SKILL.md +148 -0
- package/skills/builder/testing/SKILL.md +142 -0
- package/skills/builder/workflow-dsl/SKILL.md +493 -0
- package/skills/builder/workspace-files/SKILL.md +191 -0
- package/skills/concierge/credentials/SKILL.md +91 -0
- package/skills/concierge/intake-automation-playbook/SKILL.md +78 -0
- package/skills/concierge/scenario-invoice-to-accounting/SKILL.md +48 -0
- package/skills/concierge/scenario-procurement-intake/SKILL.md +58 -0
- package/skills/codemation-ai-agent-node/SKILL.md +0 -66
- package/skills/codemation-ai-agent-node/references/anti-patterns.md +0 -11
- package/skills/codemation-credential-development/SKILL.md +0 -57
- package/skills/codemation-custom-node-development/SKILL.md +0 -61
- package/skills/codemation-custom-node-development/references/credential-aware-nodes.md +0 -38
- package/skills/codemation-custom-node-development/references/define-batch-node.md +0 -38
- package/skills/codemation-workflow-dsl/SKILL.md +0 -78
- package/skills/codemation-workflow-dsl/references/builder-patterns.md +0 -120
- package/skills/codemation-workflow-dsl/references/complete-example.md +0 -263
- package/skills/codemation-workflow-dsl/references/workflow-testing.md +0 -194
- /package/skills/{codemation-cli → builder/cli}/references/command-map.md +0 -0
- /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:
|
|
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
|
|
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
|
-
|
|
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 `
|
|
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
|
|
package/skills/{codemation-mcp-capabilities → builder/mcp-capabilities}/references/agent-with-mcp.ts
RENAMED
|
@@ -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
|
|
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
|