@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.
- package/CHANGELOG.md +165 -0
- package/dist/metadata.json +358 -48
- 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-document-scanner/SKILL.md +0 -136
- 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-workspace-files/SKILL.md +0 -142
- /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,436 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: execution-context
|
|
3
|
+
description: Teaches how data moves between nodes in Codemation workflows — read the immediate prior node's output from item.json (the engine put it there), read ANY earlier node's output directly from its source via ctx.data.getOutputItem<T>(NODE_ID, idx) (you never carry it forward), and reserve ctx.binary for actual file bytes. Essential for any multi-step workflow (order intake, document processing, ETL) where a later step needs a value that an intermediate node replaced. Read this before writing any node that reads data from a prior step.
|
|
4
|
+
compatibility: Applies to all Codemation workflow nodes and the Callback, MapData, defineNode, and itemExpr APIs.
|
|
5
|
+
tags: execution, context, item, json, binary, mapdata, data-passing, anti-pattern, ocr, workflow, authoring, nodes, between-steps, getoutputitem, cross-step-read, order-intake, document-processing
|
|
6
|
+
uses: "@codemation/core-nodes, @codemation/core"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Execution context — how data moves between nodes
|
|
10
|
+
|
|
11
|
+
The engine passes every node's output to the **immediate next** node as `item.json`. That covers the
|
|
12
|
+
common case. But many workflows need a value that lives **further back** — the trigger's `messageId`,
|
|
13
|
+
an early extraction field — after an intermediate node (a structured-output `AIAgent`, an OCR scanner)
|
|
14
|
+
**replaced** `item.json`. For that, you do not carry the value forward through every step. You read it
|
|
15
|
+
**directly from its source node** with `ctx.data.getOutputItem<T>(NODE_ID, idx)`.
|
|
16
|
+
|
|
17
|
+
**Core rules:**
|
|
18
|
+
|
|
19
|
+
1. **Immediate prior node's output → `item.json`.** The previous node already put its result there.
|
|
20
|
+
Read it directly.
|
|
21
|
+
2. **Any EARLIER node's output → `ctx.data.getOutputItem<T>(NODE_ID, idx)`.** Fetch it from the SOURCE
|
|
22
|
+
node by id. It does not matter what an intermediate agent did to `item.json` in between — the source
|
|
23
|
+
node's output is still recorded in the run and you read it from there. No carry-forward, no re-merge.
|
|
24
|
+
3. **`ctx.binary` is for file bytes only.** Binary attachments (PDFs, images, uploaded files) live in
|
|
25
|
+
`item.binary` and are read via `ctx.binary`. Never stash structured JSON as a fabricated binary to
|
|
26
|
+
"carry it along" — structured data belongs on `item.json`.
|
|
27
|
+
|
|
28
|
+
## Reading an earlier node's output — `ctx.data.getOutputItem`
|
|
29
|
+
|
|
30
|
+
`RunDataSnapshot` (available as `ctx.data` inside any node) exposes the recorded output of every node
|
|
31
|
+
that has already run this turn:
|
|
32
|
+
|
|
33
|
+
```ts no-check
|
|
34
|
+
getOutputItem<TJson = unknown>(nodeId: NodeId, itemIndex: number, output?: OutputPortKey): Item<TJson> | undefined
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- `nodeId` — the **id you gave the source node** (the trigger, an extraction step, …). `NodeId` is a
|
|
38
|
+
plain `string`, so a stable `const` id (e.g. `const TRIGGER_ID = "gmail-trigger"`) passes directly.
|
|
39
|
+
- `itemIndex` — the index into that node's output, aligned to the **current item's lineage**. For 1:1
|
|
40
|
+
flows pass the current index — often the loop index `idx`, or `0` at a single-item point such as the
|
|
41
|
+
trunk start. Under fan-out (one upstream item produced many, or vice versa) the indices no longer line
|
|
42
|
+
up 1:1, so align carefully.
|
|
43
|
+
- Returns `Item<TJson> | undefined` — always guard with `?.json` and a fallback.
|
|
44
|
+
- `output` defaults to `"main"`; omit it for the common case.
|
|
45
|
+
|
|
46
|
+
### Recover a clobbered field from its source node
|
|
47
|
+
|
|
48
|
+
A structured-output `AIAgent` (e.g. an Odoo sync) **replaces** `item.json` with its schema-typed object
|
|
49
|
+
— the trigger's `messageId` / email metadata is GONE from `item.json` after it. You do **not** thread
|
|
50
|
+
that metadata through every node. You read it back from the **trigger** directly:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { z } from "zod";
|
|
54
|
+
import { createWorkflowBuilder, AIAgent, CodemationChatModelConfig } from "@codemation/core-nodes";
|
|
55
|
+
import { OnNewGmailTrigger, ReplyToGmailMessage } from "@codemation/core-nodes-gmail";
|
|
56
|
+
import { itemExpr } from "@codemation/core";
|
|
57
|
+
import type { OnNewGmailTriggerItemJson } from "@codemation/core-nodes-gmail";
|
|
58
|
+
|
|
59
|
+
// Stable ids so getOutputItem can back-reference the source node by id.
|
|
60
|
+
const TRIGGER_ID = "gmail-trigger";
|
|
61
|
+
const SYNC_ID = "odoo-sync";
|
|
62
|
+
|
|
63
|
+
const model = new CodemationChatModelConfig("Managed AI", "medium");
|
|
64
|
+
|
|
65
|
+
// The Odoo-sync agent's output schema — this REPLACES item.json. No messageId here.
|
|
66
|
+
const SyncOutput = z.object({ saleOrderId: z.string(), unmatched: z.array(z.string()) });
|
|
67
|
+
type SyncOutputT = z.infer<typeof SyncOutput>;
|
|
68
|
+
|
|
69
|
+
export default createWorkflowBuilder({ id: "wf.order-sync-reply", name: "Order sync + reply" })
|
|
70
|
+
.trigger(
|
|
71
|
+
new OnNewGmailTrigger("New order email", {
|
|
72
|
+
mailbox: "me",
|
|
73
|
+
labelIds: ["orders"],
|
|
74
|
+
downloadAttachments: true,
|
|
75
|
+
}),
|
|
76
|
+
)
|
|
77
|
+
// Step 1: structured-output agent. Its { saleOrderId, unmatched } REPLACES item.json —
|
|
78
|
+
// the trigger's messageId is no longer on item.json after this point.
|
|
79
|
+
.then(
|
|
80
|
+
new AIAgent<OnNewGmailTriggerItemJson, SyncOutputT>({
|
|
81
|
+
name: "Odoo sync agent",
|
|
82
|
+
id: SYNC_ID,
|
|
83
|
+
messages: [{ role: "system", content: "Sync the order to Odoo. Respond with strict JSON." }],
|
|
84
|
+
chatModel: model,
|
|
85
|
+
outputSchema: SyncOutput,
|
|
86
|
+
}),
|
|
87
|
+
)
|
|
88
|
+
// Step 2: reply to the sender. messageId is NOT on item.json anymore — read it straight
|
|
89
|
+
// from the TRIGGER's recorded output via getOutputItem. The current item is the sync output.
|
|
90
|
+
.then(
|
|
91
|
+
new ReplyToGmailMessage(
|
|
92
|
+
"Reply to sender",
|
|
93
|
+
{
|
|
94
|
+
// ✅ Read messageId from the SOURCE node (the trigger), not from item.json.
|
|
95
|
+
messageId: itemExpr(({ ctx }) => {
|
|
96
|
+
const triggerItem = ctx.data.getOutputItem<OnNewGmailTriggerItemJson>(TRIGGER_ID, 0);
|
|
97
|
+
return triggerItem?.json.messageId ?? "";
|
|
98
|
+
}),
|
|
99
|
+
text: itemExpr(({ item }) => {
|
|
100
|
+
const sync = item.json as SyncOutputT; // current item.json IS the sync output
|
|
101
|
+
return `Your order has been processed (Odoo SO: ${sync.saleOrderId}).`;
|
|
102
|
+
}),
|
|
103
|
+
},
|
|
104
|
+
"reply-to-sender",
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
.build();
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### The same read inside a `Callback` (item-index aligned)
|
|
111
|
+
|
|
112
|
+
Inside a `Callback` you iterate items, so the `idx` from the loop is the lineage index to pass — it
|
|
113
|
+
aligns the current item with the matching trigger item in a 1:1 flow:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { Callback } from "@codemation/core-nodes";
|
|
117
|
+
import type { Items } from "@codemation/core";
|
|
118
|
+
import type { OnNewGmailTriggerItemJson } from "@codemation/core-nodes-gmail";
|
|
119
|
+
import type { DocScannerOutput } from "@codemation/core-nodes";
|
|
120
|
+
|
|
121
|
+
const TRIGGER_ID = "gmail-trigger";
|
|
122
|
+
|
|
123
|
+
// The OCR scanner already replaced item.json with { markdown, fields }. We need the email
|
|
124
|
+
// subject + body from the trigger to feed the next agent — read them from the trigger by id.
|
|
125
|
+
type RouterInput = DocScannerOutput & {
|
|
126
|
+
mailSubject?: string;
|
|
127
|
+
mailBody?: string;
|
|
128
|
+
messageId: string;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// ✅ Read the trigger item by id, using the loop idx as the lineage index (1:1 flow).
|
|
132
|
+
const mergeMailBody = new Callback<DocScannerOutput, RouterInput>(
|
|
133
|
+
"Merge mail body into item",
|
|
134
|
+
(items: Items<DocScannerOutput>, ctx) =>
|
|
135
|
+
items.map((item, idx) => {
|
|
136
|
+
const triggerItem = ctx.data.getOutputItem<OnNewGmailTriggerItemJson>(TRIGGER_ID, idx);
|
|
137
|
+
return {
|
|
138
|
+
...item,
|
|
139
|
+
json: {
|
|
140
|
+
...item.json,
|
|
141
|
+
mailSubject: triggerItem?.json.subject,
|
|
142
|
+
mailBody: triggerItem?.json.textPlain ?? triggerItem?.json.snippet,
|
|
143
|
+
messageId: triggerItem?.json.messageId ?? "",
|
|
144
|
+
} satisfies RouterInput,
|
|
145
|
+
};
|
|
146
|
+
}),
|
|
147
|
+
{ id: "merge-mail-body" },
|
|
148
|
+
);
|
|
149
|
+
export {};
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The key point: the **graph between the trigger and this node is irrelevant**. An OCR step, an agent, a
|
|
153
|
+
`Switch` — whatever ran in between and however it reshaped `item.json` — the trigger's output is still
|
|
154
|
+
recorded under `TRIGGER_ID`, and `getOutputItem` reads it from there.
|
|
155
|
+
|
|
156
|
+
## Anti-patterns vs. good patterns
|
|
157
|
+
|
|
158
|
+
### (a) A recovery cast for a field that was clobbered — read it from the source instead
|
|
159
|
+
|
|
160
|
+
When a structured-output agent or OCR node replaces `item.json`, a field you still need (e.g.
|
|
161
|
+
`messageId`) is no longer there. The **broken** fix is a phantom cast that pretends the field survived —
|
|
162
|
+
it compiles but is `undefined` at runtime because the field was clobbered and never re-fetched. The
|
|
163
|
+
clean fix is to read it from its source node with `getOutputItem`.
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
import { z } from "zod";
|
|
167
|
+
import { Callback } from "@codemation/core-nodes";
|
|
168
|
+
import type { Items } from "@codemation/core";
|
|
169
|
+
import type { OnNewGmailTriggerItemJson } from "@codemation/core-nodes-gmail";
|
|
170
|
+
|
|
171
|
+
const TRIGGER_ID = "gmail-trigger";
|
|
172
|
+
|
|
173
|
+
const SyncOutput = z.object({ saleOrderId: z.string() });
|
|
174
|
+
type SyncOutputT = z.infer<typeof SyncOutput>;
|
|
175
|
+
// The agent's output replaced item.json — messageId is NOT on it. The phantom cast below
|
|
176
|
+
// pretends otherwise: it type-checks but `messageId` is `undefined` at runtime.
|
|
177
|
+
type SyncOutputWithMessageId = SyncOutputT & { messageId: string };
|
|
178
|
+
|
|
179
|
+
// ❌ WRONG — phantom recovery cast. messageId was clobbered by the agent and never re-fetched,
|
|
180
|
+
// so item.json.messageId is undefined at runtime even though TypeScript is satisfied.
|
|
181
|
+
const badRecover = new Callback<SyncOutputT, { ack: string }>(
|
|
182
|
+
"Acknowledge",
|
|
183
|
+
(items: Items<SyncOutputT>) =>
|
|
184
|
+
items.map((item) => {
|
|
185
|
+
const clobbered = item.json as SyncOutputWithMessageId; // ← lie: messageId isn't there
|
|
186
|
+
return { json: { ack: `ack for ${clobbered.messageId}` } }; // undefined at runtime
|
|
187
|
+
}),
|
|
188
|
+
{ id: "bad-recover" },
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
// ✅ CORRECT — read messageId from its SOURCE node (the trigger) via getOutputItem.
|
|
192
|
+
const goodRecover = new Callback<SyncOutputT, { ack: string }>(
|
|
193
|
+
"Acknowledge",
|
|
194
|
+
(items: Items<SyncOutputT>, ctx) =>
|
|
195
|
+
items.map((item, idx) => {
|
|
196
|
+
const triggerItem = ctx.data.getOutputItem<OnNewGmailTriggerItemJson>(TRIGGER_ID, idx);
|
|
197
|
+
return { json: { ack: `ack for ${triggerItem?.json.messageId ?? "unknown"}` } };
|
|
198
|
+
}),
|
|
199
|
+
{ id: "good-recover" },
|
|
200
|
+
);
|
|
201
|
+
export {};
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### (b) Re-deriving the same shape repeatedly instead of reading the source
|
|
205
|
+
|
|
206
|
+
Don't recompute a value from raw inputs in every node. Compute it once and read it where you need it —
|
|
207
|
+
either from the immediate prior node's `item.json`, or, for an earlier node, from its source via
|
|
208
|
+
`getOutputItem`.
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
import { createWorkflowBuilder, ManualTrigger, MapData, Callback } from "@codemation/core-nodes";
|
|
212
|
+
import type { Items, Item } from "@codemation/core";
|
|
213
|
+
|
|
214
|
+
type Raw = { companyName: string; vatId: string; lines: Array<{ sku: string; qty: number }> };
|
|
215
|
+
type Canonical = { company: string; vat: string; lineCount: number };
|
|
216
|
+
|
|
217
|
+
// ❌ WRONG — deriving the same shape twice, in two separate nodes.
|
|
218
|
+
const badDerive1 = new Callback<Raw, { tag: string }>(
|
|
219
|
+
"Tag order",
|
|
220
|
+
(items: Items<Raw>) =>
|
|
221
|
+
items.map((item) => ({
|
|
222
|
+
json: { tag: `${item.json.companyName}-${item.json.lines.length}` }, // ← derives from raw again
|
|
223
|
+
})),
|
|
224
|
+
{ id: "bad-derive-1" },
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// ✅ GOOD — one MapData shapes the value once; the next node reads THAT shape from item.json.
|
|
228
|
+
const canonical = new MapData<Raw, Canonical>(
|
|
229
|
+
"Canonicalize",
|
|
230
|
+
(item: Item<Raw>) => ({
|
|
231
|
+
company: item.json.companyName,
|
|
232
|
+
vat: item.json.vatId,
|
|
233
|
+
lineCount: item.json.lines.length,
|
|
234
|
+
}),
|
|
235
|
+
{ id: "canonical" },
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
// Reads item.json.company / item.json.lineCount — never re-derives from raw.
|
|
239
|
+
const tagOrder = new Callback<Canonical, { tag: string }>(
|
|
240
|
+
"Tag order",
|
|
241
|
+
(items: Items<Canonical>) => items.map((item) => ({ json: { tag: `${item.json.company}-${item.json.lineCount}` } })),
|
|
242
|
+
{ id: "tag-order" },
|
|
243
|
+
);
|
|
244
|
+
export {};
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### (c) JSON stashed inside a fabricated binary instead of on item.json
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
import { createWorkflowBuilder, ManualTrigger, Callback } from "@codemation/core-nodes";
|
|
251
|
+
import type { Items, NodeExecutionContext } from "@codemation/core";
|
|
252
|
+
|
|
253
|
+
type Order = { orderId: string; total: number };
|
|
254
|
+
|
|
255
|
+
// ❌ WRONG — encoding structured JSON as bytes and attaching it as a binary is wasted ceremony.
|
|
256
|
+
// item.json already carries structured data between nodes at zero cost.
|
|
257
|
+
const badBinaryStash = new Callback<Order, Order>(
|
|
258
|
+
"Stash to binary",
|
|
259
|
+
async (items: Items<Order>, ctx: NodeExecutionContext) => {
|
|
260
|
+
const results = [];
|
|
261
|
+
for (const item of items) {
|
|
262
|
+
// Attaching structured JSON as a binary → retrieving it in the next node via ctx.binary.getJson
|
|
263
|
+
// is needless overhead when the engine just passes item.json for free.
|
|
264
|
+
const att = await ctx.binary.attach({
|
|
265
|
+
name: "order",
|
|
266
|
+
body: Buffer.from(JSON.stringify(item.json)),
|
|
267
|
+
mimeType: "application/json",
|
|
268
|
+
});
|
|
269
|
+
results.push(ctx.binary.withAttachment(item, "order", att));
|
|
270
|
+
}
|
|
271
|
+
return results;
|
|
272
|
+
},
|
|
273
|
+
{ id: "bad-binary-stash" },
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
// ✅ GOOD — structured data belongs on item.json. The engine carries it for you.
|
|
277
|
+
// Use ctx.binary ONLY for real file bytes (PDFs, images, CSVs from readWorkspaceFileNode, etc.).
|
|
278
|
+
const passThrough = new Callback<Order, Order>(
|
|
279
|
+
"Pass order",
|
|
280
|
+
(items: Items<Order>) => items.map((item) => ({ json: item.json })),
|
|
281
|
+
{ id: "pass-order" },
|
|
282
|
+
);
|
|
283
|
+
export {};
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## MapData — narrow/shape an extraction output (not a cross-step carry)
|
|
287
|
+
|
|
288
|
+
A `MapData` is still a good tool to **narrow or shape** a noisy extraction output (`{ markdown, fields }`
|
|
289
|
+
from OCR, or a raw agent object) into the tight shape the **immediate next** step wants. That is its job:
|
|
290
|
+
a clean local transform. It is **not** required for cross-step survival — a downstream node that needs an
|
|
291
|
+
earlier node's data should `getOutputItem` it from the source, not depend on it having been threaded
|
|
292
|
+
through a canonical shape.
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
import {
|
|
296
|
+
createWorkflowBuilder,
|
|
297
|
+
WebhookTrigger,
|
|
298
|
+
MapData,
|
|
299
|
+
Callback,
|
|
300
|
+
codemationDocumentScannerNode,
|
|
301
|
+
} from "@codemation/core-nodes";
|
|
302
|
+
import type { Item, Items } from "@codemation/core";
|
|
303
|
+
import type { DocScannerOutput } from "@codemation/core-nodes";
|
|
304
|
+
|
|
305
|
+
// A tight shape for the immediate next step — narrowed from the noisy OCR output.
|
|
306
|
+
type OrderFields = { vendor: string; totalAmount: number };
|
|
307
|
+
|
|
308
|
+
export default createWorkflowBuilder({ id: "wf.invoice-intake", name: "Invoice intake" })
|
|
309
|
+
.trigger(new WebhookTrigger("Receive invoice", { endpointKey: "invoice-upload", methods: ["POST"] }))
|
|
310
|
+
// Step 1: OCR — replaces item.json with { markdown, fields }.
|
|
311
|
+
.then(codemationDocumentScannerNode.create({ analyzerType: "invoice" }, "Scan invoice", "scan-invoice"))
|
|
312
|
+
// Step 2: MapData NARROWS the noisy OCR output into the shape the next step needs. This is a
|
|
313
|
+
// local convenience for the immediate next node — NOT a carry-forward of earlier-node data.
|
|
314
|
+
.then(
|
|
315
|
+
new MapData<DocScannerOutput, OrderFields>(
|
|
316
|
+
"Narrow OCR fields",
|
|
317
|
+
(item: Item<DocScannerOutput>) => ({
|
|
318
|
+
vendor: (item.json.fields["VendorName"]?.value as string | undefined) ?? "Unknown",
|
|
319
|
+
totalAmount: (item.json.fields["InvoiceTotal"]?.value as number | undefined) ?? 0,
|
|
320
|
+
}),
|
|
321
|
+
{ id: "narrow-fields" },
|
|
322
|
+
),
|
|
323
|
+
)
|
|
324
|
+
// Step 3: the immediate next node reads the narrowed shape from item.json.
|
|
325
|
+
.then(
|
|
326
|
+
new Callback<OrderFields, { summary: string }>(
|
|
327
|
+
"Summarize",
|
|
328
|
+
(items: Items<OrderFields>) =>
|
|
329
|
+
items.map((item) => ({ json: { summary: `${item.json.vendor}: €${item.json.totalAmount}` } })),
|
|
330
|
+
{ id: "summarize" },
|
|
331
|
+
),
|
|
332
|
+
)
|
|
333
|
+
.build();
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## ctx.binary — when to use it
|
|
337
|
+
|
|
338
|
+
`ctx.binary` is the API for **file bytes**. Use it when:
|
|
339
|
+
|
|
340
|
+
- A `readWorkspaceFileNode`, Gmail attachment, or webhook upload put bytes on `item.binary`.
|
|
341
|
+
- You need to read a PDF, image, CSV, or JSON file that arrived as a binary attachment.
|
|
342
|
+
|
|
343
|
+
Never use it as a workaround for passing structured data between nodes. If it's JSON, it belongs on
|
|
344
|
+
`item.json` (immediate prior) or is read via `getOutputItem` (earlier node).
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
import { Callback } from "@codemation/core-nodes";
|
|
348
|
+
import type { NodeExecutionContext } from "@codemation/core";
|
|
349
|
+
|
|
350
|
+
type FileMeta = { fileId: string; binarySlot: string };
|
|
351
|
+
type Product = { sku: string; price: number };
|
|
352
|
+
|
|
353
|
+
// ✅ Correct: ctx.binary reads actual file bytes from a slot an upstream node attached.
|
|
354
|
+
const parseFile = new Callback<FileMeta, { count: number }>(
|
|
355
|
+
"Parse product file",
|
|
356
|
+
async (items, ctx: NodeExecutionContext) => {
|
|
357
|
+
const results: Array<{ json: { count: number } }> = [];
|
|
358
|
+
for (const item of items) {
|
|
359
|
+
const attachment = item.binary?.["data"]; // bytes put there by readWorkspaceFileNode
|
|
360
|
+
if (!attachment) throw new Error('No binary at slot "data"');
|
|
361
|
+
const products = await ctx.binary.getJson<Product[]>(attachment); // bounded read + parse
|
|
362
|
+
results.push({ json: { count: products.length } });
|
|
363
|
+
}
|
|
364
|
+
return results;
|
|
365
|
+
},
|
|
366
|
+
{ id: "parse-file" },
|
|
367
|
+
);
|
|
368
|
+
export {};
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### (d) Enrichment Callback REPLACES item.json — always spread the existing payload
|
|
372
|
+
|
|
373
|
+
A `Callback`'s return value **replaces `item.json` entirely** — exactly like any other node.
|
|
374
|
+
An enrichment step (one that looks up a derived field, e.g. a matched ERP partner id) that
|
|
375
|
+
returns ONLY the new field DISCARDS the entire prior payload (companyName, lineItems,
|
|
376
|
+
contactEmail, …) that the **immediate next** node needs.
|
|
377
|
+
|
|
378
|
+
**The rule:** an enrichment `Callback` MUST return `{ ...item.json, newField }` — not just
|
|
379
|
+
`{ newField }`. (If an even earlier node's value is needed downstream, that's `getOutputItem`'s job,
|
|
380
|
+
not this spread's.)
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
import { Callback } from "@codemation/core-nodes";
|
|
384
|
+
import type { Items } from "@codemation/core";
|
|
385
|
+
|
|
386
|
+
type OrderCanonical = {
|
|
387
|
+
companyName: string;
|
|
388
|
+
contactEmail: string;
|
|
389
|
+
lineItems: Array<{ sku: string; qty: number }>;
|
|
390
|
+
};
|
|
391
|
+
type WithPartner = OrderCanonical & { partnerId: number; partnerName: string };
|
|
392
|
+
|
|
393
|
+
// ❌ WRONG — returns only the looked-up fields, replacing item.json and discarding
|
|
394
|
+
// companyName, contactEmail, lineItems that the next node needs.
|
|
395
|
+
const badEnrich = new Callback<OrderCanonical, { partnerId: number; partnerName: string }>(
|
|
396
|
+
"Enrich partner",
|
|
397
|
+
(items: Items<OrderCanonical>) =>
|
|
398
|
+
items.map((i) => ({
|
|
399
|
+
json: { partnerId: 42, partnerName: i.json.companyName }, // ← DISCARDS the rest
|
|
400
|
+
})),
|
|
401
|
+
{ id: "bad-enrich" },
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
// ✅ CORRECT — spreads the entire existing payload, THEN adds the new fields.
|
|
405
|
+
// The next node still sees companyName, contactEmail, lineItems, AND the new ids.
|
|
406
|
+
const goodEnrich = new Callback<OrderCanonical, WithPartner>(
|
|
407
|
+
"Enrich partner",
|
|
408
|
+
(items: Items<OrderCanonical>) =>
|
|
409
|
+
items.map((i) => ({
|
|
410
|
+
json: { ...i.json, partnerId: 42, partnerName: i.json.companyName },
|
|
411
|
+
})),
|
|
412
|
+
{ id: "good-enrich" },
|
|
413
|
+
);
|
|
414
|
+
export {};
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
## Summary
|
|
418
|
+
|
|
419
|
+
| Question | Answer |
|
|
420
|
+
| --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
421
|
+
| Where is the IMMEDIATE prior node's output? | `item.json` — read it directly. |
|
|
422
|
+
| How do I read an EARLIER node's output (trigger, an extraction step)? | `ctx.data.getOutputItem<T>(NODE_ID, idx)` — from the source node by id. You never carry it forward. |
|
|
423
|
+
| A field I need was clobbered by an intermediate agent/OCR — now what? | Read it from its source node via `getOutputItem`. Never a phantom recovery cast — that's `undefined` at runtime. |
|
|
424
|
+
| When do I need a MapData? | To NARROW/shape an extraction output for the IMMEDIATE next step. Not required for cross-step survival. |
|
|
425
|
+
| When do I use `ctx.binary`? | Only for file bytes (PDF, image, CSV, JSON file from a read node). |
|
|
426
|
+
| Can I JSON-encode structured data into a binary to pass it along? | No — that's waste. `item.json` carries it for free; earlier nodes via `getOutputItem`. |
|
|
427
|
+
| My enrichment Callback looks up a field — what do I return? | `{ ...item.json, newField }` — spread first, add second. Never only `{ newField }`. |
|
|
428
|
+
|
|
429
|
+
## Read next when needed
|
|
430
|
+
|
|
431
|
+
- `document-ai` — the OCR node that produces `{ markdown, fields }` (one source you may later `getOutputItem`).
|
|
432
|
+
- `ai-agent` — extraction via LLM; `outputSchema` produces a typed object that REPLACES `item.json`.
|
|
433
|
+
- `workflow-dsl` — `Callback`, `MapData`, `itemExpr`, and the full builder API.
|
|
434
|
+
- `connect-external-systems` → "Build what works; report what's missing" — if a required value (label
|
|
435
|
+
name, folder ID, record ID) is unknown at build time, use a named TODO placeholder and
|
|
436
|
+
`record_decision`; never fabricate an identifier.
|
|
@@ -1,21 +1,13 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
3
|
-
description: Explains Codemation package boundaries, runtime concepts,
|
|
4
|
-
compatibility: Designed for Codemation apps, plugins, and framework contributors.
|
|
2
|
+
name: framework-concepts
|
|
3
|
+
description: Explains Codemation package boundaries, runtime concepts, and the consumer-versus-framework mental model across @codemation/core, @codemation/host, @codemation/next-host, and @codemation/cli. Use to orient on where code belongs before picking a more specific skill.
|
|
5
4
|
tags: concepts, architecture
|
|
6
5
|
---
|
|
7
6
|
|
|
8
7
|
# Codemation Framework Concepts
|
|
9
8
|
|
|
10
|
-
## Mental model
|
|
11
|
-
|
|
12
9
|
Codemation is a workflow engine with a layered package structure. `@codemation/core` owns the engine and runtime contracts (must stay pure — no HTTP, UI, or vendor SDKs). `@codemation/host` adds persistence, credentials, APIs, and scheduler wiring. `@codemation/next-host` is the framework UI shell. `@codemation/cli` runs local development, build, and serve. Consumer apps define behavior in `codemation.config.ts` and `src/workflows/` — they never touch core internals.
|
|
13
10
|
|
|
14
|
-
## When to use / when NOT
|
|
15
|
-
|
|
16
|
-
Use this skill to orient on package ownership, runtime shape, observability boundaries, and the consumer/framework divide.
|
|
17
|
-
Do not use as a substitute for detailed CLI, workflow DSL, or plugin implementation guidance when you already know which skill you need.
|
|
18
|
-
|
|
19
11
|
## Core concepts
|
|
20
12
|
|
|
21
13
|
- **workflows** define behavior; **triggers** start runs; **nodes** process items; **items** carry `item.json` data.
|
|
@@ -27,14 +19,14 @@ Do not use as a substitute for detailed CLI, workflow DSL, or plugin implementat
|
|
|
27
19
|
|
|
28
20
|
## Where to go next
|
|
29
21
|
|
|
30
|
-
| Task | Skill
|
|
31
|
-
| ------------------------------------- |
|
|
32
|
-
| Authoring workflows | `
|
|
33
|
-
| Building a reusable node | `
|
|
34
|
-
| Building a credential type | `
|
|
35
|
-
| Packaging as a plugin | `
|
|
36
|
-
| Calling an MCP server from a workflow | `
|
|
37
|
-
| CLI commands / dev loop | `
|
|
22
|
+
| Task | Skill |
|
|
23
|
+
| ------------------------------------- | ------------------------- |
|
|
24
|
+
| Authoring workflows | `workflow-dsl` |
|
|
25
|
+
| Building a reusable node | `custom-node-development` |
|
|
26
|
+
| Building a credential type | `credential-development` |
|
|
27
|
+
| Packaging as a plugin | `plugin-development` |
|
|
28
|
+
| Calling an MCP server from a workflow | `mcp-capabilities` |
|
|
29
|
+
| CLI commands / dev loop | `cli` |
|
|
38
30
|
|
|
39
31
|
## Read next when needed
|
|
40
32
|
|