@codemation/agent-skills 0.4.0 → 0.5.2

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 (49) hide show
  1. package/CHANGELOG.md +173 -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 +18 -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 +5 -12
  21. package/skills/builder/mcp-capabilities/references/agent-with-mcp.ts +24 -0
  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 +492 -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-mcp-capabilities/references/agent-with-mcp.ts +0 -44
  43. package/skills/codemation-workflow-dsl/SKILL.md +0 -78
  44. package/skills/codemation-workflow-dsl/references/builder-patterns.md +0 -120
  45. package/skills/codemation-workflow-dsl/references/complete-example.md +0 -263
  46. package/skills/codemation-workflow-dsl/references/workflow-testing.md +0 -194
  47. package/skills/codemation-workspace-files/SKILL.md +0 -142
  48. /package/skills/{codemation-cli → builder/cli}/references/command-map.md +0 -0
  49. /package/skills/{codemation-framework-concepts → builder/framework-concepts}/references/architecture-map.md +0 -0
@@ -0,0 +1,492 @@
1
+ ---
2
+ name: workflow-dsl
3
+ description: Write a Codemation workflow definition with the fluent builder (createWorkflowBuilder().trigger().then().when().build()) and the built-in node classes from @codemation/core-nodes. Use this first whenever authoring or editing any workflow under src/workflows.
4
+ compatibility: Designed for Codemation apps and plugins that author workflows.
5
+ tags: workflow, dsl, authoring
6
+ uses: "@codemation/core-nodes, @codemation/core"
7
+ ---
8
+
9
+ # Codemation Workflow DSL
10
+
11
+ A workflow is a `default`-exported definition: a **trigger** that emits items, then **node steps** that transform them. Items are `{ json, binary? }`; each node runs **per item** in a batch. You build with the fluent chain `createWorkflowBuilder({ id, name }).trigger(...).then(...).build()` and the node classes below — that is the whole surface. The per-item type flows through `.then(...)` via generics, so annotate input/output types where inference needs help.
12
+
13
+ > **Passing data between nodes?** Read the **`execution-context`** skill FIRST — it is the one rule for
14
+ > reading `item.json`, carrying data through an OCR step, and the canonical-JSON pattern. Every workflow
15
+ > that passes structured data between nodes depends on it.
16
+
17
+ > **This is the general spine.** For a specific integration (`gmail`, `odoo`, …) or topic (reaching an
18
+ > external system → `connect-external-systems`; OCR → `document-ai`), that skill is **authoritative** —
19
+ > follow it over any general pattern here. This skill teaches triggers, flow control, and the builder API.
20
+
21
+ **Discipline:** author straight from the one-liners in this file, then run `verify_workflow` and fix only what it flags. Don't open other skills speculatively — open one only when `verify_workflow` names a node or concept this file doesn't cover.
22
+
23
+ ## Name and describe every node for a non-technical reader
24
+
25
+ Two authoring rules make the workflow legible to the business user who reviews it on the canvas:
26
+
27
+ - **Title = the business action, not the node type.** The first constructor argument is a human label.
28
+ Write what the step _does_ — `"Receive new order"`, `"Tag order as received"`, `"Route high-value orders"` —
29
+ never `"Callback"`, `"If"`, `"MapData"`. The reader should understand the flow from titles alone.
30
+ - **`description` = one plain-language sentence** in the node's **options** (alongside `id`). It is a
31
+ first-class option on EVERY node — `{ id: "...", description: "..." }` — and renders in the node
32
+ sidebar. One non-technical sentence: what this step does and why. **Every node gets one — no
33
+ exceptions.** That means the **trigger** AND every step, including the ones easy to forget under a
34
+ big build: the trigger / test-trigger, document / PDF-scan (OCR) nodes, AI-agent nodes, and any
35
+ send / notify / test-notification step. A node with no `description` is a build defect — **before
36
+ you call the build done, re-read the whole workflow and confirm every single node has both a
37
+ friendly title and a `description`.**
38
+
39
+ ```ts no-check
40
+ new Callback("Tag order as received", markReceived, {
41
+ id: "mark-received",
42
+ description: "Flags the order as received so the warehouse can pick it.",
43
+ });
44
+ ```
45
+
46
+ `description` lives in the same options object as `id`. For bare-id nodes (`If`, `Filter`, `Split`,
47
+ `Switch`, `Merge`, `Wait`, …) the last argument accepts either a bare `"id"` string OR
48
+ an options object `{ id, description }` — pass the object so you can describe the node.
49
+
50
+ ## A minimal complete workflow
51
+
52
+ ```typescript
53
+ import { createWorkflowBuilder, WebhookTrigger, Callback } from "@codemation/core-nodes";
54
+ import type { Items } from "@codemation/core";
55
+
56
+ type Order = { orderId: string; total: number };
57
+
58
+ export default createWorkflowBuilder({ id: "wf.order-webhook", name: "Order webhook" })
59
+ .trigger(
60
+ new WebhookTrigger(
61
+ "Receive new order",
62
+ { endpointKey: "orders", methods: ["POST"] },
63
+ undefined, // default handler
64
+ { id: "receive-order", description: "Accepts an order POSTed by the shop and starts the flow." },
65
+ ),
66
+ )
67
+ .then(
68
+ new Callback(
69
+ "Tag order as received",
70
+ (items: Items<Order>) => {
71
+ return items.map((item) => ({ json: { ...item.json, received: true } }));
72
+ },
73
+ { id: "log-order", description: "Marks each incoming order as received." },
74
+ ),
75
+ )
76
+ .build();
77
+ ```
78
+
79
+ ## The fluent DSL
80
+
81
+ - `createWorkflowBuilder({ id, name })` → builder. `id` must be a `WorkflowId` (a `wf.*` string is fine).
82
+ - `.trigger(triggerConfig)` → cursor. Exactly one trigger, first.
83
+ - `.then(nodeConfig)` → appends a step; the chain's item type becomes that node's output.
84
+ - `.when(true, [steps]).when(false, [steps])` (boolean form) → branches off an `If` node's `"true"`/`"false"` ports. **It auto-merges when you continue the chain:** a following `.then(...)` / `.humanApproval(...)` connects EVERY branch tail into the next node (the same fan-in the object form produces). End it with `.build()` when the branch is the end of the flow.
85
+ - The **object form** `.when({ true: [...], false: [...] })` is the inline alternative → it merges both branch tails into one chainable cursor with a precise merged item type. Reach for it when you want the continuation typed exactly. (Keep only one branch alive — e.g. "drop non-orders, continue orders"? Use `.route({ true: (b) => b.then(...), false: () => undefined })`: a factory returning `undefined` is excluded from the merge.)
86
+ - `.build()` → validates node ids (throws `WorkflowDefinitionError` on empty/duplicate) and returns the definition.
87
+
88
+ **Filter vs If — choose this up front, it's the most common rewrite.** To KEEP ONLY the items that match a condition and DROP the rest — "only real orders", "only paid invoices", "skip anything that isn't an order" — use a **`Filter`** node (see "Per-item transforms" below): it's ONE node, the dropped items simply don't continue, and the chain stays a single trunk. Use **`If` + `.when`** ONLY when the true and false items do DIFFERENT downstream work (true → send for approval, false → auto-approve). Rule of thumb: if one side would just drop the item and the other continues, that is a `Filter`, not an `If`. Reaching for `If` to express "keep only X" and then continuing the trunk is the classic shape that has to be rewritten into a `Filter` — start with the `Filter`.
89
+
90
+ Branch off an `If` with `.when`. Each step's predicate/mapper sees an `Item<T>` — read `item.json`:
91
+
92
+ ```typescript
93
+ import { createWorkflowBuilder, ManualTrigger, If, Callback } from "@codemation/core-nodes";
94
+ import type { Item, Items } from "@codemation/core";
95
+
96
+ type Invoice = { amount: number };
97
+
98
+ export default createWorkflowBuilder({ id: "wf.approve", name: "Approve invoice" })
99
+ .trigger(new ManualTrigger<Invoice>("Start", undefined, { id: "start", description: "Run this by hand." }))
100
+ .then(
101
+ new If<Invoice>("Does it need approval?", (item: Item<Invoice>) => item.json.amount > 1000, {
102
+ id: "needs-approval",
103
+ description: "Sends invoices over €1,000 for manual sign-off.",
104
+ }),
105
+ )
106
+ .when(true, [
107
+ new Callback("Send for approval", (items: Items<Invoice>) => items, {
108
+ id: "send-approval",
109
+ description: "Routes the invoice to a reviewer.",
110
+ }),
111
+ ])
112
+ .when(false, [
113
+ new Callback("Auto-approve", (items: Items<Invoice>) => items, {
114
+ id: "auto-approve",
115
+ description: "Approves small invoices automatically.",
116
+ }),
117
+ ])
118
+ .build();
119
+ ```
120
+
121
+ **Continuing AFTER a branch** (the common shape: branch, then a shared tail like a human-approval gate).
122
+ Just keep chaining — a `.then(...)` / `.humanApproval(...)` after the boolean `.when(...).when(...)` chain
123
+ auto-merges EVERY branch tail into the next node. Here a HITL gate sits on the merged trunk:
124
+
125
+ ```typescript
126
+ import { createWorkflowBuilder, ManualTrigger, If, MapData, inboxApproval } from "@codemation/core-nodes";
127
+ import type { Item } from "@codemation/core";
128
+
129
+ type Order = { amount: number; priority: string };
130
+
131
+ export default createWorkflowBuilder({ id: "wf.order", name: "Order intake" })
132
+ .trigger(new ManualTrigger<Order>("Start", undefined, { id: "start", description: "Run this by hand." }))
133
+ .then(
134
+ new If<Order>("Is this a big order?", (item: Item<Order>) => item.json.amount > 1000, {
135
+ id: "big-gate",
136
+ description: "Splits large orders from normal ones.",
137
+ }),
138
+ )
139
+ // Boolean branches auto-merge on continuation: BOTH tails feed the next step.
140
+ // (The object form `.when({ true: [...], false: [...] })` is the inline alternative
141
+ // when you want the merged item type typed exactly.)
142
+ .when(true, [
143
+ new MapData<Order, Order>("Mark as high priority", (item: Item<Order>) => ({ ...item.json, priority: "high" }), {
144
+ id: "mark-high",
145
+ description: "Tags big orders as high priority.",
146
+ }),
147
+ ])
148
+ .when(false, [
149
+ new MapData<Order, Order>(
150
+ "Mark as normal priority",
151
+ (item: Item<Order>) => ({ ...item.json, priority: "normal" }),
152
+ {
153
+ id: "mark-normal",
154
+ description: "Tags regular orders as normal priority.",
155
+ },
156
+ ),
157
+ ])
158
+ // The merged trunk keeps flowing — `.humanApproval(node, config)` is sugar for `.then(node.create(config))`.
159
+ // Pass every config field (Zod defaults don't relax the TS type).
160
+ .humanApproval(inboxApproval, {
161
+ title: "Approve order?",
162
+ body: "Review before processing.",
163
+ priority: "normal",
164
+ timeout: "48h",
165
+ onTimeout: "halt",
166
+ })
167
+ .build();
168
+ ```
169
+
170
+ ## Core nodes — one-liners
171
+
172
+ Import every node from `@codemation/core-nodes`. Each construction below is complete.
173
+
174
+ ### Triggers (exactly one, first)
175
+
176
+ ```typescript
177
+ import { ManualTrigger, WebhookTrigger, TestTrigger, schedulePollingTrigger } from "@codemation/core-nodes";
178
+
179
+ // Schedule — fires one tick item on every poll cycle (defaults to every 60s). Emits { firedAt, tick }.
180
+ // It's a defined node: build it with `.create(config, label, { id, description })`, not `new`.
181
+ const onSchedule = schedulePollingTrigger.create({}, "Run on schedule", {
182
+ id: "daily",
183
+ description: "Runs the flow on a recurring schedule.",
184
+ });
185
+
186
+ // Manual — for runs you start by hand; optionally seed default items (object/array in arg 2),
187
+ // then put { id, description } in the trailing slot (use arg 2 = undefined when seeding nothing).
188
+ const manual = new ManualTrigger<{ q: string }>("Start by hand", [{ q: "hello" }], {
189
+ id: "manual",
190
+ description: "Lets an operator run the flow on demand.",
191
+ });
192
+
193
+ // Webhook — endpointKey is the URL path segment; methods are the accepted verbs.
194
+ // Pass a Zod schema as `inputSchema` to validate the request body. The default handler is fine,
195
+ // so options go in the trailing (4th) argument.
196
+ const hook = new WebhookTrigger("Receive inbound request", { endpointKey: "inbound", methods: ["POST"] }, undefined, {
197
+ id: "hook",
198
+ description: "Starts the flow when an external system calls in.",
199
+ });
200
+
201
+ // Test — one item per test case; drives the Tests tab. generateItems is an async generator.
202
+ // TestTrigger takes `description` directly in its single options object.
203
+ const testCases = new TestTrigger<{ subject: string }>({
204
+ name: "Replay sample mails",
205
+ description: "Feeds saved sample emails through the flow for the Tests tab.",
206
+ async *generateItems() {
207
+ yield { json: { subject: "RFQ #14" } };
208
+ },
209
+ });
210
+ ```
211
+
212
+ ### Flow control
213
+
214
+ `If` and `IsTestRun` route on ports `"true"` / `"false"` — branch them with `.when`, never a bare `.then`. `Switch` exposes one port per case key.
215
+
216
+ ```typescript
217
+ import { If, Switch, Merge, NoOp, IsTestRun } from "@codemation/core-nodes";
218
+ import type { Item } from "@codemation/core";
219
+
220
+ type Doc = { kind: string; urgent: boolean };
221
+
222
+ const gate = new If<Doc>("Is it urgent?", (item: Item<Doc>) => item.json.urgent, {
223
+ id: "urgent-gate",
224
+ description: "Sends urgent documents down the fast path.",
225
+ });
226
+
227
+ const route = new Switch<Doc>(
228
+ "Route by document type",
229
+ {
230
+ cases: ["invoice", "order"],
231
+ defaultCase: "other",
232
+ resolveCaseKey: (item: Item<Doc>) => item.json.kind,
233
+ },
234
+ { id: "route-by-kind", description: "Sends each document to the handler for its type." },
235
+ );
236
+
237
+ const merge = new Merge<Doc>(
238
+ "Rejoin branches",
239
+ { mode: "passThrough" },
240
+ {
241
+ id: "rejoin",
242
+ description: "Brings the branches back into one stream.",
243
+ },
244
+ );
245
+ const passthrough = new NoOp<Doc>("Checkpoint", { id: "marker", description: "A no-op marker step." });
246
+ const testGate = new IsTestRun<Doc>("Is this a test run?", {
247
+ id: "test-gate",
248
+ description: "Routes test runs away from live side effects.",
249
+ }); // ports "true"/"false"
250
+ ```
251
+
252
+ ### Data shaping
253
+
254
+ Predicates and mappers receive an `Item<T>` (read `item.json`); `Aggregate`/`Split` see the whole batch / one item.
255
+
256
+ ```typescript
257
+ import { MapData, Filter, Aggregate, Split } from "@codemation/core-nodes";
258
+ import type { Item, Items } from "@codemation/core";
259
+
260
+ type Row = { price: number; tags: string[] };
261
+
262
+ const enrich = new MapData<Row, Row & { withVat: number }>(
263
+ "Add VAT to each line",
264
+ (item: Item<Row>) => ({ ...item.json, withVat: item.json.price * 1.21 }),
265
+ { id: "add-vat", description: "Adds 21% VAT to every row." },
266
+ );
267
+
268
+ const keep = new Filter<Row>("Keep cheap items only", (item: Item<Row>) => item.json.price < 100, {
269
+ id: "cheap-only",
270
+ description: "Drops anything priced €100 or more.",
271
+ });
272
+
273
+ const total = new Aggregate<Row, { sum: number }>(
274
+ "Total the prices",
275
+ (items: Items<Row>) => ({ sum: items.reduce((acc, i) => acc + i.json.price, 0) }),
276
+ { id: "sum-prices", description: "Adds all the row prices into one total." },
277
+ );
278
+
279
+ const fanout = new Split<Row, string>("Split into one item per tag", (item: Item<Row>) => item.json.tags, {
280
+ id: "per-tag",
281
+ description: "Emits one item for each tag on a row.",
282
+ });
283
+ ```
284
+
285
+ ### Fan-out & fan-in (important)
286
+
287
+ A node **fans out** when its `execute` returns an **array**: the engine emits **one item per element**, so
288
+ every later `.then(...)` runs **once per element** (zero elements ⇒ zero items downstream — branch on
289
+ "no matches"). This is automatic — you do **not** wrap the result yourself. Because of this, a fan-out
290
+ node must declare its output type as the **per-item element**, not the array: `.then()` carries that
291
+ element type forward as the next node's `item.json`. (e.g. a search node that returns rows declares its
292
+ output as one row; downstream `item.json` is one row.)
293
+
294
+ - **`Split<T, E>`** — fan out an array that lives **inside** one item's json (`item.json.tags`), vs a node
295
+ whose `execute` returns the array directly. Both produce one item per element.
296
+ - **`Aggregate<T, O>`** — **fan in**: collapse the whole batch back to a single item (sum, group, build one payload).
297
+ - `.then(...)` does **not** auto-unwrap — the chain's item type is whatever the upstream node _declares_ as its
298
+ per-item output. If a node declares the array as its output, nothing chains cleanly after it; declare the element.
299
+
300
+ ### HTTP
301
+
302
+ `HttpRequest` calls an external API. For `body.kind: "json"` pass the object in `body.data` (encoded once). Declare auth with `credentialSlot` — that auto-wires the bindable slot (see Credentials). Output is response metadata (`{ status, ok, json, text, ... }`), not the input item.
303
+
304
+ ```typescript
305
+ import { HttpRequest } from "@codemation/core-nodes";
306
+
307
+ const post = new HttpRequest("Create the record in the API", {
308
+ method: "POST",
309
+ url: "https://api.example.com/v1/records",
310
+ headers: { "content-type": "application/json" },
311
+ body: { kind: "json", data: { name: "ACME" } },
312
+ credentialSlot: "example",
313
+ responseFormat: "json",
314
+ id: "create-record",
315
+ description: "Sends the record to the external API.",
316
+ });
317
+ ```
318
+
319
+ ### Glue
320
+
321
+ `Callback` is the escape hatch for arbitrary per-batch logic; `Wait` pauses.
322
+
323
+ ```typescript
324
+ import { Callback, Wait } from "@codemation/core-nodes";
325
+ import type { Items } from "@codemation/core";
326
+
327
+ type Msg = { text: string };
328
+
329
+ const shout = new Callback<Msg>(
330
+ "Shout the message",
331
+ (items: Items<Msg>) => items.map((i) => ({ json: { text: i.json.text.toUpperCase() } })),
332
+ { id: "uppercase", description: "Uppercases every message." },
333
+ );
334
+
335
+ const pause = new Wait("Cool down before retry", 5000, {
336
+ id: "cooldown",
337
+ description: "Waits 5 seconds before continuing.",
338
+ });
339
+ ```
340
+
341
+ ## Binary payloads in a Callback
342
+
343
+ `item.binary[key]` is **metadata** (`BinaryAttachment`: id, storageKey, mimeType, size — there is **no `.data`**). Bytes come only from `ctx.binary` methods. **Never return `{ binary: { key: { data: Buffer } } }` — use `ctx.binary.attach` + `ctx.binary.withAttachment`.**
344
+
345
+ Reading bytes: `ctx.binary.getJson<T>(attachment)` (also `getBytes` / `getText`) — pass the `BinaryAttachment` object, not the key string.
346
+
347
+ Attaching bytes to an item: `ctx.binary.attach({ name, body, mimeType })` returns a `BinaryAttachment`; then `ctx.binary.withAttachment(item, name, att)` slots it onto the item.
348
+
349
+ ```typescript
350
+ import { Callback } from "@codemation/core-nodes";
351
+ import type { Items, NodeExecutionContext, BinaryAttachment } from "@codemation/core";
352
+
353
+ type Doc = { filename: string };
354
+ type Parsed = { wordCount: number };
355
+
356
+ // Reading bytes from an upstream binary slot:
357
+ const readBinary = new Callback<Doc, Parsed>(
358
+ "Count words in the file",
359
+ async (items: Items<Doc>, ctx: NodeExecutionContext) => {
360
+ return await Promise.all(
361
+ items.map(async (item) => {
362
+ const att: BinaryAttachment | undefined = item.binary?.["data"];
363
+ if (!att) return { json: { wordCount: 0 } };
364
+ const text = await ctx.binary.getText(att); // also: getBytes / getJson<T>
365
+ return { json: { wordCount: text.split(/\s+/).length } };
366
+ }),
367
+ );
368
+ },
369
+ { id: "count-words", description: "Counts the words in each attached file." },
370
+ );
371
+
372
+ // Attaching bytes to an item:
373
+ const attachBinary = new Callback<Doc, Doc>(
374
+ "Attach the processed file",
375
+ async (items: Items<Doc>, ctx: NodeExecutionContext) => {
376
+ return await Promise.all(
377
+ items.map(async (item) => {
378
+ const body = Buffer.from("processed content");
379
+ const att = await ctx.binary.attach({ name: "result", body, mimeType: "text/plain" });
380
+ return ctx.binary.withAttachment(item, "result", att);
381
+ // item.binary["result"] is now a BinaryAttachment — pass it to downstream nodes
382
+ }),
383
+ );
384
+ },
385
+ { id: "attach-binary", description: "Saves the processed file onto the item." },
386
+ );
387
+ ```
388
+
389
+ ## Credentials in a workflow (builder)
390
+
391
+ A binding is keyed by `(workflowId, nodeId, slotKey)`, so the node that **declares** the slot is the node the credential binds to. You declare + use; the concierge binds (separate skill).
392
+
393
+ **Default — `HttpRequest.credentialSlot`.** The string declares a bindable slot and authenticates the request; nothing else needed:
394
+
395
+ ```typescript
396
+ import { HttpRequest } from "@codemation/core-nodes";
397
+
398
+ const fetch = new HttpRequest("List contacts from the ERP", {
399
+ url: "https://erp.example.com/api/contacts",
400
+ credentialSlot: "erp", // bindable slot "erp" on this node
401
+ responseFormat: "json",
402
+ id: "list-contacts",
403
+ description: "Reads the contact list from the ERP.",
404
+ });
405
+ ```
406
+
407
+ **Never `fetch` in a `Callback`.** Raw `fetch`/HTTP/JSON-RPC inside a `Callback` is FORBIDDEN and the
408
+ build gate rejects it. To call an external system, in priority order: a specialized integration node
409
+ (e.g. the `odoo` / `gmail` nodes — search for it), an MCP-backed agent, `defineRestNode` (`rest-node`
410
+ skill), or the `HttpRequest` node above. These declare the credential slot for you. `ctx.getCredential`
411
+ in a `Callback` is only for non-HTTP uses (handing a token to a node SDK, signing a payload) — not for
412
+ making the request yourself. See `connect-external-systems`.
413
+
414
+ A plain `Callback` slot is **not** discoverable for binding. For credentialed custom logic that must surface a bindable slot, author a `defineNode` node with a `credentials` map (see the custom-node-development skill) — that node both declares and resolves the slot.
415
+
416
+ ## Error handling
417
+
418
+ Pass a `retryPolicy` to retry transient failures; an unhandled `throw` fails the run. `verify_workflow` is the build gate — write, verify, fix what it names, repeat.
419
+
420
+ ```typescript
421
+ import { HttpRequest } from "@codemation/core-nodes";
422
+ import type { RetryPolicySpec } from "@codemation/core";
423
+
424
+ const retry: RetryPolicySpec = { kind: "fixed", maxAttempts: 3, delayMs: 1000 };
425
+
426
+ const flaky = new HttpRequest(
427
+ "Ping the flaky API",
428
+ { url: "https://api.example.com/ping", id: "flaky-api", description: "Pings an API that sometimes fails." },
429
+ retry,
430
+ );
431
+ ```
432
+
433
+ ## One realistic complete example
434
+
435
+ Schedule → authenticated GET via the **HttpRequest node** (never `fetch`) → fan the response array into one
436
+ item per lead → shape each. To WRITE each lead back, add another `HttpRequest` (or a `defineRestNode`) —
437
+ not a hand-rolled `fetch`. Each node has a stable explicit `id`, a business-action title, and a
438
+ plain-language `description`.
439
+
440
+ ```typescript
441
+ import { createWorkflowBuilder, schedulePollingTrigger, HttpRequest, Split, MapData } from "@codemation/core-nodes";
442
+ import type { HttpRequestOutputJson } from "@codemation/core-nodes";
443
+ import type { Item } from "@codemation/core";
444
+
445
+ type Lead = { id: string; email: string };
446
+
447
+ export default createWorkflowBuilder({ id: "wf.sync-leads", name: "Sync leads on a schedule" })
448
+ .trigger(
449
+ schedulePollingTrigger.create({}, "Run on schedule", {
450
+ id: "nightly",
451
+ description: "Runs the lead sync on a recurring schedule.",
452
+ }),
453
+ )
454
+ // Authenticated request via the HttpRequest node — it declares the "crm" credential slot for you.
455
+ .then(
456
+ new HttpRequest("Fetch new leads from the CRM", {
457
+ url: "https://crm.example.com/api/leads",
458
+ credentialSlot: "crm",
459
+ responseFormat: "json",
460
+ id: "fetch-leads",
461
+ description: "Pulls the latest leads from the CRM.",
462
+ }),
463
+ )
464
+ // Fan out the response array into one item per lead.
465
+ .then(
466
+ new Split<HttpRequestOutputJson, Lead>(
467
+ "Split into one item per lead",
468
+ (item: Item<HttpRequestOutputJson>) => (item.json.json as Lead[]) ?? [],
469
+ { id: "per-lead", description: "Processes each lead on its own." },
470
+ ),
471
+ )
472
+ // Shape each lead. To upsert it, add another HttpRequest (method: "PUT") or a defineRestNode — not fetch.
473
+ .then(
474
+ new MapData<Lead, Lead & { queued: boolean }>(
475
+ "Mark each lead as queued",
476
+ (item: Item<Lead>) => ({ ...item.json, queued: true }),
477
+ { id: "queue-upsert", description: "Flags each lead ready to write back." },
478
+ ),
479
+ )
480
+ .build();
481
+ ```
482
+
483
+ ## Gotchas
484
+
485
+ - **Unique, stable node ids.** Set an explicit `id` on every node. Without one, the engine slugifies the `name`, so two same-named nodes collide and `.build()` throws — and renaming a label re-keys credential bindings.
486
+ - **Friendly title + `description` on every node.** Title = the business action a non-tech reader recognizes; `description` = one plain sentence in the node's options (`{ id, description }`). Both render in the canvas / sidebar.
487
+ - **Per-item execution.** Nodes run once per item in the batch. `MapData`/`Filter`/`If`/`Split` see one `Item<T>`; `Callback`/`Aggregate` see the whole `Items<T>`.
488
+ - **`If` / `IsTestRun` route on ports.** Branch their `"true"`/`"false"` ports with `.when`, not a bare `.then`. `Switch` ports are its case keys.
489
+ - **Binary payloads never go on `item.json`.** Store bytes via `ctx.binary.attach(...)` and pass the `Item.binary` slot through — never base64 on JSON.
490
+ - **Boolean `.when` branches auto-merge on continuation.** `.when(true,[...]).when(false,[...]).then(next)` connects EVERY branch tail into `next` (the same fan-in the object form gives). End on `.build()` when the branch is the flow's end. The continued cursor is typed as the pre-branch item type — use the **object form** `.when({ true:[...], false:[...] })` when you need the merged type typed exactly, or `.route({...})` to drop a branch (return `undefined`).
491
+ - **Branches must rejoin compatible types.** When two `.when` branches feed a shared downstream node, both must emit the type that node accepts.
492
+ - **Talking to an external system?** Follow the routing hierarchy in `connect-external-systems` — prefer a specialized node, MCP server, or `defineRestNode` over a hand-rolled `fetch` in a `Callback`.