@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.
- package/CHANGELOG.md +42 -0
- package/dist/metadata.json +110 -0
- package/package.json +4 -1
- package/skills/codemation-ai-agent-node/SKILL.md +66 -0
- package/skills/codemation-ai-agent-node/references/anti-patterns.md +11 -0
- package/skills/codemation-cli/SKILL.md +30 -24
- package/skills/codemation-credential-development/SKILL.md +27 -12
- package/skills/codemation-custom-node-development/SKILL.md +42 -27
- package/skills/codemation-custom-node-development/references/credential-aware-nodes.md +38 -0
- package/skills/codemation-custom-node-development/references/define-batch-node.md +38 -0
- package/skills/codemation-custom-node-development/references/define-node-per-item.md +61 -0
- package/skills/codemation-custom-node-development/references/node-patterns.md +141 -0
- package/skills/codemation-framework-concepts/SKILL.md +23 -27
- package/skills/codemation-mcp-capabilities/SKILL.md +53 -0
- package/skills/codemation-mcp-capabilities/references/agent-with-mcp.ts +44 -0
- package/skills/codemation-plugin-development/SKILL.md +11 -32
- package/skills/codemation-plugin-development/references/plugin-anatomy.md +115 -0
- package/skills/codemation-workflow-dsl/SKILL.md +49 -59
- package/skills/codemation-workflow-dsl/references/builder-patterns.md +47 -15
- package/skills/codemation-workflow-dsl/references/complete-example.md +263 -0
|
@@ -1,88 +1,78 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: codemation-workflow-dsl
|
|
3
|
-
description: Guides Codemation workflow authoring
|
|
4
|
-
compatibility: Designed for Codemation apps and plugins that author workflows
|
|
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
|
-
##
|
|
11
|
+
## Mental model
|
|
10
12
|
|
|
11
|
-
|
|
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
|
-
|
|
15
|
+
## When to use / when NOT
|
|
14
16
|
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
+
**When no example matches — self-solving fallback chain.**
|
|
65
55
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
-
|
|
69
|
-
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
+
## Anti-patterns
|
|
82
69
|
|
|
83
|
-
|
|
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
|
-
-
|
|
88
|
-
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
|
24
|
-
.
|
|
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
|
-
.
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
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.
|