@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,338 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: msgraph
|
|
3
|
+
description: Builds a Codemation workflow against Microsoft 365 via Microsoft Graph — trigger on new Outlook mail, reply, patch (read-state/categories/move), and reach OneDrive/Excel. Use whenever a workflow reads from or writes to a Microsoft 365 mailbox, OneDrive, or Excel workbook.
|
|
4
|
+
compatibility: Requires @codemation/core-nodes-msgraph. A Microsoft Graph OAuth credential binds on slot "auth".
|
|
5
|
+
tags: msgraph, microsoft, outlook, email, onedrive, excel, integration
|
|
6
|
+
uses: "@codemation/core-nodes-msgraph, @codemation/core-nodes, @codemation/core"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Codemation Microsoft Graph
|
|
10
|
+
|
|
11
|
+
`@codemation/core-nodes-msgraph` covers three Microsoft 365 surfaces, each binding a Graph OAuth
|
|
12
|
+
credential on slot **`"auth"`**:
|
|
13
|
+
|
|
14
|
+
- **Outlook mail** — trigger on new mail, plus reply / send / patch / get / attachment-download / folder-resolve.
|
|
15
|
+
- **OneDrive** — resolve, list, get, download, upload, copy, list-drives.
|
|
16
|
+
- **Excel** — open/close workbook, list/add sheets, read/write/style ranges.
|
|
17
|
+
|
|
18
|
+
Mail and Excel-on-mailbox bind the **`msgraph-mail-oauth`** credential type; OneDrive/Excel-on-drive
|
|
19
|
+
bind **`msgraph-drive-oauth`**. The two differ only in granted scopes — pick by surface.
|
|
20
|
+
|
|
21
|
+
These nodes are `defineNode` / `definePollingTrigger` definitions: instantiate with
|
|
22
|
+
**`node.create(config, label?, id?)`**, not `new`. Config fields accept per-item expressions via
|
|
23
|
+
`itemExpr` (type the expression with the item shape, e.g. `itemExpr<string, MsGraphMailItem>`).
|
|
24
|
+
|
|
25
|
+
**Discipline:** author straight from this file, then run `verify_workflow` and fix only what it flags.
|
|
26
|
+
Use `workflow-dsl` for the surrounding builder, trigger, and flow-control surface.
|
|
27
|
+
|
|
28
|
+
## The default flow: trigger on new mail → reply
|
|
29
|
+
|
|
30
|
+
`onNewMsGraphMailTrigger` polls a folder and emits one `MsGraphMailItem` per new message.
|
|
31
|
+
`outlookMessageReplyNode` replies by `messageId`. Type the `.create<MsGraphMailItem>` call so the
|
|
32
|
+
`itemExpr` callbacks see the trigger payload.
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { createWorkflowBuilder } from "@codemation/core-nodes";
|
|
36
|
+
import { itemExpr } from "@codemation/core";
|
|
37
|
+
import { onNewMsGraphMailTrigger, outlookMessageReplyNode, type MsGraphMailItem } from "@codemation/core-nodes-msgraph";
|
|
38
|
+
|
|
39
|
+
export default createWorkflowBuilder({ id: "wf.msgraph.acknowledge", name: "Acknowledge inbound mail" })
|
|
40
|
+
.trigger(
|
|
41
|
+
// Polls the mailbox folder; default filter is "isRead eq false". Slot "auth" must be bound.
|
|
42
|
+
onNewMsGraphMailTrigger.create(
|
|
43
|
+
{ mailbox: "me", folderId: "inbox", filter: "isRead eq false" },
|
|
44
|
+
"On new mail",
|
|
45
|
+
"mail-trigger",
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
.then(
|
|
49
|
+
outlookMessageReplyNode.create<MsGraphMailItem>(
|
|
50
|
+
{
|
|
51
|
+
mailbox: "me",
|
|
52
|
+
messageId: itemExpr<string, MsGraphMailItem>(({ item }) => item.json.messageId),
|
|
53
|
+
body: itemExpr<string, MsGraphMailItem>(
|
|
54
|
+
({ item }) => `Thanks — we received "${item.json.subject ?? "your email"}" and are processing it.`,
|
|
55
|
+
),
|
|
56
|
+
bodyType: "text", // or "html"
|
|
57
|
+
// replyAll: true / forward: true (forward requires `to`) / draftOnly: true to save without sending
|
|
58
|
+
},
|
|
59
|
+
"Reply",
|
|
60
|
+
"reply",
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
.build();
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Mail item shape
|
|
67
|
+
|
|
68
|
+
`onNewMsGraphMailTrigger` emits `MsGraphMailItem` — import the type, do not redefine it:
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
MsGraphMailItem = {
|
|
72
|
+
messageId: string; // target for reply / patch / get
|
|
73
|
+
conversationId?: string;
|
|
74
|
+
receivedDateTime: string;
|
|
75
|
+
internetMessageId?: string;
|
|
76
|
+
replyToMessageId?: string;
|
|
77
|
+
from?: { name?: string; address?: string };
|
|
78
|
+
to: { name?: string; address?: string }[];
|
|
79
|
+
cc?, bcc?: same shape;
|
|
80
|
+
subject?: string;
|
|
81
|
+
bodyText?: string; // prefer this for an LLM step
|
|
82
|
+
bodyHtml?: string;
|
|
83
|
+
attachments?: MsGraphMailAttachment[]; // metadata only — see below
|
|
84
|
+
headers?: Record<string, string>;
|
|
85
|
+
skippedAttachments?: { name; size; reason: "size-cap" }[];
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Trigger options: `{ mailbox; folderId?="inbox"; filter?; downloadAttachments?; attachmentSizeCapBytes?; pollIntervalMs? }`.
|
|
90
|
+
`mailbox: "me"` watches the credential owner's inbox; a UPN watches a shared mailbox (needs
|
|
91
|
+
`Mail.Read.Shared`). `filter` is a raw Graph OData `$filter` (e.g. `"hasAttachments eq true"`).
|
|
92
|
+
|
|
93
|
+
## Patch a message (mark read, categorize, move)
|
|
94
|
+
|
|
95
|
+
`outlookMessagePatchNode` is the common "tidy up after handling" step. Chain it directly off the
|
|
96
|
+
trigger.
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import { createWorkflowBuilder } from "@codemation/core-nodes";
|
|
100
|
+
import { itemExpr } from "@codemation/core";
|
|
101
|
+
import { onNewMsGraphMailTrigger, outlookMessagePatchNode, type MsGraphMailItem } from "@codemation/core-nodes-msgraph";
|
|
102
|
+
|
|
103
|
+
export default createWorkflowBuilder({ id: "wf.msgraph.triage", name: "Triage inbound mail" })
|
|
104
|
+
.trigger(onNewMsGraphMailTrigger.create({ mailbox: "me" }, "On new mail", "trigger"))
|
|
105
|
+
.then(
|
|
106
|
+
outlookMessagePatchNode.create<MsGraphMailItem>(
|
|
107
|
+
{
|
|
108
|
+
mailbox: "me",
|
|
109
|
+
messageId: itemExpr<string, MsGraphMailItem>(({ item }) => item.json.messageId),
|
|
110
|
+
isRead: true,
|
|
111
|
+
categories: ["Processed"],
|
|
112
|
+
move: { folderId: "Archive" }, // a well-known name or a folder id from outlookFolderResolveNode
|
|
113
|
+
},
|
|
114
|
+
"Mark processed",
|
|
115
|
+
"mark-processed",
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
.build();
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Attachments → binary
|
|
122
|
+
|
|
123
|
+
Set the trigger's `downloadAttachments: true` and the trigger's `execute` step stores each attachment's
|
|
124
|
+
bytes via `ctx.binary` under a slot named after the attachment (`name`, or `inline:{contentId}` for
|
|
125
|
+
embedded images). `item.json.attachments` carries metadata only
|
|
126
|
+
(`{ id, name, contentType, size, isInline?, contentId? }`) — never the payload. To pull one attachment
|
|
127
|
+
on demand later, use `outlookAttachmentDownloadNode` (`{ mailbox?, messageId, attachmentId, binarySlot?="attachment" }`).
|
|
128
|
+
Read the stored bytes off `item.binary[slot]`; feed them to `document-ai` for OCR.
|
|
129
|
+
|
|
130
|
+
## Node catalog
|
|
131
|
+
|
|
132
|
+
Instantiate each with `node.create(config, label?, id?)`. Bind `"auth"` to the matching credential type.
|
|
133
|
+
|
|
134
|
+
```text
|
|
135
|
+
Outlook mail (msgraph-mail-oauth)
|
|
136
|
+
onNewMsGraphMailTrigger { mailbox; folderId?; filter?; downloadAttachments?; pollIntervalMs? }
|
|
137
|
+
outlookMessageReplyNode { mailbox; messageId; body; bodyType; replyAll?; forward?; to?; cc?; bcc?; draftOnly?; attachments? }
|
|
138
|
+
outlookMessageSendNode { mailbox; to; subject; body; bodyType; cc?; bcc?; attachments?; draftOnly? }
|
|
139
|
+
outlookMessagePatchNode { mailbox; messageId; isRead?; categories?; move?: { folderId } }
|
|
140
|
+
outlookMessageGetNode { mailbox; messageId; expandAttachments? }
|
|
141
|
+
outlookAttachmentDownloadNode{ mailbox?; messageId; attachmentId; binarySlot?; sizeCapBytes? }
|
|
142
|
+
outlookFolderResolveNode { mailbox; folderPath } → { folderId, path, mailbox }
|
|
143
|
+
|
|
144
|
+
OneDrive (msgraph-drive-oauth)
|
|
145
|
+
driveResolveNode · driveListChildrenNode · driveItemGetNode · driveDownloadNode
|
|
146
|
+
driveUploadNode · driveCopyNode · driveListMyDrivesNode · driveListSharedWithMeNode
|
|
147
|
+
|
|
148
|
+
Excel (msgraph-drive-oauth; open a workbook first, pass the handle downstream)
|
|
149
|
+
excelOpenWorkbookNode · excelCloseWorkbookNode · excelListWorksheetsNode · excelAddSheetNode
|
|
150
|
+
excelReadRangeNode · excelWriteRangeNode · excelStyleRangeNode
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
For a OneDrive/Excel case not covered, resolve `ctx.getCredential<MsGraphSession>("auth")` in a
|
|
154
|
+
`Callback` and call `createGraphClient(session)` (exported from `@codemation/core-nodes-msgraph`) to
|
|
155
|
+
reach the raw Graph SDK.
|
|
156
|
+
|
|
157
|
+
## Gotchas
|
|
158
|
+
|
|
159
|
+
- **`.create(...)`, never `new`.** These are `defineNode` definitions — `new outlookMessageReplyNode(...)`
|
|
160
|
+
does not exist.
|
|
161
|
+
- **Type the `itemExpr`.** `.create<MsGraphMailItem>` alone leaves a bare `itemExpr` callback's
|
|
162
|
+
`item.json` as `unknown`. Write `itemExpr<string, MsGraphMailItem>(({ item }) => item.json.messageId)`.
|
|
163
|
+
- **Pick the right credential type on `"auth"`.** Mail nodes → `msgraph-mail-oauth`; OneDrive/Excel →
|
|
164
|
+
`msgraph-drive-oauth`. Bind every node's slot before activation.
|
|
165
|
+
- **`filter` is server-side OData.** It is a raw `$filter` string — malformed expressions fail the poll.
|
|
166
|
+
The trigger establishes a baseline on its first cycle (emits nothing) so existing mail doesn't replay.
|
|
167
|
+
- **Attachment bytes need `downloadAttachments: true`** (or a later `outlookAttachmentDownloadNode`);
|
|
168
|
+
the item JSON only ever carries attachment metadata.
|
|
169
|
+
|
|
170
|
+
## Testing an Outlook workflow on real mail (not fabricated fixtures)
|
|
171
|
+
|
|
172
|
+
Use `OutlookFolderTestSource` (a `TestTriggerNodeConfig`) to run the persistent Tests-tab suite on
|
|
173
|
+
real emails from a **designated test folder** in the same mailbox. Each message becomes one test
|
|
174
|
+
case and yields items in the **identical `MsGraphMailItem` shape** the live trigger emits — so OCR,
|
|
175
|
+
extraction, and matching all run downstream in tests too.
|
|
176
|
+
|
|
177
|
+
**The designated test folder is communicated to the builder by the concierge as part of the build
|
|
178
|
+
task.** If the folder id/name is absent from the task, call `report_flag({ kind: "gap" })` and note
|
|
179
|
+
that the test folder must be provided. Prefer declaring the folder id as a named constant the owner
|
|
180
|
+
fills (e.g. `const TEST_FOLDER = "TODO(setup): paste the test folder id here"`) rather than leaving
|
|
181
|
+
an unexplained placeholder string buried inside the node config. NEVER fabricate a folder id or fall
|
|
182
|
+
back to hard-coded fixture data.
|
|
183
|
+
|
|
184
|
+
The topology that matters:
|
|
185
|
+
|
|
186
|
+
```text
|
|
187
|
+
trigger → [shared processing: OCR / extraction / matching] → IsTestRun → {
|
|
188
|
+
true (test): Assertion — checks the EXTRACTION OUTCOME, not the raw email
|
|
189
|
+
false (live): ALL side-effects — reply + patch (mark read/categorize) + ERP write, all gated together
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
`IsTestRun` must go **after** the shared processing and **just before** the side-effects. If it
|
|
194
|
+
forks before the extraction, the test path asserts raw trigger bytes and proves nothing.
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import { z } from "zod";
|
|
198
|
+
import {
|
|
199
|
+
createWorkflowBuilder,
|
|
200
|
+
AIAgent,
|
|
201
|
+
IsTestRun,
|
|
202
|
+
Assertion,
|
|
203
|
+
CodemationChatModelConfig,
|
|
204
|
+
} from "@codemation/core-nodes";
|
|
205
|
+
import { itemExpr } from "@codemation/core";
|
|
206
|
+
import {
|
|
207
|
+
onNewMsGraphMailTrigger,
|
|
208
|
+
OutlookFolderTestSource,
|
|
209
|
+
outlookMessageReplyNode,
|
|
210
|
+
outlookMessagePatchNode,
|
|
211
|
+
type MsGraphMailItem,
|
|
212
|
+
} from "@codemation/core-nodes-msgraph";
|
|
213
|
+
|
|
214
|
+
// ── output schema for the extraction step ─────────────────────────────────
|
|
215
|
+
// messageId is passed through so the side-effect nodes can address the message after extraction.
|
|
216
|
+
const ExtractionSchema = z.object({
|
|
217
|
+
messageId: z.string(), // pass-through from trigger — needed by reply/patch nodes
|
|
218
|
+
customer: z.string(), // recognised company/customer name; empty string when unrecognised
|
|
219
|
+
lineItems: z.array(z.object({ description: z.string(), amount: z.number() })),
|
|
220
|
+
});
|
|
221
|
+
type ExtractionOutput = z.infer<typeof ExtractionSchema>;
|
|
222
|
+
|
|
223
|
+
// ── test folder ───────────────────────────────────────────────────────────
|
|
224
|
+
// Declare the folder id as a named constant the owner fills.
|
|
225
|
+
// If the concierge did not provide it → call report_flag({ kind: "gap" }) instead.
|
|
226
|
+
const TEST_FOLDER = "TODO(setup): paste the test folder id here";
|
|
227
|
+
|
|
228
|
+
// SEPARATE top-level export — the Tests tab discovers it. NOT a second .trigger().
|
|
229
|
+
// `new`, not `.create(...)`: the test source is a class (like GmailLabelTestSource), not a defineNode.
|
|
230
|
+
export const testSource = new OutlookFolderTestSource(
|
|
231
|
+
"Outlook test cases",
|
|
232
|
+
{ mailbox: "me", folderId: TEST_FOLDER, maxResults: 20 },
|
|
233
|
+
"outlook-test-source",
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
export default createWorkflowBuilder({ id: "wf.msgraph.procurement", name: "Procurement intake" })
|
|
237
|
+
.trigger(onNewMsGraphMailTrigger.create({ mailbox: "me", folderId: "inbox" }, "New email", "mail-trigger"))
|
|
238
|
+
// ── shared processing (runs in BOTH test and live paths) ────────────────
|
|
239
|
+
// Extract structured fields from the email body. Include messageId so
|
|
240
|
+
// downstream side-effect nodes (reply, patch, ERP) can address the message.
|
|
241
|
+
.then(
|
|
242
|
+
new AIAgent<MsGraphMailItem, ExtractionOutput>({
|
|
243
|
+
name: "Extract procurement data",
|
|
244
|
+
id: "extract-procurement-data",
|
|
245
|
+
messages: [
|
|
246
|
+
{
|
|
247
|
+
role: "system",
|
|
248
|
+
content:
|
|
249
|
+
"Extract the supplier/customer name and line items from the email. " +
|
|
250
|
+
"Include the messageId field verbatim from the input. " +
|
|
251
|
+
'Reply with strict JSON matching {"messageId","customer","lineItems":[{"description","amount"}]}.',
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
role: "user",
|
|
255
|
+
content: ({ item }) => `messageId: ${item.json.messageId}\n\n${item.json.bodyText ?? ""}`,
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
chatModel: new CodemationChatModelConfig("Managed AI", "low"),
|
|
259
|
+
outputSchema: ExtractionSchema,
|
|
260
|
+
guardrails: { maxTurns: 1 },
|
|
261
|
+
}),
|
|
262
|
+
)
|
|
263
|
+
// ── IsTestRun: AFTER all shared processing, JUST BEFORE side-effects ───
|
|
264
|
+
.then(new IsTestRun<ExtractionOutput>("Is this a test run?", "is-test-run"))
|
|
265
|
+
.when({
|
|
266
|
+
// true = test path: assert the EXTRACTION OUTCOME, not the raw email fields
|
|
267
|
+
true: [
|
|
268
|
+
new Assertion<ExtractionOutput>({
|
|
269
|
+
name: "Extraction outcome",
|
|
270
|
+
id: "assert-extraction-outcome",
|
|
271
|
+
assertions: (item) => [
|
|
272
|
+
{
|
|
273
|
+
// Did the model recognise a company/customer?
|
|
274
|
+
name: "customer recognised",
|
|
275
|
+
score: item.json.customer.trim().length > 0 ? 1 : 0,
|
|
276
|
+
actual: item.json.customer,
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
// Did the model extract at least one line item?
|
|
280
|
+
name: "line items extracted",
|
|
281
|
+
score: item.json.lineItems.length > 0 ? 1 : 0,
|
|
282
|
+
actual: item.json.lineItems.length,
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
}),
|
|
286
|
+
],
|
|
287
|
+
// false = live path: EVERY side-effect is gated here together
|
|
288
|
+
// (ERP write / create-order call belongs here too — never outside this branch)
|
|
289
|
+
false: [
|
|
290
|
+
outlookMessageReplyNode.create<ExtractionOutput>(
|
|
291
|
+
{
|
|
292
|
+
mailbox: "me",
|
|
293
|
+
messageId: itemExpr<string, ExtractionOutput>(({ item }) => item.json.messageId),
|
|
294
|
+
body: itemExpr<string, ExtractionOutput>(
|
|
295
|
+
({ item }) =>
|
|
296
|
+
`Thank you — we received your procurement request and are processing it.` +
|
|
297
|
+
(item.json.customer ? ` Customer: ${item.json.customer}.` : ""),
|
|
298
|
+
),
|
|
299
|
+
bodyType: "text",
|
|
300
|
+
// draftOnly is NOT a test switch — see the note below. Sending is gated by IsTestRun.
|
|
301
|
+
},
|
|
302
|
+
"Acknowledge receipt",
|
|
303
|
+
"reply",
|
|
304
|
+
),
|
|
305
|
+
outlookMessagePatchNode.create<ExtractionOutput>(
|
|
306
|
+
{
|
|
307
|
+
mailbox: "me",
|
|
308
|
+
messageId: itemExpr<string, ExtractionOutput>(({ item }) => item.json.messageId),
|
|
309
|
+
isRead: true,
|
|
310
|
+
categories: ["Processed"],
|
|
311
|
+
// move: { folderId: "Archive" } — a well-known name or an id from outlookFolderResolveNode
|
|
312
|
+
},
|
|
313
|
+
"Mark processed",
|
|
314
|
+
"mark-processed",
|
|
315
|
+
),
|
|
316
|
+
// → Add the ERP write node here (e.g. odooCreateSaleOrderNode) before marking processed.
|
|
317
|
+
],
|
|
318
|
+
})
|
|
319
|
+
.build();
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**Key rules:**
|
|
323
|
+
|
|
324
|
+
- `OutlookFolderTestSource` is a **separate `export const`**, never a second `.trigger(...)` on the chain. It is a class (mirroring `GmailLabelTestSource`) — instantiate with `new`, not `.create(...)`.
|
|
325
|
+
- **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.
|
|
326
|
+
- **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.
|
|
327
|
+
- **Gate EVERY side-effect on the `false` branch.** Reply, patch (mark read / categorize / move), and ERP writes all belong on the `false` branch — together. A single gated patch 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.
|
|
328
|
+
- **`draftOnly` is NOT the test mechanism.** Gate the reply behind `IsTestRun` (it lives on the `false`/live branch, so a test run never reaches it and no mail is sent). Do not reach for `outlookMessageReplyNode`'s `draftOnly: true` to make a run "safe": `draftOnly` saves a draft on the live account, it is not a test-mode switch.
|
|
329
|
+
- **Absent test folder → `report_flag` + declare a required input, never a silent placeholder.** If the concierge did not provide the test folder, call `report_flag({ kind: "gap" })`. When a placeholder is unavoidable, declare it as a named constant at the top of the file (as `TEST_FOLDER` above) so the owner knows exactly what to fill in — never bury an opaque string inside a node config.
|
|
330
|
+
- `OutlookFolderTestSource` yields items in the same raw `MsGraphMailItem` shape as the live `onNewMsGraphMailTrigger` — no pre-processing, no canonicalization. That's what makes the test meaningful.
|
|
331
|
+
- The `caseLabel` on `OutlookFolderTestSource` defaults to the email subject so each test-case row in the Tests tab is human-readable.
|
|
332
|
+
|
|
333
|
+
## Read next when needed
|
|
334
|
+
|
|
335
|
+
- `workflow-dsl` — builder, triggers, flow control, the per-item contract.
|
|
336
|
+
- `document-ai` — OCR an attachment's bytes once they're in `ctx.binary`.
|
|
337
|
+
- `ai-agent` — summarize or triage `item.json.bodyText` with an LLM before replying.
|
|
338
|
+
- `testing` — the full `IsTestRun` + `Assertion` + `TestTrigger` pattern (trigger-agnostic).
|