@codemation/agent-skills 0.1.9 → 0.2.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/package.json +6 -3
- package/skills/codemation-ai-agent-node/SKILL.md +128 -0
- package/skills/codemation-credential-development/SKILL.md +6 -0
- package/skills/codemation-credential-development/references/credential-patterns.md +43 -0
- package/skills/codemation-custom-node-development/SKILL.md +33 -18
- 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 +164 -0
- package/skills/codemation-framework-concepts/SKILL.md +12 -1
- package/skills/codemation-mcp-capabilities/SKILL.md +85 -0
- package/skills/codemation-mcp-capabilities/references/agent-with-mcp.ts +44 -0
- package/skills/codemation-plugin-development/SKILL.md +43 -0
- package/skills/codemation-plugin-development/references/plugin-structure.md +31 -0
- package/skills/codemation-workflow-dsl/SKILL.md +237 -13
- package/skills/codemation-workflow-dsl/references/builder-patterns.md +70 -9
- package/skills/codemation-workflow-dsl/references/complete-example.md +263 -0
- package/skills/codemation-workflow-dsl/references/workflow-testing.md +194 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# @codemation/agent-skills
|
|
2
2
|
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8285ec0: Add `codemation-ai-agent-node` skill teaching the coding agent the `AIAgent` constructor signature, message shape, output contract (`{ output: string }`), and the four managed model IDs currently in the allowlist. Also covers BYOK (`OpenAIChatModelConfig`) and MCP tool attachment.
|
|
8
|
+
- 8285ec0: Add `codemation-mcp-capabilities` skill: documents the control-plane `GET /api/registry/capabilities` endpoint, MCP server credential kinds, and the workflow-author flow for discovering and binding MCP servers. Originally produced under Story 14 in a workspace-local install; relocated here so `pnpm codemation skills sync` picks it up.
|
|
9
|
+
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- 8285ec0: docs(codemation-workflow-dsl): add "Verifying your workflow" section pointing agents to use `verify_workflow` MCP tool instead of `pnpm typecheck`.
|
|
13
|
+
- 8285ec0: Reorganize agent skills for faster coding-agent start-up (Story C).
|
|
14
|
+
- `codemation-framework-concepts` promoted to router skill: adds "Where to go next" section mapping tasks to skills, and updated frontmatter describing it as the index to read first.
|
|
15
|
+
- `codemation-custom-node-development` split into a slim TOC (53 lines) plus three new focused refs: `define-node-per-item.md`, `define-batch-node.md`, `credential-aware-nodes.md`. Binary/HTTP/MS-Graph patterns consolidated into the existing `node-patterns.md`.
|
|
16
|
+
|
|
17
|
+
- 8285ec0: Remove dead references to `describe_node` and `list_nodes` from workflow-dsl and ai-agent-node skills; replace with `find_examples` as the discovery surface.
|
|
18
|
+
- 8285ec0: Sprint 11 Story D: deepen `find_examples` as the canonical discovery surface in skills.
|
|
19
|
+
- Expand `codemation-workflow-dsl` "Discovering nodes and patterns" section with why examples are canonical, when to call first, two query patterns, zero-hit response (ask user or adapt with confirmation), and anti-pattern (don't grep node_modules).
|
|
20
|
+
- Strengthen `codemation-ai-agent-node` call-to-action with concrete `find_examples` query suggestions for AIAgent patterns.
|
|
21
|
+
|
|
22
|
+
- 8285ec0: docs(codemation-workflow-dsl): note that search results include install-state fields (`installed`, `requiresInstall`) and how agents should use them to plan `install_package` calls.
|
|
23
|
+
- 8285ec0: Sprint 12 Story C: add self-solving fallback chain to `codemation-workflow-dsl` skill.
|
|
24
|
+
- Add "When no example matches — the self-solving fallback chain" section after "Discovering nodes and patterns".
|
|
25
|
+
- Four-tier chain: retry with intent variations → defineRestNode (HTTP APIs) → HttpRequest (inline one-off) → defineNode (non-HTTP custom logic).
|
|
26
|
+
- Explicit "What NOT to do" and "Surfacing what you did" sub-sections.
|
|
27
|
+
- Agent never asks the non-technical user for fallback choices; picks per the chain and reports what it used.
|
|
28
|
+
|
|
29
|
+
- 8285ec0: Sprint 14 coverage: tests for WhenBuilder DSL helper, InMemoryWorkflowExecutionRepository retention paths, DevTrackedProcessTreeKiller edge cases, ConsumerCliTsconfigPreparation resolution, ListenPortConflictDescriber ss fallback, RedisRunEventBus publish/subscribe/teardown, CodemationChatModelFactory HMAC signing, registerCoreNodes smoke, single-react-component-per-file rule branches, and CodemationAgentSkillsCli error/help paths. No production code changes.
|
|
30
|
+
- 8285ec0: docs(skills): add complete workflow example to workflow-dsl skill
|
|
31
|
+
- 8285ec0: Fix workflow-dsl skill docs: `workflow("id")` is manual-trigger only. Cron, webhook, and other non-manual triggers must use `createWorkflowBuilder({id, name}).trigger(new XxxTrigger(...))` and chain with `.then(new SomeNodeConfig(...))` — the fluent `.map`/`.if`/`.agent`/`.node` sugar is only available via `manualTrigger(...)`.
|
|
32
|
+
|
|
33
|
+
Affected: `codemation-workflow-dsl/SKILL.md`, `codemation-workflow-dsl/references/builder-patterns.md`, `codemation-workflow-dsl/references/complete-example.md`, `codemation-mcp-capabilities/references/agent-with-mcp.ts`.
|
|
34
|
+
|
|
35
|
+
## 0.1.10
|
|
36
|
+
|
|
37
|
+
### Patch Changes
|
|
38
|
+
|
|
39
|
+
- [#114](https://github.com/MadeRelevant/codemation/pull/114) [`ec985a3`](https://github.com/MadeRelevant/codemation/commit/ec985a3264696b421e8be7c84c7cead6a85cbe6c) Thanks [@cblokland90](https://github.com/cblokland90)! - Fix `pnpm create codemation <name>` failing with `ENOENT … node_modules/agent-skills/skills` when dlx'd from npm.
|
|
40
|
+
|
|
41
|
+
`@codemation/agent-skills`'s `exports` field only declared `.`, so `require.resolve("@codemation/agent-skills/package.json")` was blocked by Node's exports gate. `create-codemation`'s resolver fell back to a workspace-only relative path that doesn't exist outside the monorepo. Adds `./package.json` and `./skills/*` to the exports map so subpath access works for consumers — and bumps `create-codemation` patch so the next release pins the fixed agent-skills version.
|
|
42
|
+
|
|
43
|
+
- [#110](https://github.com/MadeRelevant/codemation/pull/110) [`4902978`](https://github.com/MadeRelevant/codemation/commit/49029782243ece59ab6aa5bb46396db445cad47c) Thanks [@cblokland90](https://github.com/cblokland90)! - Add per-package `test:unit` scripts so Turbo can address each package individually for affected-only filtering. No runtime changes — dev-tooling only.
|
|
44
|
+
|
|
3
45
|
## 0.1.9
|
|
4
46
|
|
|
5
47
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codemation/agent-skills",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Reusable agent skills for Codemation projects and plugin development.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
"types": "./lib/agent-skills-extractor.d.ts",
|
|
20
20
|
"import": "./lib/agent-skills-extractor.mjs",
|
|
21
21
|
"default": "./lib/agent-skills-extractor.mjs"
|
|
22
|
-
}
|
|
22
|
+
},
|
|
23
|
+
"./package.json": "./package.json",
|
|
24
|
+
"./skills/*": "./skills/*"
|
|
23
25
|
},
|
|
24
26
|
"bin": {
|
|
25
27
|
"codemation-agent-skills": "./bin/codemation-agent-skills.mjs"
|
|
@@ -46,6 +48,7 @@
|
|
|
46
48
|
],
|
|
47
49
|
"scripts": {
|
|
48
50
|
"changeset:verify": "pnpm --workspace-root run changeset:verify",
|
|
49
|
-
"test": "vitest run"
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"test:unit": "vitest run"
|
|
50
53
|
}
|
|
51
54
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: codemation-ai-agent-node
|
|
3
|
+
description: AIAgent constructor, message shape, output contract, and chat-model configs (managed and BYOK). Read before writing any workflow that uses AIAgent.
|
|
4
|
+
compatibility: Codemation core-nodes. Requires @codemation/core-nodes import.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Codemation AI Agent Node
|
|
8
|
+
|
|
9
|
+
> **Start here: call `find_examples` before reading further.**
|
|
10
|
+
>
|
|
11
|
+
> - `find_examples({ query: "AIAgent" })` — basic usage and constructor patterns
|
|
12
|
+
> - `find_examples({ query: "AIAgent multi-step" })` — chained pipeline patterns
|
|
13
|
+
> - `find_examples({ query: "AIAgent tools" })` — agent with callable tools
|
|
14
|
+
> - `find_examples({ query: "AIAgent gmail classify" })` — domain-specific examples
|
|
15
|
+
>
|
|
16
|
+
> The sections below are a quick orientation for when you need the exact constructor or output shape.
|
|
17
|
+
|
|
18
|
+
## Use this skill when
|
|
19
|
+
|
|
20
|
+
Writing a workflow that uses `AIAgent` — classification, extraction, summarisation, drafting, decision, or any step that calls an LLM.
|
|
21
|
+
Use `codemation-workflow-dsl` for the surrounding workflow structure.
|
|
22
|
+
Use `codemation-mcp-capabilities` when the agent needs MCP servers.
|
|
23
|
+
|
|
24
|
+
## When to use `AIAgent` vs other approaches
|
|
25
|
+
|
|
26
|
+
Use `AIAgent` when an item needs an LLM call with a fixed or per-item prompt and optional tool use.
|
|
27
|
+
Use a plain `Callback` instead when the logic is deterministic code (no LLM needed).
|
|
28
|
+
Use the `.agent(...)` fluent helper on a manual-trigger workflow only if you need the full fluent chain sugar — under the hood it also produces an `AIAgent`.
|
|
29
|
+
|
|
30
|
+
## Constructor
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { AIAgent } from "@codemation/core-nodes";
|
|
34
|
+
|
|
35
|
+
new AIAgent({
|
|
36
|
+
name: string, // display name and default node id slug
|
|
37
|
+
messages: AgentMessageConfig, // see below
|
|
38
|
+
chatModel: ChatModelConfig, // see Managed and BYOK sections below
|
|
39
|
+
tools?: ReadonlyArray<ToolConfig>, // optional callable tools
|
|
40
|
+
id?: string, // stable node id (set explicitly if node has credential bindings)
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## `messages` shape
|
|
45
|
+
|
|
46
|
+
`messages` is an ordered array of `{ role, content }` objects.
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
messages: [
|
|
50
|
+
{ role: "system", content: "You are a helpful assistant that classifies emails." },
|
|
51
|
+
{ role: "user", content: (args) => `Classify this email:\n\n${args.item.json.body}` },
|
|
52
|
+
];
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- `role` is `"system"` | `"user"` (use `"assistant"` only for few-shot examples — rare).
|
|
56
|
+
- `content` can be a plain string or a function `(args: { item, itemIndex, items, ctx }) => string`.
|
|
57
|
+
- Put the detailed instructions in the `system` message and the per-item data in the `user` message.
|
|
58
|
+
|
|
59
|
+
## Output shape
|
|
60
|
+
|
|
61
|
+
`AIAgent` emits `{ output: string }` on its single port `main`.
|
|
62
|
+
|
|
63
|
+
The next node sees `item.json.output` as the agent's text response.
|
|
64
|
+
Type your downstream `Callback` accordingly:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
.then(new Callback<{ output: string }>("Handle result", (item) => {
|
|
68
|
+
const reply = item.json.output;
|
|
69
|
+
// ...
|
|
70
|
+
}))
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
If you set `outputSchema` (a Zod schema), the agent validates and parses the output into a structured object. Without `outputSchema`, `item.json.output` is always a plain string.
|
|
74
|
+
|
|
75
|
+
## Managed model (no credentials needed)
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { AIAgent, CodemationChatModelConfig } from "@codemation/core-nodes";
|
|
79
|
+
|
|
80
|
+
new AIAgent({
|
|
81
|
+
name: "Classify email",
|
|
82
|
+
messages: [
|
|
83
|
+
{ role: "system", content: "Classify the email as spam or not-spam." },
|
|
84
|
+
{ role: "user", content: (args) => args.item.json.body as string },
|
|
85
|
+
],
|
|
86
|
+
chatModel: new CodemationChatModelConfig(
|
|
87
|
+
"Claude Haiku", // display label
|
|
88
|
+
"anthropic/claude-haiku-4-5-20251001", // managed model id
|
|
89
|
+
),
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Currently allowlisted managed models
|
|
94
|
+
|
|
95
|
+
| Model id | Notes |
|
|
96
|
+
| ------------------------------------- | -------------------- |
|
|
97
|
+
| `anthropic/claude-haiku-4-5-20251001` | Fastest and cheapest |
|
|
98
|
+
| `anthropic/claude-sonnet-4-6` | Balanced |
|
|
99
|
+
| `anthropic/claude-opus-4-5-20251101` | High capability |
|
|
100
|
+
| `anthropic/claude-opus-4-6` | Latest flagship |
|
|
101
|
+
|
|
102
|
+
Discover live: `GET <CONTROL_PLANE_URL>/api/llm/managed-models`
|
|
103
|
+
|
|
104
|
+
## BYOK model (user supplies their own key)
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { AIAgent, OpenAIChatModelConfig } from "@codemation/core-nodes";
|
|
108
|
+
|
|
109
|
+
new AIAgent({
|
|
110
|
+
name: "Summarise",
|
|
111
|
+
id: "summarise-agent", // stable id — required when node has a credential binding
|
|
112
|
+
messages: [
|
|
113
|
+
{ role: "system", content: "Summarise the following text in one paragraph." },
|
|
114
|
+
{ role: "user", content: (args) => args.item.json.text as string },
|
|
115
|
+
],
|
|
116
|
+
chatModel: new OpenAIChatModelConfig(
|
|
117
|
+
"OpenAI GPT-4o", // display label
|
|
118
|
+
"gpt-4o", // OpenAI model id
|
|
119
|
+
"openai", // credential slot key — matches the slot used in getCredentialRequirements
|
|
120
|
+
),
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`OpenAIChatModelConfig` requires the user to connect an `openai.apiKey` credential. The concierge handles credential acquisition — the coding agent must not invent credentials.
|
|
125
|
+
|
|
126
|
+
## MCP servers
|
|
127
|
+
|
|
128
|
+
If you need tools / MCP servers on the agent, see the `codemation-mcp-capabilities` skill.
|
|
@@ -12,6 +12,12 @@ Use this skill for defining new credential types, wiring them into apps or plugi
|
|
|
12
12
|
|
|
13
13
|
Do not use this skill for general workflow authoring unless credential slots or runtime sessions are the core problem.
|
|
14
14
|
|
|
15
|
+
## Credential binding stability
|
|
16
|
+
|
|
17
|
+
Credentials bind to a node via `(workflowId, nodeId, slotKey)`. The `nodeId` defaults to a slug of the node's `name` label (lowercase, non-alphanumeric runs replaced with `-`). Renaming a credential-using node's label silently changes its id and the binding appears unbound in the UI — the operator must re-attach manually.
|
|
18
|
+
|
|
19
|
+
To prevent this: either keep the node's label stable across edits, or set an explicit `id:` on the node config so the id is decoupled from the label.
|
|
20
|
+
|
|
15
21
|
## Core mental model
|
|
16
22
|
|
|
17
23
|
1. A credential type defines public config, secret material, session creation, and health testing.
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Credential Patterns
|
|
2
2
|
|
|
3
|
+
## Node id and binding stability
|
|
4
|
+
|
|
5
|
+
A credential binding is stored as `(workflowId, nodeId, slotKey)`. The `nodeId` for each workflow node defaults to a slug of its `name` label. Changing the label changes the id, and the previously configured binding appears unbound.
|
|
6
|
+
|
|
7
|
+
For production workflows with credential-using nodes, prefer an explicit `id:` on the node config:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
.node("Fetch from API", MyApiNodeConfig, {
|
|
11
|
+
id: "fetch-from-api", // stable across label renames
|
|
12
|
+
credentials: { apiKey: myApiCredential },
|
|
13
|
+
})
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Without an explicit `id:`, keep the node's label constant or plan to re-bind after a rename.
|
|
17
|
+
|
|
3
18
|
## Standard shape
|
|
4
19
|
|
|
5
20
|
Use `defineCredential(...)` to declare:
|
|
@@ -40,6 +55,34 @@ Optional or power-user fields (for example custom OAuth scopes) can be tucked be
|
|
|
40
55
|
|
|
41
56
|
See **`packages/core/docs/credential-ui-fields.md`** in the repository root layout.
|
|
42
57
|
|
|
58
|
+
## OAuth2 credentials (URL-template variant)
|
|
59
|
+
|
|
60
|
+
For credentials that go through the OAuth2 redirect flow (Microsoft Graph, Slack, GitHub, Notion, etc.), declare the authorize and token URLs directly on the credential's `auth` definition. The host's `OAuth2ProviderRegistry` substitutes `{publicFieldKey}` placeholders from the credential's public config at connect time (URL-encoded).
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
auth: {
|
|
64
|
+
kind: "oauth2",
|
|
65
|
+
// providerId is a free-form label for telemetry / DB rows / Better Auth provider naming.
|
|
66
|
+
// It is NOT used for any registry lookup — URLs come from the fields below.
|
|
67
|
+
providerId: "microsoft",
|
|
68
|
+
authorizeUrl: "https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize",
|
|
69
|
+
tokenUrl: "https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token",
|
|
70
|
+
scopes: ["openid", "offline_access", "User.Read", "Mail.Read"],
|
|
71
|
+
},
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Three `auth` variants exist:
|
|
75
|
+
|
|
76
|
+
1. **URL-template (preferred for new plugins).** Carries `authorizeUrl` / `tokenUrl` / optional `userInfoUrl` directly with `{fieldKey}` substitution. Self-contained — adding a new provider needs no core or host edits.
|
|
77
|
+
2. **Built-in `providerId` shortcut.** Only `google` is recognized; kept for backwards compatibility. Do not add new providers here.
|
|
78
|
+
3. **`providerFromPublicConfig`.** URLs read verbatim from public field values at runtime. Rare; the template variant covers almost every real case more ergonomically.
|
|
79
|
+
|
|
80
|
+
Notes for plugin authors:
|
|
81
|
+
|
|
82
|
+
- Host stores post-callback OAuth material with snake_case keys (`access_token`, `refresh_token`, `expiry`, `scope`, `token_type`). Read those keys inside `createSession` / `test`, NOT camelCase.
|
|
83
|
+
- The redirect URI returned to providers rewrites loopback IPs (`127.0.0.1`, `[::1]`) to `localhost` so Azure AD (AADSTS50011) and other providers with the same restriction accept it.
|
|
84
|
+
- The default `Mail.Read` (and similar single-mailbox) Microsoft scopes only cover the credential owner. To monitor a shared mailbox via `/users/{upn}/...`, request `Mail.Read.Shared` (delegated) or admin-consented application permissions.
|
|
85
|
+
|
|
43
86
|
## Health and activation
|
|
44
87
|
|
|
45
88
|
- deploy the workflow and credential type
|
|
@@ -12,27 +12,42 @@ Use this skill for reusable custom node work, whether the node lives inside an a
|
|
|
12
12
|
|
|
13
13
|
Do not use this skill for pure workflow chaining questions unless the node implementation itself is changing.
|
|
14
14
|
|
|
15
|
-
##
|
|
15
|
+
## Per-item vs batch
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
2. Implement **`execute(args, context)`** — one mapped **input** in, one output payload per item (activations are still batch-shaped; the engine iterates items for you).
|
|
19
|
-
3. Give the node a stable key and a clear title.
|
|
20
|
-
4. Optionally set **`icon`** on the `defineNode` definition so the workflow canvas shows a proper glyph (same string contract as `NodeConfigBase.icon`).
|
|
21
|
-
5. Use **`defineBatchNode(...)`** with **`run(items, context)`** only when the node must process the **entire batch** at once (legacy batch semantics).
|
|
22
|
-
6. Promote callback-heavy logic into a node when the graph or tests need a stronger boundary.
|
|
17
|
+
**`defineNode(...)` (per-item)** — the engine calls `execute(args, context)` once per item. This is the right default for the vast majority of nodes: straightforward logic, credential slots, input schema, optional fan-out.
|
|
23
18
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
1. Prefer helper-based nodes first.
|
|
27
|
-
2. Keep nodes deterministic and focused.
|
|
28
|
-
3. Request credentials through named slots instead of hard-coded secrets.
|
|
29
|
-
4. Put **static** options (credentials, retry policy, labels) on **config**; put **per-item** behavior in **inputs** / wire JSON and optional **`itemExpr`** on config fields (consistent with built-in nodes).
|
|
30
|
-
5. Drop to class-based node APIs only when you need constructor-injected collaborators, decorators, or deeper runtime metadata.
|
|
19
|
+
**`defineBatchNode(...)` (batch)** — the engine calls `run(items, context)` with the full activation batch. Use only when the node genuinely needs to see all items at once (aggregation, bulk API calls, cross-item correlation).
|
|
31
20
|
|
|
32
|
-
|
|
21
|
+
When in doubt, start with `defineNode`.
|
|
33
22
|
|
|
34
|
-
|
|
23
|
+
## Node rules
|
|
35
24
|
|
|
36
|
-
|
|
25
|
+
1. Keep nodes deterministic and focused.
|
|
26
|
+
2. Request credentials through named slots — never hard-code secrets.
|
|
27
|
+
3. Put **static** options (credentials, retry policy, labels) on **config**; put **per-item** behavior in **inputs** / wire JSON and optional `itemExpr` on config fields.
|
|
28
|
+
4. **Emit files with `ctx.binary`, not base64 in `json`** — base64 in `item.json` bloats persisted run data. See `references/node-patterns.md`.
|
|
29
|
+
5. Drop to class-based node APIs only when you need constructor-injected collaborators, decorators, or deeper runtime metadata.
|
|
37
30
|
|
|
38
|
-
|
|
31
|
+
## Minimal `defineNode` example
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { defineNode } from "@codemation/core";
|
|
35
|
+
import { z } from "zod";
|
|
36
|
+
|
|
37
|
+
export const uppercaseNode = defineNode({
|
|
38
|
+
key: "example.uppercase",
|
|
39
|
+
title: "Uppercase field",
|
|
40
|
+
icon: "lucide:languages",
|
|
41
|
+
inputSchema: z.object({ field: z.string() }),
|
|
42
|
+
async execute({ input }) {
|
|
43
|
+
return { ...input, field: input.field.toUpperCase() };
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Read next
|
|
49
|
+
|
|
50
|
+
- `references/define-node-per-item.md` — full `defineNode(...)` contract, `inputSchema`, `itemExpr`, fan-out, assertion nodes, and `WorkflowTestKit` usage. Load this when writing or debugging a per-item node.
|
|
51
|
+
- `references/define-batch-node.md` — `defineBatchNode(...)` contract and when to choose batch over per-item. Load this when the node must see the entire batch at once.
|
|
52
|
+
- `references/credential-aware-nodes.md` — credential slots, typed sessions, and how to test credential-aware nodes. Load this when your node needs a credential.
|
|
53
|
+
- `references/node-patterns.md` — binary payloads (`ctx.binary`, `attach`, `withAttachment`), fan-out return shapes, polling-trigger binary patterns, MS Graph attachment download, and HTTP binary round-trips. Load this when working with file data or HTTP binaries.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Credential-Aware Nodes
|
|
2
|
+
|
|
3
|
+
Load this when your node needs a typed credential (OAuth token, API key, or any `defineCredential(...)` type) injected at runtime.
|
|
4
|
+
|
|
5
|
+
## Core rule
|
|
6
|
+
|
|
7
|
+
Request credentials through **named slots** on the node config instead of hard-coding secrets. The framework resolves the slot to a live typed session at execution time.
|
|
8
|
+
|
|
9
|
+
## Adding a credential slot to `defineNode`
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { defineNode } from "@codemation/core";
|
|
13
|
+
import { myApiCredentialType } from "./myApiCredential.js";
|
|
14
|
+
|
|
15
|
+
export const callApiNode = defineNode({
|
|
16
|
+
key: "example.call-api",
|
|
17
|
+
title: "Call My API",
|
|
18
|
+
credentials: {
|
|
19
|
+
api: myApiCredentialType, // slot name → credential type
|
|
20
|
+
},
|
|
21
|
+
async execute({ input }, { credentials }) {
|
|
22
|
+
const session = await credentials.api.getSession();
|
|
23
|
+
// session is typed by myApiCredentialType.sessionSchema
|
|
24
|
+
const response = await fetch("https://api.example.com/data", {
|
|
25
|
+
headers: { Authorization: `Bearer ${session.accessToken}` },
|
|
26
|
+
});
|
|
27
|
+
return response.json();
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Typed sessions
|
|
33
|
+
|
|
34
|
+
`credentials.<slot>.getSession()` returns the shape declared in the credential type's `sessionSchema`. The framework handles refresh, storage, and error propagation — your node only consumes the session.
|
|
35
|
+
|
|
36
|
+
## Testing credential-aware nodes
|
|
37
|
+
|
|
38
|
+
Supply a mock credential in `WorkflowTestKit` rather than live credentials. See `codemation-credential-development` for the full `defineCredential(...)` story, typed sessions, and credential testing patterns.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Define Batch Node
|
|
2
|
+
|
|
3
|
+
Load this when you need to author a `defineBatchNode(...)` node that processes all items in one call.
|
|
4
|
+
|
|
5
|
+
## When to use `defineBatchNode` instead of `defineNode`
|
|
6
|
+
|
|
7
|
+
- The node must see the **entire activation batch** at once (e.g. an aggregation, a bulk API call, or a node that correlates items against each other).
|
|
8
|
+
- Legacy batch semantics are required by the calling workflow.
|
|
9
|
+
- You need the same contract as built-in batch-shaped nodes such as `Aggregate`.
|
|
10
|
+
|
|
11
|
+
For the common case (one-item-at-a-time logic), prefer `defineNode` — the engine handles iteration for you.
|
|
12
|
+
|
|
13
|
+
## Minimal skeleton
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { defineBatchNode } from "@codemation/core";
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
|
|
19
|
+
export const sumNode = defineBatchNode({
|
|
20
|
+
key: "example.sum",
|
|
21
|
+
title: "Sum numeric field",
|
|
22
|
+
inputSchema: z.object({ value: z.number() }),
|
|
23
|
+
async run(items, { config }) {
|
|
24
|
+
const total = items.reduce((acc, item) => acc + (item.json as { value: number }).value, 0);
|
|
25
|
+
return [{ json: { total } }];
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Contract
|
|
31
|
+
|
|
32
|
+
- `run(items, context)` receives the **full array** of activation items.
|
|
33
|
+
- Return an array of output items (same length as input is not required — you can fan-in to one, or fan-out to many).
|
|
34
|
+
- The context object exposes `config`, `credentials`, and `execution` (same as `defineNode`).
|
|
35
|
+
|
|
36
|
+
## Advanced fallback
|
|
37
|
+
|
|
38
|
+
Reach for class-based node APIs when constructor-injected collaborators are required, plugin packaging needs the lower-level runtime contract, or decorators/persisted metadata need tighter control.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Define Node (Per-Item)
|
|
2
|
+
|
|
3
|
+
Load this when you need to author a `defineNode(...)` node that processes one item at a time.
|
|
4
|
+
|
|
5
|
+
## When to use `defineNode`
|
|
6
|
+
|
|
7
|
+
- Node logic is straightforward.
|
|
8
|
+
- The node belongs to one app or plugin package.
|
|
9
|
+
- Helper-based credential slots are sufficient.
|
|
10
|
+
- You do not need to inspect the entire batch in one call.
|
|
11
|
+
|
|
12
|
+
## Minimal skeleton
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { defineNode } from "@codemation/core";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
|
|
18
|
+
export const uppercaseNode = defineNode({
|
|
19
|
+
key: "example.uppercase",
|
|
20
|
+
title: "Uppercase field",
|
|
21
|
+
icon: "lucide:languages", // optional — Lucide, builtin:, si:, or image URL
|
|
22
|
+
inputSchema: z.object({ field: z.string() }),
|
|
23
|
+
async execute({ input, item }, { config }) {
|
|
24
|
+
return { ...input, [config.field]: String(input.field).toUpperCase() };
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Contract
|
|
30
|
+
|
|
31
|
+
- `execute(args, context)` is called **once per item** by the engine.
|
|
32
|
+
- Return a plain JSON object → one output item on `main`.
|
|
33
|
+
- Return a top-level array → **fan-out** (one item per element).
|
|
34
|
+
- Return `emitPorts({ portName: [...] })` for multi-port routing.
|
|
35
|
+
- Return an item-shaped `{ json, binary?, meta?, paired? }` when you need explicit binary/meta control.
|
|
36
|
+
|
|
37
|
+
## Config fields with `itemExpr`
|
|
38
|
+
|
|
39
|
+
Place **static** options (credentials, retry policy, labels) on `config`; place **per-item** values in `inputs` using `itemExpr` on config fields — consistent with built-in nodes.
|
|
40
|
+
|
|
41
|
+
## Input schema and `inputSchema`
|
|
42
|
+
|
|
43
|
+
Supply `inputSchema` (Zod) to get typed `input` in `execute` and to drive the canvas form. The engine validates items against it before calling `execute`.
|
|
44
|
+
|
|
45
|
+
## Testing with `WorkflowTestKit`
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { createEngineTestKit, registerDefinedNodes } from "@codemation/core/testing";
|
|
49
|
+
|
|
50
|
+
const kit = createEngineTestKit();
|
|
51
|
+
registerDefinedNodes([uppercaseNode]);
|
|
52
|
+
const result = await kit.runNode(uppercaseNode, { json: { field: "hello" } });
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Use `WorkflowTestKit` from `@codemation/core/testing` for engine-backed tests without the host.
|
|
56
|
+
|
|
57
|
+
## Custom assertion nodes
|
|
58
|
+
|
|
59
|
+
Set `emitsAssertions: true` on the node config to record results into `TestSuiteRun` infrastructure. The host's `TestSuiteRunTracker` listens for `nodeCompleted` events on runs with `ctx.testContext` set and persists each emitted item (matching `AssertionResult`) as a `TestAssertion` row.
|
|
60
|
+
|
|
61
|
+
Per-item nodes can also read `ctx.testContext?.{testSuiteRunId, testCaseIndex}` to branch on test mode — useful for synthetic outputs or skipping irreversible side effects.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Node Patterns
|
|
2
2
|
|
|
3
|
+
Load this when working with file data, binary payloads, HTTP binaries, MS Graph attachments, or when you need reference on fan-out return shapes and polling-trigger binary patterns.
|
|
4
|
+
|
|
3
5
|
## Start here
|
|
4
6
|
|
|
5
7
|
Use `defineNode(...)` when:
|
|
@@ -55,3 +57,165 @@ Reach for class-based node APIs when:
|
|
|
55
57
|
- **`defineNode`** runs **`execute` once per item** (with optional **`inputSchema`** and **`itemExpr`** on config fields before **`execute`**)
|
|
56
58
|
- **`defineBatchNode`** runs **`run`** once per activation batch
|
|
57
59
|
- keep nodes deterministic and testable; prefer real code paths or in-memory collaborators over heavy mocking
|
|
60
|
+
|
|
61
|
+
## Emitting items, fan-out, and binaries (for AI codegen)
|
|
62
|
+
|
|
63
|
+
**Return shapes**
|
|
64
|
+
|
|
65
|
+
- Return **plain JSON** → one output item with that **`json`** (unless the value is a **top-level array**, which **fans out** to one item per element).
|
|
66
|
+
- Return **`emitPorts({ portName: [...] })`** for multi-port routing.
|
|
67
|
+
- Return an **item-shaped** `{ json, binary?, meta?, paired? }` when you need explicit **`binary`** / **`meta`** / **`paired`** control.
|
|
68
|
+
|
|
69
|
+
**Never put bulk file content in `item.json`**
|
|
70
|
+
|
|
71
|
+
- Fields like `contentBase64`, `data`, or multi-megabyte strings are stored **inside persisted run / step JSON** in the database. That **scales poorly** (base64 is larger than raw bytes) and hurts snapshots and tooling.
|
|
72
|
+
- **Correct:** `const attachment = await args.ctx.binary.attach({ name: "file", body: bytesOrStream, mimeType, filename })` then `return args.ctx.binary.withAttachment({ json: { ok: true } }, "file", attachment)` (or build `{ json, binary }` by hand).
|
|
73
|
+
- **`body`** types match **`BinaryBody`**: `Uint8Array`, `ArrayBuffer`, `ReadableStream`, or async iterable of chunks (same idea as **`HttpRequest`** downloading a body).
|
|
74
|
+
- **`keepBinaries: true`** only **preserves existing** **`item.binary`** through a plain JSON return; it does **not** convert base64 strings in **`json`** into attachments.
|
|
75
|
+
|
|
76
|
+
**Triggers**
|
|
77
|
+
|
|
78
|
+
- Emit **one `Item` per external record**; use **`item.binary`** per record for files—not one item whose **`json`** contains an array of embedded files.
|
|
79
|
+
- For polling triggers that fetch records carrying file payloads (mail attachments, message media, etc.), do this in two phases:
|
|
80
|
+
1. In `runCycle` (the polling step), fetch only the **metadata** (id, name, contentType, size). The result is persisted into the trigger's setup state and into emitted item JSON, so it must stay small.
|
|
81
|
+
2. In `execute(items, ctx)`, when the cfg opts into downloads, fetch each blob's bytes from the source API and register them via `ctx.binary.attach(...)`. Then return items via `ctx.binary.withAttachment(item, slot, stored)`.
|
|
82
|
+
- **Do not** request the full payload in the polling fetch (e.g. Microsoft Graph `$expand=attachments` returns base64 `contentBytes` inline; use `$expand=attachments($select=id,name,contentType,size)` to keep the response light). Large polling responses bloat the run state on every cycle, even when no item is emitted.
|
|
83
|
+
|
|
84
|
+
## Binary payloads in sub-workflow chains
|
|
85
|
+
|
|
86
|
+
Binary slots attached inside a node survive SubWorkflow boundaries with no extra work. The shared `BinaryStorage` DI singleton means `ctx.binary.openReadStream` works regardless of which run originally stored the bytes.
|
|
87
|
+
|
|
88
|
+
### Pattern: attach in a node, read in the parent after SubWorkflow
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
// Child node — attaches a slot and returns the modified item.
|
|
92
|
+
export const parseAndStoreNode = defineNode({
|
|
93
|
+
key: "example.parse-store",
|
|
94
|
+
title: "Parse and Store",
|
|
95
|
+
inputSchema: z.object({ filename: z.string() }),
|
|
96
|
+
async execute({ input, item }, { binary }) {
|
|
97
|
+
const bytes = Buffer.from("...parsed content...");
|
|
98
|
+
const att = await binary.attach({
|
|
99
|
+
name: "parsed",
|
|
100
|
+
body: bytes,
|
|
101
|
+
mimeType: "text/plain",
|
|
102
|
+
filename: `${input.filename}.txt`,
|
|
103
|
+
});
|
|
104
|
+
return binary.withAttachment(item, "parsed", att);
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
After `SubWorkflowNode` returns, the parent's continuation nodes see `item.binary["parsed"]` and can call `ctx.binary.openReadStream(item.binary["parsed"])` to read the bytes.
|
|
110
|
+
|
|
111
|
+
### Testing binary across SubWorkflow with `WorkflowTestKit`
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { DefaultExecutionContextFactory, InMemoryBinaryStorage } from "@codemation/core";
|
|
115
|
+
import { createEngineTestKit } from "@codemation/core/testing";
|
|
116
|
+
import { ItemHarnessNodeConfig } from "@codemation/core/testing";
|
|
117
|
+
|
|
118
|
+
const storage = new InMemoryBinaryStorage();
|
|
119
|
+
const kit = createEngineTestKit({
|
|
120
|
+
executionContextFactory: new DefaultExecutionContextFactory(storage),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Use ItemHarnessNodeConfig (NOT CallbackNodeConfig) for nodes that must modify items:
|
|
124
|
+
const attachNode = new ItemHarnessNodeConfig(
|
|
125
|
+
"Attach",
|
|
126
|
+
z.unknown(),
|
|
127
|
+
async ({ item, ctx }) => {
|
|
128
|
+
const att = await ctx.binary.attach({
|
|
129
|
+
name: "doc",
|
|
130
|
+
body: Buffer.from("content"),
|
|
131
|
+
mimeType: "application/pdf",
|
|
132
|
+
filename: "doc.pdf",
|
|
133
|
+
});
|
|
134
|
+
return ctx.binary.withAttachment(item as Item, "doc", att);
|
|
135
|
+
},
|
|
136
|
+
{ id: "attach" },
|
|
137
|
+
);
|
|
138
|
+
// CallbackNodeConfig is fine for assertion-only (observe) nodes — it echoes input unchanged.
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Important: `CallbackNodeConfig` discards its callback return value and always echoes input items. Never use it for nodes that must attach binary or transform items.
|
|
142
|
+
|
|
143
|
+
## MS Graph: selective attachment download
|
|
144
|
+
|
|
145
|
+
Use `OutlookAttachmentDownload` from `@codemation/core-nodes-msgraph` when you have already obtained attachment metadata (filename, contentType, id) and want to download only specific attachments.
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { onNewMsGraphMailTrigger, outlookAttachmentDownloadNode } from "@codemation/core-nodes-msgraph";
|
|
149
|
+
|
|
150
|
+
workflow("wf.download-resumes")
|
|
151
|
+
.trigger(onNewMsGraphMailTrigger, { mailbox: "me", folderId: "inbox" })
|
|
152
|
+
.then(
|
|
153
|
+
outlookAttachmentDownloadNode.create(
|
|
154
|
+
{
|
|
155
|
+
messageId: "", // falls back to item.json when empty
|
|
156
|
+
attachmentId: "", // falls back to item.json when empty
|
|
157
|
+
binarySlot: "resume",
|
|
158
|
+
sizeCapBytes: 10 * 1024 * 1024,
|
|
159
|
+
},
|
|
160
|
+
"DownloadResume",
|
|
161
|
+
),
|
|
162
|
+
)
|
|
163
|
+
.build();
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Key constraints:
|
|
167
|
+
|
|
168
|
+
- Only `#microsoft.graph.fileAttachment` is supported — `itemAttachment` / `referenceAttachment` throw immediately.
|
|
169
|
+
- Set `keepBinaries: true` on any downstream node that needs to pass the binary slot forward.
|
|
170
|
+
- The credential is `msGraphMailOAuthCredentialType`; `Mail.Read` scope is sufficient.
|
|
171
|
+
|
|
172
|
+
## HTTP + binary: download to a slot, then upload from a slot
|
|
173
|
+
|
|
174
|
+
`HttpRequest` (from `@codemation/core-nodes`) natively handles binary response and request bodies.
|
|
175
|
+
|
|
176
|
+
### Download a file to a binary slot
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
import { HttpRequest } from "@codemation/core-nodes";
|
|
180
|
+
import { workflow } from "@codemation/host";
|
|
181
|
+
|
|
182
|
+
export default workflow("wf.download-pdf")
|
|
183
|
+
.manualTrigger<{ url: string }>("Start", { url: "" })
|
|
184
|
+
.then(
|
|
185
|
+
new HttpRequest("DownloadResume", {
|
|
186
|
+
responseFormat: "binary",
|
|
187
|
+
responseBinarySlot: "resume", // default is "response"
|
|
188
|
+
responseSizeCapBytes: 10 * 1024 * 1024, // 10 MiB cap (default 100 MiB)
|
|
189
|
+
}),
|
|
190
|
+
)
|
|
191
|
+
.build();
|
|
192
|
+
// item.json gets: { status, headers, binarySlot, contentType, size, filename? }
|
|
193
|
+
// item.binary["resume"] holds the BinaryAttachment reference — never base64.
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Upload binary bytes from a slot
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
new HttpRequest("UploadResume", {
|
|
200
|
+
method: "POST",
|
|
201
|
+
url: "https://api.example.com/files",
|
|
202
|
+
body: { kind: "binary", slot: "resume" },
|
|
203
|
+
// Content-Type defaults to the attachment's mimeType.
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Download then upload (full round-trip)
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
export default workflow("wf.mirror-pdf")
|
|
211
|
+
.manualTrigger<{ sourceUrl: string; targetUrl: string }>("Start", { sourceUrl: "", targetUrl: "" })
|
|
212
|
+
.then(new HttpRequest("Download", { urlField: "sourceUrl", responseFormat: "binary", responseBinarySlot: "file" }))
|
|
213
|
+
.then(new HttpRequest("Upload", { urlField: "targetUrl", method: "PUT", body: { kind: "binary", slot: "file" } }))
|
|
214
|
+
.build();
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Key rules:
|
|
218
|
+
|
|
219
|
+
- Never put bytes or base64 in `item.json` — always use `ctx.binary`.
|
|
220
|
+
- `responseSizeCapBytes` is checked against `Content-Length` before reading the body; set it for untrusted sources.
|
|
221
|
+
- Use `keepBinaries: true` on downstream nodes that must forward the slot.
|