@codemation/agent-skills 0.1.10 → 0.3.0

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.
@@ -1,88 +1,78 @@
1
1
  ---
2
2
  name: codemation-workflow-dsl
3
- description: Guides Codemation workflow authoring with the fluent Workflow DSL. Use when creating or updating `workflow("...")` definitions, triggers, `.map(...)`, `.node(...)`, branch flow, item handling, or `.build()` chains in `src/workflows`.
4
- compatibility: Designed for Codemation apps and plugins that author workflows with the fluent DSL.
3
+ description: Guides Codemation workflow authoring. Use when creating or updating workflow definitions in `src/workflows` — manual-trigger flows via `workflow("...").manualTrigger(...)`, or cron/webhook/other triggers via `createWorkflowBuilder({id, name}).trigger(...)`.
4
+ compatibility: Designed for Codemation apps and plugins that author workflows.
5
+ tags: workflow, dsl, authoring
6
+ uses: "@codemation/core-nodes, @codemation/host"
5
7
  ---
6
8
 
7
9
  # Codemation Workflow DSL
8
10
 
9
- ## Use this skill when
11
+ ## Mental model
10
12
 
11
- Use this skill for authoring or reviewing workflow definitions built with `workflow("...")`.
13
+ A workflow definition describes how items move from a trigger through downstream node steps. Items carry data in `item.json`; earlier outputs are available through `ctx.data`. Activations are batch-shaped but most node steps execute per-item. Every workflow definition finishes with `.build()`, which validates node ids and emits a `WorkflowDefinitionError` on collision or empty id.
12
14
 
13
- Do not use this skill for CLI-only troubleshooting or deep host architecture questions unless they directly affect workflow authoring.
15
+ ## When to use / when NOT
14
16
 
15
- ## Core mental model
17
+ Use this skill when authoring or reviewing workflow definitions under `src/workflows/`.
18
+ Do not use for CLI-only troubleshooting or deep host architecture questions unless they directly affect workflow authoring.
16
19
 
17
- 1. A workflow definition describes how items move from a trigger through downstream steps.
18
- 2. The fluent authoring chain is the normal starting point for Codemation apps.
19
- 3. Finish fluent workflow definitions with `.build()`.
20
- 4. Activations are **batch-shaped** (`Items`); many steps use **per-item** execution (`execute`, including helper **`defineNode`**) with optional **`inputSchema`** and **`itemExpr`** on config fields. Batch reshape steps (split/filter/aggregate, **`defineBatchNode`**) work on the whole batch.
21
- 5. Fluent callback helpers follow the runtime item contract: `.map(...)`, `.if(...)`, and `.switch({ resolveCaseKey })` receive `(item, ctx)`, so row fields live under `item.json` and earlier completed outputs are available through `ctx.data`.
22
-
23
- ## Authoring rules
24
-
25
- 1. Prefer the fluent `workflow(...)` chain for app-local workflow files.
26
- 2. Keep workflow files focused on orchestration and named steps.
27
- 3. Use custom nodes when a callback grows into reusable product logic.
28
- 4. Distinguish **batch activations** from **per-item node bodies**: custom nodes from **`defineNode`** implement **`execute`** per item unless you chose **`defineBatchNode`** for batch **`run`**.
29
-
30
- ## Node ids and stability
31
-
32
- Every node in a workflow definition has an `id`. When no explicit `id:` is given, `WorkflowBuilder` derives one by slugifying the node's `name` label: lowercase, non-alphanumeric runs replaced with `-`, trimmed. `"Send Email"` becomes `"send-email"`.
33
-
34
- `.build()` throws `WorkflowDefinitionError` if any node ends up with an empty id (blank label and no explicit `id`) or if two nodes share the same id. The check covers agent connection children (model + tools) as well.
35
-
36
- For nodes that hold credential bindings, the binding is keyed by `(workflowId, nodeId, slotKey)`. Renaming a node's label changes its slug-derived id and orphans the binding — the operator must re-attach the credential in the UI. Prefer stable labels or set an explicit `id:` on credential-using nodes:
20
+ ## Quickstart pick API by trigger type
37
21
 
38
22
  ```ts
39
- .node("Send notification", SendEmailNodeConfig, {
40
- id: "send-notification", // stable even if the label is later renamed
41
- // ...
42
- })
23
+ // Manual trigger — full fluent sugar (.map, .if, .switch, .agent, .node, .then)
24
+ import { workflow } from "@codemation/host";
25
+ export default workflow("wf.example")
26
+ .manualTrigger("Start", {
27
+ /* seed items */
28
+ })
29
+ .map(/* ... */)
30
+ .build();
31
+
32
+ // Cron / webhook / any other trigger — low-level .then(new NodeConfig(...)) only
33
+ import { createWorkflowBuilder, CronTrigger } from "@codemation/core-nodes";
34
+ export default createWorkflowBuilder({ id: "wf.example", name: "Example" })
35
+ .trigger(new CronTrigger("Daily", { schedule: "0 9 * * *", timezone: "UTC" }))
36
+ .then(/* new SomeNodeConfig(...) */)
37
+ .build();
43
38
  ```
44
39
 
45
- ## Typical flow
40
+ For full patterns — multi-step pipelines, branching, SubWorkflow, binary, agent tools, TestTrigger, and complete working examples — use your harness's example-discovery tool: `find_examples({ query: "..." })`. Useful queries: `"CronTrigger"`, `"if branch"`, `"AIAgent multi-step"`, `"SubWorkflow binary"`, `"TestTrigger assertion"`.
41
+
42
+ ## Decision branches & gotchas
46
43
 
47
- 1. Start with `workflow("wf.example.id")`.
48
- 2. Name the workflow with `.name(...)`.
49
- 3. Add a trigger such as `.manualTrigger(...)` or `builder.trigger(new CronTrigger(...))`.
50
- 4. Add transformations or nodes in execution order.
51
- 5. End with `.build()`.
44
+ **Two authoring APIs — pick by trigger type.** `workflow("id").manualTrigger(...)` returns a `WorkflowChain` with full fluent helpers (`.map`, `.if`, `.switch`, `.split`, `.agent`, `.node`). `createWorkflowBuilder({id, name}).trigger(new XxxTrigger(...))` returns a `ChainCursor` whose only chain method is `.then(new NodeConfig(...))`. Do NOT call `.trigger(...)` on the `workflow(...)` builder — it doesn't exist there.
52
45
 
53
- ## Built-in triggers
46
+ **Node ids and stability.** When no explicit `id:` is given, the engine slugifies the node's `name` label (lowercase, non-alphanumeric → `-`). `"Send Email"` → `"send-email"`. Nodes sharing credential bindings use `(workflowId, nodeId, slotKey)` as the binding key — renaming a label orphans the binding. **Set explicit `id:` on every credential-using node.** `.build()` throws `WorkflowDefinitionError` on empty or duplicate ids.
54
47
 
55
- - **`ManualTrigger`** one-shot manual run, optionally seeded with default items. Use `.manualTrigger(name, items?)` on the fluent builder.
56
- - **`WebhookTrigger`** — fires on an incoming HTTP request. Construct with `new WebhookTrigger(name, { endpointKey, methods })` and attach with `builder.trigger(...)`.
57
- - **`CronTrigger`** — fires on a cron schedule. Construct with `new CronTrigger(name, { schedule, timezone? })` and attach with `builder.trigger(...)`. The expression is validated at workflow build time. Each tick emits one item: `{ firedAt: string, scheduledFor: string }` (both ISO-8601). Defaults to UTC — always supply `timezone` for DST-sensitive schedules.
48
+ **Id collision pitfall.** A manual-trigger label and a downstream agent label that share the same string both slugify to the same id — `.build()` throws. Fix: add `id: "...-agent"` to disambiguate.
58
49
 
59
- ## Agent tools (callable helpers)
50
+ **Collection nodes** use `.then(node.create(...))` instead of `.node(label, node, opts)` — TypeScript can't infer the `ParamDeep` constraint via the fluent helper. See `find_examples({ query: "collection crud" })`.
60
51
 
61
- - For **inline** agent tools in workflow files (no separate `@tool()` class), use **`callableTool(...)`** from `@codemation/core`: supply `name`, Zod `inputSchema` / `outputSchema`, and `execute({ input, item, ctx, ... })`. **`CallableToolFactory.callableTool(...)`** is the same implementation if you prefer the factory style.
62
- - Prefer **plugin `Tool` classes** when the tool is reusable across packages; use **`AgentToolFactory.asTool(...)`** when exposing an existing runnable node to the agent.
52
+ **Install state in example results.** Every `find_examples` result includes `installed: boolean` and `requiresInstall: string[]`. If `installed` is `false` or `requiresInstall` is non-empty, call `install_package` for each missing package before writing any workflow code that imports them.
63
53
 
64
- ## Workflow agent authoring
54
+ **When no example matches — self-solving fallback chain.**
65
55
 
66
- - Use `.agent(...)` for fluent workflow-defined agent steps.
67
- - Define agent messages with `messages`, not a workflow-specific prompt shortcut.
68
- - Use a static `messages` array for fixed prompts.
69
- - Use `itemExpr(...)` when agent messages depend on the current item.
70
- - Use fluent `.map((item, ctx) => ...)` when workflow data itself needs reshaping before the agent step.
71
- - `model` may be a provider string such as `"openai:gpt-4o-mini"` or a `ChatModelConfig`.
56
+ 1. Retry with intent variations (different verb, more generic term).
57
+ 2. For HTTP APIs: `find_examples({ query: "defineRestNode" })` covers basic and credential-slotted REST.
58
+ 3. For one-shot inline HTTP: `find_examples({ query: "HttpRequest" })`.
59
+ 4. For non-HTTP custom logic: `find_examples({ query: "defineNode template" })`.
60
+ Do NOT ask the user to pick between primitives — they can't help; use the chain. Do NOT grep `node_modules/@codemation/*` for node implementations examples are authoritative. Surface the technique used in your reply.
72
61
 
73
- ## Workflow testing nodes
62
+ **Workflow testing.** Three built-in nodes from `@codemation/core-nodes`: `TestTrigger` (yields one item per test case), `IsTestRun` (routes `true`/`false` by `ctx.testContext`), `Assertion` (emits `AssertionResult[]`, sets `emitsAssertions: true`). See `references/workflow-testing.md` for authoring details.
74
63
 
75
- Codemation ships first-class **workflow tests**: each test case is one full workflow run, persisted with assertion records. Three nodes from `@codemation/core-nodes`:
64
+ **SubWorkflow binary.** `item.binary` slots pass transparently through SubWorkflow boundaries in both directions no special config needed. Both runs share the same `BinaryStorage` singleton.
76
65
 
77
- 1. **`TestTrigger`** drop alongside live triggers. Author callback `generateItems(ctx)` returns an `AsyncIterable<Item>`; the orchestrator dispatches one workflow run per yielded item with `executionOptions.testContext` set. `triggerKind: "test"` is set automatically live activation skips it.
78
- 2. **`IsTestRun`** — per-item router with `true` / `false` ports. Routes `true` iff `ctx.testContext` is set. Use it to skip side-effects in tests (don't actually send a real reply).
79
- 3. **`Assertion`** — generic callback emitter; returns `AssertionResult[]`. Each result is `{ name, score: 0..1, passThreshold?, errored?, expected?, actual?, message?, details? }` — pass/fail derives from `score >= (passThreshold ?? 0.5)` (use `score: 1`/`0` for boolean checks, set `passThreshold` for continuous metrics, `errored: true` for assertion-code crashes). Each result becomes one emitted item on `main` and one persisted `TestAssertion` row when running inside a test. Sets `emitsAssertions: true` so the host persister identifies it.
66
+ **Verify your workflow.** Call `verify_workflow({ path: "src/workflows/my-workflow.ts" })` instead of running `pnpm typecheck` yourself. Returns `{ ok, data: { typecheck, lint, build, structure }, hint? }`.
80
67
 
81
- Authors invoke a TestSuiteRun from the canvas **Tests tab** or via `POST /api/workflows/:id/test-suite-runs`. The orchestrator caps concurrency (default 4, configurable per trigger) and aggregates results into `succeeded | failed | partial | cancelled | errored`.
68
+ ## Anti-patterns
82
69
 
83
- Custom nodes can also read `ctx.testContext?.{testSuiteRunId, testCaseIndex}` directly useful for synthetic outputs in test mode without `IsTestRun` branching.
70
+ - Do not call `.trigger(...)` on the `workflow(...)` manual builder use `createWorkflowBuilder(...)` for non-manual triggers.
71
+ - Do not rely on slug-derived node ids for production workflows with credential bindings — always set an explicit `id:`.
72
+ - Do not improvise from memory when `find_examples` returns zero hits — use the fallback chain above.
84
73
 
85
74
  ## Read next when needed
86
75
 
87
- - Read `references/builder-patterns.md` for item-flow rules and fluent authoring patterns.
88
- - Read `references/workflow-testing.md` for TestTrigger / IsTestRun / Assertion authoring with full examples.
76
+ - `references/builder-patterns.md` item-flow rules and fluent authoring patterns.
77
+ - `references/workflow-testing.md` TestTrigger / IsTestRun / Assertion with full examples.
78
+ - `references/complete-example.md` — dense end-to-end example covering most authoring features.
@@ -1,13 +1,15 @@
1
+ Load this when you need item-flow rules, the two-API decision, and fluent authoring patterns.
2
+
1
3
  # Builder Patterns
2
4
 
3
- ## Standard workflow shape
5
+ ## Manual-trigger workflow (fluent — full sugar available)
4
6
 
5
7
  ```ts
8
+ import { workflow } from "@codemation/host";
9
+
6
10
  export default workflow("wf.example.id")
7
11
  .name("Example")
8
- .manualTrigger("Start", {
9
- step: "start",
10
- })
12
+ .manualTrigger("Start", { step: "start" })
11
13
  .map("Transform", (item, _ctx) => ({
12
14
  ...item.json,
13
15
  transformed: true,
@@ -15,27 +17,57 @@ export default workflow("wf.example.id")
15
17
  .build();
16
18
  ```
17
19
 
18
- ## Cron-triggered workflow
20
+ The `.map`, `.if`, `.switch`, `.split`, `.agent`, `.node`, `.then` helpers are available because `manualTrigger(...)` returns a `WorkflowChain`.
21
+
22
+ ## Cron-triggered workflow (low-level — `.then(new NodeConfig(...))` only)
19
23
 
20
24
  ```ts
21
- import { CronTrigger } from "@codemation/core-nodes";
25
+ import { Callback, CronTrigger, createWorkflowBuilder } from "@codemation/core-nodes";
22
26
 
23
- export default workflow("wf.nightly.id")
24
- .name("Nightly job")
27
+ export default createWorkflowBuilder({
28
+ id: "wf.nightly.id",
29
+ name: "Nightly job",
30
+ })
25
31
  .trigger(new CronTrigger("Nightly", { schedule: "0 3 * * *", timezone: "Europe/Amsterdam" }))
26
- .map("Process tick", (item, _ctx) => ({
27
- firedAt: (item.json as { firedAt: string }).firedAt,
28
- }))
32
+ .then(
33
+ new Callback("Process tick", (items, _ctx) => {
34
+ // Callback receives the whole batch (Items), not a single item.
35
+ // For a cron trigger the batch is always one item: { firedAt, scheduledFor }.
36
+ return items.map((item) => ({ firedAt: (item.json as { firedAt: string }).firedAt }));
37
+ }),
38
+ )
29
39
  .build();
30
40
  ```
31
41
 
32
42
  The cron expression is validated at workflow build time. Each tick emits one item with `{ firedAt, scheduledFor }` ISO-8601 strings. Always supply `timezone` for DST-sensitive schedules — defaults to UTC.
33
43
 
34
- ## Use the fluent DSL by default
44
+ **Note:** non-manual triggers do NOT give you `.map(...)` / `.if(...)` / `.agent(...)` sugar. Compose with `.then(new Callback(...))`, `.then(new If(...))`, `.then(new AIAgent({...}))`, etc.
45
+
46
+ ## Webhook-triggered workflow
47
+
48
+ ```ts
49
+ import { WebhookTrigger, createWorkflowBuilder, Callback } from "@codemation/core-nodes";
50
+
51
+ export default createWorkflowBuilder({
52
+ id: "wf.webhook.example",
53
+ name: "Webhook example",
54
+ })
55
+ .trigger(new WebhookTrigger("Incoming", { endpointKey: "inbound", methods: ["POST"] }))
56
+ .then(new Callback("Handle payload", (items) => items.map((it) => ({ received: it.json }))))
57
+ .build();
58
+ ```
59
+
60
+ ## Decision rule
61
+
62
+ - **Manual one-shot trigger?** Use `workflow("id").manualTrigger(...)` — short, fluent, full sugar.
63
+ - **Anything else?** Use `createWorkflowBuilder({ id, name }).trigger(new Trigger(...))` — verbose, node-config style.
64
+
65
+ ## Imports cheat sheet
35
66
 
36
- - import `workflow` from `@codemation/host`
37
- - keep the file under `src/workflows`
38
- - export the built workflow definition as the default export when following starter patterns
67
+ - `workflow` `@codemation/host` (re-exports from `@codemation/core-nodes`)
68
+ - `createWorkflowBuilder`, `CronTrigger`, `WebhookTrigger`, `Callback`, `HttpRequest`, `AIAgent`, `If`, `Split`, `Merge`, `SubWorkflow` → `@codemation/core-nodes`
69
+ - `callableTool`, `itemExpr` `@codemation/core`
70
+ - Workflow file location: `src/workflows/`. Export the built definition as the default export.
39
71
 
40
72
  ## Item rules
41
73
 
@@ -0,0 +1,263 @@
1
+ Load this when you need to see a complete workflow that exercises most authoring features end-to-end.
2
+
3
+ ## The dense example (manual trigger — full fluent sugar)
4
+
5
+ The fluent `.map`/`.if`/`.switch`/`.split`/`.agent`/`.node` helpers are only available after `.manualTrigger(...)`. The example below is a manual-trigger workflow so it can demonstrate all of them. For cron / webhook variants, see the snippet at the bottom.
6
+
7
+ ```ts
8
+ // src/workflows/dailyCsvDigest.ts
9
+ //
10
+ // Theme: a manual-triggered "daily CSV digest". Caller passes { date: "YYYY-MM-DD" }.
11
+ // The flow fetches that day's sales CSV from a reporting API, parses each row,
12
+ // classifies rows with an LLM agent, and sends a per-row digest email.
13
+ //
14
+ // Register in codemation.config.ts:
15
+ // import dailyCsvDigest from "./src/workflows/dailyCsvDigest";
16
+ // workflows: [dailyCsvDigest]
17
+
18
+ import { z } from "zod";
19
+ import { callableTool, itemExpr } from "@codemation/core";
20
+ import { HttpRequest } from "@codemation/core-nodes";
21
+ import { workflow } from "@codemation/host";
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Types
25
+ // ---------------------------------------------------------------------------
26
+
27
+ type TriggerInput = { date: string }; // e.g. "2025-05-14"
28
+
29
+ type FetchMeta = {
30
+ url: string;
31
+ ok: boolean;
32
+ status: number;
33
+ binarySlot: string;
34
+ };
35
+
36
+ type CsvRow = {
37
+ region: string;
38
+ product: string;
39
+ revenue: number;
40
+ anomaly: boolean;
41
+ };
42
+
43
+ type ClassifiedRow = CsvRow & {
44
+ classification: "normal" | "warning" | "critical";
45
+ rationale: string;
46
+ };
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // Inline callable tool — classify a single row
50
+ // ---------------------------------------------------------------------------
51
+
52
+ const classifyRowTool = callableTool({
53
+ name: "classify_row",
54
+ description: "Classify a revenue row as normal, warning, or critical.",
55
+ inputSchema: z.object({
56
+ region: z.string(),
57
+ product: z.string(),
58
+ revenue: z.number(),
59
+ anomaly: z.boolean(),
60
+ }),
61
+ outputSchema: z.object({
62
+ classification: z.enum(["normal", "warning", "critical"]),
63
+ rationale: z.string(),
64
+ }),
65
+ execute: async ({ input }) => {
66
+ // Fallback executor if the agent doesn't call the tool — keeps the workflow deterministic in tests.
67
+ const classification =
68
+ input.anomaly || input.revenue < 0 ? "critical" : input.revenue < 1000 ? "warning" : "normal";
69
+ return { classification, rationale: `Revenue ${input.revenue}, anomaly=${input.anomaly}` };
70
+ },
71
+ });
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // Workflow
75
+ // ---------------------------------------------------------------------------
76
+
77
+ export default workflow("wf.daily-csv-digest")
78
+ .name("Daily CSV Digest")
79
+ // Manual trigger seeded with a default date — callers can override at run time.
80
+ .manualTrigger<TriggerInput>("Start", { date: "2025-05-14" })
81
+
82
+ // ── Step 1: build the fetch URL ────────────────────────────────────────────
83
+ // async .map — use when you need await (date math here is sync, but the API call below is async).
84
+ .map("Build fetch URL", async (item, _ctx) => ({
85
+ date: item.json.date,
86
+ reportUrl: `https://reports.internal/sales/${item.json.date}.csv`,
87
+ }))
88
+
89
+ // HttpRequest with responseFormat:"binary" stores the body in ctx.binary automatically.
90
+ // Explicit id "fetch-report" keeps the credential binding stable across label renames.
91
+ .then(
92
+ new HttpRequest("Fetch report CSV", {
93
+ id: "fetch-report",
94
+ urlField: "reportUrl",
95
+ responseFormat: "binary",
96
+ responseBinarySlot: "csvFile",
97
+ credentialSlot: "reportApi",
98
+ }),
99
+ )
100
+
101
+ // ── Step 2: gate on HTTP success ───────────────────────────────────────────
102
+ // .if predicate receives (item, ctx). Use for fast boolean branches; .switch is overkill for two outcomes.
103
+ .if((item: { json: FetchMeta }, _ctx) => item.json.ok, {
104
+ true: (branch) =>
105
+ branch
106
+ // ── Step 3: parse CSV from binary ──────────────────────────────────────
107
+ // async .map — needs await to read from binary storage.
108
+ .map("Parse CSV rows", async (item: { json: FetchMeta }, ctx) => {
109
+ const stream = await ctx.binary.openReadStream(item.json.binarySlot);
110
+ const text = await streamToText(stream);
111
+ const rows = parseCsv(text);
112
+ return { rows, fetchedAt: item.json.url };
113
+ })
114
+
115
+ // .split emits one item per CSV row so downstream steps run per-row.
116
+ .split("Split rows", (item: { json: { rows: CsvRow[] } }) => item.json.rows)
117
+
118
+ // ── Step 4: classify each row with an agent ──────────────────────────
119
+ // itemExpr defers message construction to per-item runtime — required when content depends on the current item.
120
+ .agent("Classify row", {
121
+ model: "openai:gpt-4o-mini",
122
+ messages: itemExpr(({ item }: { item: { json: CsvRow } }) => [
123
+ { role: "system" as const, content: "You are a revenue analyst. Use classify_row." },
124
+ { role: "user" as const, content: JSON.stringify(item.json) },
125
+ ]),
126
+ tools: [classifyRowTool],
127
+ outputSchema: z.object({
128
+ classification: z.enum(["normal", "warning", "critical"]),
129
+ rationale: z.string(),
130
+ }),
131
+ })
132
+
133
+ // ── Step 5: merge agent output with the original row via ctx.data ──────
134
+ // ctx.data is keyed by node id (the slug of the node label).
135
+ // "Split rows" slugs to "split-rows"; we read its emitted item back here.
136
+ // sync .map — pure object merge, no I/O.
137
+ .map("Enrich classification", (item: { json: { classification: string; rationale: string } }, ctx) => {
138
+ const originalRow = ctx.data["split-rows"]?.items?.[0]?.json as CsvRow | undefined;
139
+ return {
140
+ ...originalRow,
141
+ classification: item.json.classification as ClassifiedRow["classification"],
142
+ rationale: item.json.rationale,
143
+ } satisfies Partial<ClassifiedRow>;
144
+ })
145
+
146
+ // ── Step 6: send digest email via a registered node ───────────────────
147
+ // .node(name, config, options) — explicit id keeps credential binding stable.
148
+ // SendEmailNodeConfig is illustrative; replace with the email node available in your project.
149
+ .node(
150
+ "Send digest email",
151
+ new SendEmailNodeConfig({
152
+ // itemExpr on a config field — engine resolves once per item at execution time.
153
+ subject: itemExpr(
154
+ ({ item }: { item: { json: Partial<ClassifiedRow> } }) =>
155
+ `[${item.json.classification?.toUpperCase()}] ${item.json.region} – ${item.json.product}`,
156
+ ),
157
+ to: "ops-team@example.com",
158
+ body: itemExpr(
159
+ ({ item }: { item: { json: Partial<ClassifiedRow> } }) =>
160
+ `Region: ${item.json.region}\nRevenue: ${item.json.revenue}\nRationale: ${item.json.rationale}`,
161
+ ),
162
+ }),
163
+ { id: "send-digest-email" },
164
+ ),
165
+
166
+ false: (branch) =>
167
+ branch.map("Log fetch failure", (item: { json: FetchMeta }, _ctx) => ({
168
+ error: `Fetch failed: HTTP ${item.json.status}`,
169
+ url: item.json.url,
170
+ })),
171
+ })
172
+
173
+ // .build() validates non-empty + unique node ids (including agent connection children).
174
+ // Throws WorkflowDefinitionError on violation.
175
+ .build();
176
+
177
+ // ---------------------------------------------------------------------------
178
+ // Helpers (inline for brevity — promote to lib/ if reused)
179
+ // ---------------------------------------------------------------------------
180
+
181
+ async function streamToText(stream: AsyncIterable<Uint8Array>): Promise<string> {
182
+ const chunks: Buffer[] = [];
183
+ for await (const chunk of stream) chunks.push(Buffer.from(chunk));
184
+ return Buffer.concat(chunks).toString("utf-8");
185
+ }
186
+
187
+ function parseCsv(text: string): CsvRow[] {
188
+ const [header, ...lines] = text.trim().split("\n");
189
+ const cols = header!.split(",");
190
+ return lines.map((line) => {
191
+ const vals = line.split(",");
192
+ return {
193
+ region: vals[cols.indexOf("region")] ?? "",
194
+ product: vals[cols.indexOf("product")] ?? "",
195
+ revenue: Number(vals[cols.indexOf("revenue")] ?? 0),
196
+ anomaly: vals[cols.indexOf("anomaly")] === "true",
197
+ };
198
+ });
199
+ }
200
+ ```
201
+
202
+ ## What this exercises
203
+
204
+ - **Manual trigger with typed default item** → `workflow("...").manualTrigger<TriggerInput>("Start", {...})`
205
+ - **sync `.map`** → "Enrich classification" — pure object merge, no `await`
206
+ - **async `.map`** → "Build fetch URL" and "Parse CSV rows" — uses `await` for binary read
207
+ - **`.if` per-item predicate** → `(item, _ctx) => item.json.ok` with branch factories
208
+ - **`HttpRequest` with explicit `id:`** → `id: "fetch-report"` (credential binding stability)
209
+ - **`.split`** → fan-out one batch into many items
210
+ - **`.agent(...)` with `messages`, `model`, `tools`, `outputSchema`** → typed structured output
211
+ - **`callableTool` with Zod schemas and `execute({ input })`** → inline tool definition
212
+ - **`itemExpr(...)`** → on agent messages (per-item content) and on `.node` config fields (per-item subject/body)
213
+ - **`.node(name, config, options)` with explicit id** → stable credential binding
214
+ - **`ctx.data["<slug>"]`** → reading earlier node output without threading it through every step
215
+ - **`ctx.binary.openReadStream(slot)`** → reading bytes from a binary slot attached upstream
216
+ - **`.build()`** → final validation pass
217
+
218
+ ## Cron / webhook variant (alternative trigger)
219
+
220
+ When the trigger isn't manual, the fluent `.map`/`.if`/`.agent` sugar isn't available — you use the lower-level builder and `.then(new SomeNodeConfig(...))`. Shape:
221
+
222
+ ```ts
223
+ import { Callback, CronTrigger, createWorkflowBuilder, HttpRequest } from "@codemation/core-nodes";
224
+
225
+ export default createWorkflowBuilder({
226
+ id: "wf.daily-csv-digest.cron",
227
+ name: "Daily CSV Digest (cron)",
228
+ })
229
+ .trigger(new CronTrigger("Daily 06:00", { schedule: "0 6 * * *", timezone: "UTC" }))
230
+ // Cron fires one item per tick: { firedAt, scheduledFor }. Wrap downstream logic in Callback configs:
231
+ .then(
232
+ new Callback("Build fetch URL", (items, _ctx) => {
233
+ return items.map((item) => {
234
+ const date = new Date((item.json as { scheduledFor: string }).scheduledFor).toISOString().slice(0, 10);
235
+ return { date, reportUrl: `https://reports.internal/sales/${date}.csv` };
236
+ });
237
+ }),
238
+ )
239
+ .then(
240
+ new HttpRequest("Fetch report CSV", {
241
+ id: "fetch-report",
242
+ urlField: "reportUrl",
243
+ responseFormat: "binary",
244
+ responseBinarySlot: "csvFile",
245
+ credentialSlot: "reportApi",
246
+ }),
247
+ )
248
+ // For branching, use `new If(...)`. For per-item agent calls, use `new AIAgent({...})`.
249
+ // For row fan-out, use `new Split(...)`. The execution semantics match the fluent helpers
250
+ // — only the surface syntax differs.
251
+ .build();
252
+ ```
253
+
254
+ If you need both cron + the fluent sugar in the same workflow, you can wrap the cursor manually:
255
+
256
+ ```ts
257
+ import { WorkflowChain } from "@codemation/core-nodes";
258
+
259
+ const cursor = createWorkflowBuilder({ id, name }).trigger(new CronTrigger("Tick", { schedule: "..." }));
260
+ export default new WorkflowChain(cursor).map("First step", (item) => ({ ...item.json })).build();
261
+ ```
262
+
263
+ This is uncommon in production code; reach for it only when the fluent helpers genuinely help readability.