@codemation/agent-skills 0.3.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +182 -0
  2. package/dist/metadata.json +383 -36
  3. package/package.json +3 -1
  4. package/skills/builder/ai-agent/SKILL.md +314 -0
  5. package/skills/builder/ai-agent/references/anti-patterns.md +24 -0
  6. package/skills/{codemation-cli → builder/cli}/SKILL.md +1 -8
  7. package/skills/builder/connect-external-systems/SKILL.md +191 -0
  8. package/skills/builder/credential-development/SKILL.md +86 -0
  9. package/skills/{codemation-credential-development → builder/credential-development}/references/credential-patterns.md +3 -3
  10. package/skills/builder/custom-node-development/SKILL.md +61 -0
  11. package/skills/builder/custom-node-development/references/credential-aware-nodes.md +52 -0
  12. package/skills/builder/custom-node-development/references/define-batch-node.md +54 -0
  13. package/skills/{codemation-custom-node-development → builder/custom-node-development}/references/define-node-per-item.md +14 -14
  14. package/skills/{codemation-custom-node-development → builder/custom-node-development}/references/node-patterns.md +33 -49
  15. package/skills/builder/document-ai/SKILL.md +167 -0
  16. package/skills/builder/execution-context/SKILL.md +436 -0
  17. package/skills/{codemation-framework-concepts → builder/framework-concepts}/SKILL.md +10 -18
  18. package/skills/builder/gmail/SKILL.md +327 -0
  19. package/skills/builder/human-in-the-loop/SKILL.md +82 -0
  20. package/skills/{codemation-mcp-capabilities → builder/mcp-capabilities}/SKILL.md +4 -11
  21. package/skills/{codemation-mcp-capabilities → builder/mcp-capabilities}/references/agent-with-mcp.ts +1 -1
  22. package/skills/builder/msgraph/SKILL.md +338 -0
  23. package/skills/builder/odoo/SKILL.md +498 -0
  24. package/skills/{codemation-plugin-development → builder/plugin-development}/SKILL.md +4 -7
  25. package/skills/{codemation-plugin-development → builder/plugin-development}/references/plugin-anatomy.md +36 -15
  26. package/skills/{codemation-plugin-development → builder/plugin-development}/references/plugin-structure.md +2 -2
  27. package/skills/builder/rest-node/SKILL.md +148 -0
  28. package/skills/builder/testing/SKILL.md +142 -0
  29. package/skills/builder/workflow-dsl/SKILL.md +493 -0
  30. package/skills/builder/workspace-files/SKILL.md +191 -0
  31. package/skills/concierge/credentials/SKILL.md +91 -0
  32. package/skills/concierge/intake-automation-playbook/SKILL.md +78 -0
  33. package/skills/concierge/scenario-invoice-to-accounting/SKILL.md +48 -0
  34. package/skills/concierge/scenario-procurement-intake/SKILL.md +58 -0
  35. package/skills/codemation-ai-agent-node/SKILL.md +0 -66
  36. package/skills/codemation-ai-agent-node/references/anti-patterns.md +0 -11
  37. package/skills/codemation-credential-development/SKILL.md +0 -57
  38. package/skills/codemation-custom-node-development/SKILL.md +0 -61
  39. package/skills/codemation-custom-node-development/references/credential-aware-nodes.md +0 -38
  40. package/skills/codemation-custom-node-development/references/define-batch-node.md +0 -38
  41. package/skills/codemation-workflow-dsl/SKILL.md +0 -78
  42. package/skills/codemation-workflow-dsl/references/builder-patterns.md +0 -120
  43. package/skills/codemation-workflow-dsl/references/complete-example.md +0 -263
  44. package/skills/codemation-workflow-dsl/references/workflow-testing.md +0 -194
  45. /package/skills/{codemation-cli → builder/cli}/references/command-map.md +0 -0
  46. /package/skills/{codemation-framework-concepts → builder/framework-concepts}/references/architecture-map.md +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemation/agent-skills",
3
- "version": "0.3.0",
3
+ "version": "0.5.1",
4
4
  "description": "Reusable agent skills for Codemation projects and plugin development.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -51,6 +51,8 @@
51
51
  "changeset:verify": "pnpm --workspace-root run changeset:verify",
52
52
  "build": "pnpm build:metadata",
53
53
  "build:metadata": "tsx ../../tooling/discovery/scripts/extract-metadata.ts",
54
+ "typecheck": "node scripts/verify-skill-code.mjs",
55
+ "verify-skills": "node scripts/verify-skill-code.mjs",
54
56
  "test": "vitest run",
55
57
  "test:unit": "vitest run"
56
58
  }
@@ -0,0 +1,314 @@
1
+ ---
2
+ name: ai-agent
3
+ description: Add an LLM step to a workflow with the AIAgent node — classification, extraction, drafting, or tool-using reasoning. It runs one chat completion per item and emits the model's reply on its port. Read this when a step needs an LLM (and prefer a plain Callback when the logic is deterministic).
4
+ compatibility: Designed for Codemation workflows authored with @codemation/core-nodes.
5
+ tags: agent, llm, ai, aiagent, classification, extraction
6
+ uses: "@codemation/core-nodes, @codemation/core"
7
+ ---
8
+
9
+ # Codemation AI Agent Node
10
+
11
+ `AIAgent` is the single building block for any LLM step in a workflow. It receives items, runs one chat completion **per item** using the configured `chatModel` and `messages`, and emits the result on its `main` port — always exactly one output item per input item (it never fans out or filters). Reach for it when a step needs an LLM: classification, extraction, summarisation, drafting, or a decision. For deterministic logic use a plain `Callback` instead (cheaper, faster, no LLM billing). See `workflow-dsl` for the surrounding workflow chain.
12
+
13
+ ## The node — one-liner
14
+
15
+ `AIAgent<TInput, TOutput>` takes an options object. `messages` are `{ role, content }` lines; `content` is a string or a function `({ item, itemIndex, items, ctx }) => string`. Parameterise the generics so `item.json` is typed inside the content callbacks.
16
+
17
+ ```typescript
18
+ import { AIAgent, CodemationChatModelConfig } from "@codemation/core-nodes";
19
+
20
+ type Mail = { body: string };
21
+
22
+ // No outputSchema → output shape is NON-DETERMINISTIC: JSON.parse succeeds → the parsed object;
23
+ // JSON.parse fails → { output: string }. Always set outputSchema (see below).
24
+ const classify = new AIAgent<Mail>({
25
+ name: "Classify email",
26
+ id: "classify-email",
27
+ messages: [
28
+ { role: "system", content: "Classify the email as spam or not-spam. Reply with one word." },
29
+ { role: "user", content: ({ item }) => item.json.body },
30
+ ],
31
+ chatModel: new CodemationChatModelConfig("Managed AI", "low"),
32
+ });
33
+ ```
34
+
35
+ ## chatModel — managed is the default
36
+
37
+ **Managed (default — no API key needed):** `CodemationChatModelConfig(label, complexity)`. The LLM broker auto-authenticates via the workspace HMAC pairing — no API key, no credential slot, no user setup. Do **not** tell managed users to "get an API key". The first argument is just a **neutral display label** for the node (e.g. `"Managed AI"`) — never put a provider or model name (`"Claude"`, `"GPT"`, …) in it, because the framework does **not** choose the provider: the broker does, from the complexity token. The second argument is that **complexity token** (`"low" | "medium" | "high" | "xhigh"`), **not** a model id — the broker maps the token to a concrete provider model and thinking effort. Never hard-code a model id; there is no live list to discover and no endpoint to call.
38
+
39
+ ```typescript
40
+ import { CodemationChatModelConfig } from "@codemation/core-nodes";
41
+
42
+ const cheap = new CodemationChatModelConfig("Managed AI", "low");
43
+ ```
44
+
45
+ **BYOK (self-hosted / non-managed only):** `OpenAIChatModelConfig(label, modelId, slotKey)` — it creates a credential slot the operator must bind with an API key. Only use this where no managed broker is available; in managed mode it prompts the user to bind a key they don't need.
46
+
47
+ ```typescript
48
+ import { OpenAIChatModelConfig } from "@codemation/core-nodes";
49
+
50
+ const gpt = new OpenAIChatModelConfig("OpenAI", "gpt-4.1-mini", "openai"); // creates a bindable "openai" slot
51
+ ```
52
+
53
+ `chatModel` does **not** accept a plain string — pass a config instance.
54
+
55
+ ## Structured output and the downstream shape
56
+
57
+ Without `outputSchema` the output shape is **non-deterministic**: internally `AgentOutputFactory.fromAgentContent` does `JSON.parse(content)` — if that succeeds the output json is the parsed object; if it throws (plain-text reply) the fallback is `{ output: content }`. This means you can't reliably read `item.json.output` unless you also own the prompt.
58
+
59
+ **Prefer `outputSchema` — read typed fields directly; do NOT hand-parse `output`.** With `outputSchema` (a Zod object) the model is instructed to reply with structured JSON, the response is validated and parsed, and the output json **is the parsed object** — read its fields directly (`item.json.sentiment`, not `item.json.output`). Match the `AIAgent<TInput, TOutput>` second generic to the schema so the downstream node is correctly typed.
60
+
61
+ ```typescript
62
+ import { z } from "zod";
63
+ import { AIAgent, CodemationChatModelConfig } from "@codemation/core-nodes";
64
+
65
+ type Feedback = { text: string };
66
+ const VerdictSchema = z.object({
67
+ sentiment: z.enum(["positive", "neutral", "negative"]),
68
+ topic: z.string(),
69
+ });
70
+ type Verdict = z.infer<typeof VerdictSchema>;
71
+
72
+ // outputSchema → output json IS the parsed object; read item.json.sentiment / item.json.topic directly.
73
+ new AIAgent<Feedback, Verdict>({
74
+ name: "Classify feedback",
75
+ id: "classify-feedback",
76
+ messages: [
77
+ { role: "system", content: 'Reply with strict JSON {"sentiment","topic"}.' },
78
+ { role: "user", content: ({ item }) => item.json.text },
79
+ ],
80
+ chatModel: new CodemationChatModelConfig("Managed AI", "low"),
81
+ outputSchema: VerdictSchema,
82
+ guardrails: { maxTurns: 2 },
83
+ });
84
+ // Downstream reads item.json.sentiment / item.json.topic — `output` does not exist when a schema is set.
85
+ ```
86
+
87
+ ## Tools and MCP servers
88
+
89
+ An `AIAgent` becomes a reasoning loop when it has **tools**. The model calls tools mid-turn; `guardrails.maxTurns` caps the loop. Three tool kinds, in priority order:
90
+
91
+ ### (a) Node-backed tools — `AgentToolFactory.asTool` (preferred for Codemation integrations)
92
+
93
+ Wrap any Codemation node as an agent tool so the real node does the ERP/service RPC — not a hand-rolled `execute` body. The node config carries the static parts (model, fields); `mapInput` maps the agent's tool-call args to the node's item input; `mapOutput` maps the node's output back to the tool result. Use `onRejected: "halt"` for HITL nodes (e.g. `inboxApproval`).
94
+
95
+ ```typescript
96
+ import { AIAgent, CodemationChatModelConfig, inboxApproval } from "@codemation/core-nodes";
97
+ import { AgentToolFactory, callableTool } from "@codemation/core";
98
+ import { z } from "zod";
99
+
100
+ type AnalysisInput = { text: string };
101
+
102
+ // (a) Node-backed: wrap inboxApproval so the agent can escalate to a human.
103
+ const escalateToHuman = AgentToolFactory.asTool(
104
+ inboxApproval.create(
105
+ {
106
+ title: ({ item }: { item: { json: unknown } }) => String((item.json as { title?: unknown }).title ?? "Review"),
107
+ body: ({ item }: { item: { json: unknown } }) => String((item.json as { body?: unknown }).body ?? ""),
108
+ priority: "normal",
109
+ timeout: "8h",
110
+ onTimeout: "halt",
111
+ },
112
+ "Human escalation",
113
+ ),
114
+ {
115
+ name: "escalate_to_human",
116
+ description: "Pause and ask a human reviewer to approve or reject. Halt if rejected.",
117
+ onRejected: "halt",
118
+ inputSchema: z.object({
119
+ title: z.string().describe("Short summary for the reviewer"),
120
+ reason: z.string().describe("Why this needs human review"),
121
+ }),
122
+ outputSchema: z.object({ approved: z.boolean(), note: z.string().optional() }),
123
+ mapInput: ({ input, item }) => ({
124
+ json: {
125
+ ...((item.json as Record<string, unknown>) ?? {}),
126
+ title: input.title,
127
+ body: input.reason,
128
+ },
129
+ }),
130
+ mapOutput: ({ outputs }: { outputs: { main?: ReadonlyArray<{ json: unknown }> } }) => {
131
+ const first = outputs.main?.[0]?.json as { decision?: { status?: string; note?: string } } | undefined;
132
+ return { approved: first?.decision?.status === "approved", note: first?.decision?.note };
133
+ },
134
+ },
135
+ );
136
+
137
+ // (b) Inline callable: custom logic without a Codemation node.
138
+ const lookup_rate = callableTool({
139
+ name: "lookup_rate",
140
+ description: "Look up the current EUR/USD exchange rate.",
141
+ inputSchema: z.object({ pair: z.string() }),
142
+ outputSchema: z.object({ rate: z.number() }),
143
+ execute: async () => ({ rate: 1.08 }),
144
+ });
145
+
146
+ const SyncOutput = z.object({ decision: z.string(), note: z.string().optional() });
147
+ type SyncOutputT = z.infer<typeof SyncOutput>;
148
+
149
+ // Agent with both tool kinds — model reasons, calls tools as needed, emits structured output.
150
+ new AIAgent<AnalysisInput, SyncOutputT>({
151
+ name: "Analysis agent",
152
+ id: "analysis-agent",
153
+ messages: [
154
+ {
155
+ role: "system",
156
+ content:
157
+ "Analyse the text. If you need current exchange rates call lookup_rate. " +
158
+ "If the text describes a critical unresolvable issue, call escalate_to_human. " +
159
+ "Reply with { decision, note }.",
160
+ },
161
+ { role: "user", content: ({ item }) => item.json.text },
162
+ ],
163
+ chatModel: new CodemationChatModelConfig("Managed AI", "medium"),
164
+ tools: [escalateToHuman, lookup_rate],
165
+ outputSchema: SyncOutput,
166
+ guardrails: { maxTurns: 10 },
167
+ });
168
+ export {};
169
+ ```
170
+
171
+ Key constraints:
172
+
173
+ - `mapInput` returns `{ json: <node-input> }` (an `Item`) or a bare `<node-input>` object. Use the `{ json: {} }` form when you need to merge additional fields.
174
+ - `mapOutput` receives `{ outputs }` where `outputs.main` is the node's output items. Always handle the empty-array case (`outputs.main?.[0]?.json`) — some nodes (e.g. `odooQueryNode`) emit zero items on no-match, which throws without a custom `mapOutput`.
175
+ - `outputSchema` of `asTool` is the **tool result** schema (what the agent sees back), not the node's own output type.
176
+
177
+ ### (b) Inline callable tools — `callableTool`
178
+
179
+ For custom logic with no Codemation node behind it. Declare a credential slot if the execute body needs auth:
180
+
181
+ ```typescript
182
+ import { callableTool } from "@codemation/core";
183
+ import { z } from "zod";
184
+
185
+ const ping = callableTool({
186
+ name: "ping",
187
+ description: "Check that a URL is reachable.",
188
+ inputSchema: z.object({ url: z.string().url() }),
189
+ outputSchema: z.object({ ok: z.boolean() }),
190
+ execute: async ({ input }) => {
191
+ const res = await fetch(input.url, { method: "HEAD" });
192
+ return { ok: res.ok };
193
+ },
194
+ });
195
+ export {};
196
+ ```
197
+
198
+ ### (c) MCP servers — `mcpServers`
199
+
200
+ For tool access to services registered on the control plane, pass `mcpServers: ["<server-id>"]`. The agent gets `find_tools` auto-injected when `mcpServers` is set — but **the agent will not use it unless you tell it to** in the system `messages`. Add this line to the system prompt:
201
+
202
+ ```text
203
+ Use find_tools(query) to discover available tools before calling them. Never call a tool name you have not seen returned by find_tools.
204
+ ```
205
+
206
+ Node-backed and `callableTool` tools in `tools: [...]` are always present — no `find_tools` needed for them. Soft limit: 8 pinned MCP tools (`pinnedMcpTools`); hard limit: 16.
207
+
208
+ ## One realistic complete example
209
+
210
+ Manual trigger → classify each feedback item with a managed model → tag the result downstream. Uses `outputSchema` so the downstream `MapData` reads `item.json.sentiment` directly — no hand-parsing, no non-determinism.
211
+
212
+ ```typescript
213
+ import { z } from "zod";
214
+ import {
215
+ createWorkflowBuilder,
216
+ ManualTrigger,
217
+ AIAgent,
218
+ MapData,
219
+ CodemationChatModelConfig,
220
+ } from "@codemation/core-nodes";
221
+
222
+ type Feedback = { text: string };
223
+ const SentimentSchema = z.object({ sentiment: z.enum(["positive", "neutral", "negative"]) });
224
+ type SentimentOutput = z.infer<typeof SentimentSchema>;
225
+
226
+ export default createWorkflowBuilder({ id: "wf.classify-feedback", name: "Classify customer feedback" })
227
+ .trigger(
228
+ new ManualTrigger<Feedback>("Classify feedback", [
229
+ { text: "The new onboarding flow is really intuitive — great job!" },
230
+ { text: "Couldn't find the export button anywhere, very confusing." },
231
+ ]),
232
+ )
233
+ .then(
234
+ new AIAgent<Feedback, SentimentOutput>({
235
+ name: "Classify feedback",
236
+ id: "classify-feedback-agent",
237
+ messages: [
238
+ { role: "system", content: 'Reply with strict JSON {"sentiment":"positive"|"neutral"|"negative"}.' },
239
+ { role: "user", content: ({ item }) => `Feedback: ${item.json.text}` },
240
+ ],
241
+ chatModel: new CodemationChatModelConfig("Managed AI", "low"),
242
+ outputSchema: SentimentSchema,
243
+ guardrails: { maxTurns: 1 },
244
+ }),
245
+ )
246
+ .then(
247
+ new MapData<SentimentOutput, { label: string }>("Tag sentiment", (item) => ({ label: item.json.sentiment }), {
248
+ id: "tag-sentiment",
249
+ }),
250
+ )
251
+ .build();
252
+ ```
253
+
254
+ ## Choosing the model
255
+
256
+ Pick by the complexity and size of the content being processed — not by habit. The second arg to `CodemationChatModelConfig` is a **complexity token**; the broker maps it to a concrete model and thinking effort.
257
+
258
+ | Tier | Use when | Complexity token |
259
+ | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- |
260
+ | **Small/cheap** | Short, simple text: classification, triage, single-field extraction from a clean snippet. | `"low"` |
261
+ | **Mid** | Long or complex content: order PDFs, multi-page invoices, multi-field extraction, documents that would tax a smaller context. A `"low"` model may truncate or underperform on large inputs. | `"medium"` |
262
+ | **Heavy** | Multi-step reasoning loops, tricky multi-tool decisions, demanding extraction where `"medium"` is unreliable. | `"high"` |
263
+ | **Frontier** | The hardest problems where you need the most capable model regardless of cost. | `"xhigh"` |
264
+
265
+ Rule of thumb: if the prompt payload exceeds a paragraph or the output schema has more than two or three fields, step up to `"medium"`. Never hard-code a model id — the token IS the API; the broker picks the concrete model.
266
+
267
+ ```typescript
268
+ import { AIAgent, CodemationChatModelConfig } from "@codemation/core-nodes";
269
+ import { z } from "zod";
270
+
271
+ // Small/cheap: one-field classification of a short text snippet.
272
+ const triage = new AIAgent<{ subject: string }>({
273
+ name: "Triage email",
274
+ id: "triage-email",
275
+ messages: [
276
+ { role: "system", content: "Reply with one word: order, complaint, or other." },
277
+ { role: "user", content: ({ item }) => item.json.subject },
278
+ ],
279
+ chatModel: new CodemationChatModelConfig("Managed AI", "low"),
280
+ outputSchema: z.object({ category: z.enum(["order", "complaint", "other"]) }),
281
+ });
282
+
283
+ // Larger: multi-field extraction from a full order document (PDF → markdown from OCR node upstream).
284
+ // Step up the complexity token instead of hard-coding a model id.
285
+ const extract = new AIAgent<{ markdown: string }>({
286
+ name: "Extract order",
287
+ id: "extract-order",
288
+ messages: [
289
+ { role: "system", content: "Extract vendor, total, and line items from the order document." },
290
+ { role: "user", content: ({ item }) => item.json.markdown },
291
+ ],
292
+ chatModel: new CodemationChatModelConfig("Managed AI", "medium"),
293
+ outputSchema: z.object({
294
+ vendor: z.string(),
295
+ total: z.number(),
296
+ lines: z.array(z.object({ description: z.string(), amount: z.number() })),
297
+ }),
298
+ guardrails: { maxTurns: 1 },
299
+ });
300
+ export {};
301
+ ```
302
+
303
+ ## Gotchas
304
+
305
+ - **Set an explicit `id`.** Without one the id derives from `name`; renaming the label re-keys any credential binding (BYOK / MCP). See the id-stability rule in `workflow-dsl`.
306
+ - **Always set `outputSchema`.** Without it the output shape is non-deterministic (parsed JSON object or `{ output: string }` depending on whether the reply parses). With it, read parsed fields directly (`item.json.field`). Never read `item.json.output` when a schema is set, and avoid relying on `output` without a schema.
307
+ - **One output per input.** `AIAgent` never fans out or filters — use `Split`/`Filter` for that.
308
+ - **Don't use `AIAgent` for deterministic logic** — a `Callback` is cheaper and not billed.
309
+ - **Managed = no key.** Never use `OpenAIChatModelConfig` (or "get an API key" guidance) in managed mode; the broker authenticates transparently.
310
+ - **Binaries pass natively — never hand-roll the encoding.** Every attachment on the incoming item's `binary` is sent to the chat model AUTOMATICALLY as an inline multimodal block — images as `image` blocks, every other type (PDF, xlsx, pptx, docx, CSV, JSON, …) as `file`/`document` blocks — so the agent reads the file's actual bytes directly. You prepare nothing: the agent attaches whatever is on `item.binary` for you.
311
+ - **DON'T** add a "convert/encode to base64" node, stringify the bytes into a prompt/message, or OCR-to-text upstream just to feed an agent. Putting a base64 string into a text prompt is the #1 binary mistake — the model gets unreadable text, not a real image/document, and the extra hand-rolled node fails at runtime when the binary isn't where its code looked (`"No binary attachment found on item"`). There is no `Encode`/`Base64` node and you never need one here.
312
+ - **DO (binary from the direct upstream node):** keep the file on `item.binary` and feed that item straight into the `AIAgent` — a Gmail attachment, a file-pool read, or an HTTP download just flows in; the passdown is automatic. (To make an `HttpRequest` node hand you the raw bytes, set `responseFormat: "binary"` on it — it writes the response to `ctx.binary` under `binaryName` (default `"body"`) instead of parsing JSON/text. There is no `responseType` option.)
313
+ - **DO (binary from a NON-adjacent earlier node):** when the bytes were produced several nodes back, not by the direct upstream one, don't thread them through every node by hand — forward them explicitly with the agent's `binaries` option, e.g. `binaries: ({ item }) => item.binary ? Object.values(item.binary) : []` (a `BinaryAttachment[]`, or a `({ item }) => BinaryAttachment[]` selector that pulls the bytes from wherever they live on the item).
314
+ - Binaries are not filtered by type; an unsupported type surfaces a runtime error you can filter out upstream. Set `passBinariesToModel: false` only to deliberately turn the passdown off.
@@ -0,0 +1,24 @@
1
+ # AIAgent anti-patterns (version-specific)
2
+
3
+ ## Do NOT hard-code concrete model ids
4
+
5
+ Do NOT pass a concrete model id (e.g. `"anthropic/claude-..."`, `"gpt-4o"`) to `CodemationChatModelConfig`.
6
+ The second argument is a **complexity token**: `"low" | "medium" | "high" | "xhigh"`.
7
+ The broker maps that token to a concrete provider model. Hard-coded ids will be rejected.
8
+
9
+ ```ts no-check
10
+ // WRONG — concrete model id
11
+ new CodemationChatModelConfig("Managed AI", "anthropic/claude-haiku-4-5-20251001");
12
+
13
+ // CORRECT — complexity token
14
+ new CodemationChatModelConfig("Managed AI", "medium");
15
+ ```
16
+
17
+ ## Do NOT call GET /api/llm/managed-models
18
+
19
+ The model-discovery endpoint has been removed. The complexity token IS the API — no discovery step is needed or available.
20
+
21
+ ## `chatModel` string shorthand is not supported on AIAgent
22
+
23
+ `AIAgent` does not accept a plain string for `chatModel` — only `CodemationChatModelConfig` or `OpenAIChatModelConfig` instances.
24
+ (The string shorthand `model: "openai:gpt-4o-mini"` works on the `.agent(...)` fluent DSL helper only.)
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: codemation-cli
2
+ name: cli
3
3
  description: Guides Codemation CLI work for consumer apps and framework-author development. Use when the user asks about `codemation dev`, `build`, `serve web`, `serve worker`, `user create`, `user list`, `--consumer-root`, `.codemation/output`, or consumer versus framework-author mode.
4
4
  compatibility: Designed for Codemation repositories and projects that use the Codemation CLI.
5
5
  tags: cli, dev
@@ -7,15 +7,8 @@ tags: cli, dev
7
7
 
8
8
  # Codemation CLI
9
9
 
10
- ## Mental model
11
-
12
10
  The CLI is a thin orchestrator: it loads `codemation.config.ts`, delegates to `@codemation/host` (web server) and worker packages, and manages build artifacts in `.codemation/output/`. It owns no workflow logic. There are two modes: **consumer mode** (`codemation dev`) runs a stable packaged UI against the consumer's workflows; **framework-author mode** (`codemation dev --watch-framework`) enables `next-host` HMR for monorepo development.
13
11
 
14
- ## When to use / when NOT
15
-
16
- Use this skill for command selection, local development flow, and CLI troubleshooting.
17
- Do not use for workflow graph design, custom node implementation, or credential modeling unless the CLI command is the core question.
18
-
19
12
  ## Quickstart
20
13
 
21
14
  ```
@@ -0,0 +1,191 @@
1
+ ---
2
+ name: connect-external-systems
3
+ description: The decision guide for connecting a workflow step to ANY external system — an ERP, CRM, mailbox, or any HTTP/REST API. Follow this EVERY time a step talks to a service outside the workflow: pick the least-code route in strict priority order (specialized node → MCP server → defineRestNode → hand-rolled Callback). Read this before authoring any integration step.
4
+ compatibility: Cross-vertical guidance for Codemation workflows. No specific package required.
5
+ tags: integration, external, api, http, rest, mcp, connect, erp, crm, credential, routing
6
+ uses: "@codemation/core-nodes, @codemation/core"
7
+ ---
8
+
9
+ # Connecting to external systems — the routing hierarchy
10
+
11
+ Whenever a workflow step must talk to a system outside the workflow (an ERP, CRM, mailbox, payment
12
+ provider, any HTTP/REST or JSON-RPC API), pick the **least-code route** that works, in this **strict
13
+ priority order**. Always start at route 1 and only descend when the route above genuinely doesn't apply.
14
+
15
+ > **The rule, plainly: never hand-roll an external API in a `Callback` when a specialized node, an MCP
16
+ > server, or `defineRestNode` can do it.** A hand-rolled `fetch`/JSON-RPC call is the last resort only —
17
+ > it is considered non-production-grade whenever any route above it is available (no declared credential
18
+ > slot, no shared session, no inspector surface, easy to get auth/retries wrong).
19
+
20
+ **Discipline:** run this decision tree for **every** external step before you write code. Then author
21
+ the step from the route's deeper skill, run `verify_workflow`, and fix only what it flags.
22
+
23
+ ## The decision tree
24
+
25
+ ```text
26
+ Step talks to an external system?
27
+
28
+ ├─ 1. Is there a SPECIALIZED NODE for this service?
29
+ │ discover: search_skills("<service>") → e.g. odoo / gmail / msgraph
30
+ │ yes → use that package's nodes (they declare the credential slot for you). DONE.
31
+
32
+ ├─ 2. Is there an MCP SERVER for this service on the control plane?
33
+ │ discover: list_mcp_servers (or GET /api/registry/capabilities?query=<service>)
34
+ │ yes → drive it from an AIAgent: new AIAgent({ ..., mcpServers: ["<id>"] }). DONE.
35
+
36
+ ├─ 3. Is it a plain REST/HTTP JSON API (no node, no MCP)?
37
+ │ yes → wrap one endpoint with defineRestNode({ api, credentials, request, response }). DONE.
38
+
39
+ └─ 4. None of the above fits (multi-step JSON-RPC, streaming, an SDK with no REST surface)?
40
+ LAST RESORT → hand-roll fetch / JSON-RPC inside a Callback, resolving auth via
41
+ ctx.getCredential(...). Justify why routes 1–3 didn't apply.
42
+ ```
43
+
44
+ ## Route 1 — specialized node (preferred)
45
+
46
+ If a node package exists for the service, use it. It ships typed nodes, declares the credential slot for
47
+ you, and encodes the service's quirks. **Discover it before assuming it doesn't exist:**
48
+
49
+ ```text
50
+ search_skills("odoo") → odoo skill: odooQueryNode / odooCreateNode / call_kw, slot "odoo"
51
+ search_skills("gmail") → gmail skill: OnNewGmailTrigger / ReplyToGmailMessage, slot "auth"
52
+ search_skills("msgraph") → Microsoft Graph (Outlook / Teams / SharePoint) nodes
53
+ ```
54
+
55
+ When `search_skills` returns a matching integration skill, open it and author from its nodes. Deeper
56
+ skills: **`odoo`** (ERP — match/create sale orders, partners, products), **`gmail`** (mailbox),
57
+ **`msgraph`** (Microsoft 365). This is always the least-code, most-correct option — never reach past a
58
+ specialized node to a hand-rolled call.
59
+
60
+ ## Route 2 — MCP server on the control plane
61
+
62
+ No specialized node, but the service may be reachable as an **MCP server** registered on the control
63
+ plane. An `AIAgent` step can then call its tools. **Discover the server id first — never guess it:**
64
+
65
+ ```text
66
+ list_mcp_servers
67
+ GET /api/registry/capabilities?query=<service> → { id, displayName, acceptedCredentialTypes }
68
+ ```
69
+
70
+ Pass the discovered `id` into the agent's `mcpServers` array. The credential binds on the materialized
71
+ MCP connection node via the canvas dropdown (the operator binds; you only declare):
72
+
73
+ ```typescript
74
+ import { AIAgent, CodemationChatModelConfig } from "@codemation/core-nodes";
75
+
76
+ type Ticket = { summary: string };
77
+
78
+ // Drive a CP-registered MCP server's tools from an LLM step. "<server-id>" comes from
79
+ // list_mcp_servers / the capabilities registry — do not invent it.
80
+ const triage = new AIAgent<Ticket>({
81
+ name: "Triage via MCP",
82
+ id: "triage-via-mcp",
83
+ messages: [
84
+ { role: "system", content: "Use the available tools to look up and update the ticket." },
85
+ { role: "user", content: ({ item }) => item.json.summary },
86
+ ],
87
+ chatModel: new CodemationChatModelConfig("Managed AI", "low"),
88
+ mcpServers: ["<server-id>"],
89
+ });
90
+ ```
91
+
92
+ Deeper skills: **`mcp-capabilities`** (discovery + credential rules) and **`ai-agent`** (the `AIAgent`
93
+ node, `mcpServers`, tools, and structured output).
94
+
95
+ ## Route 3 — `defineRestNode` for a plain REST API
96
+
97
+ No specialized node and no MCP server, but the service is a plain REST/HTTP JSON API. Wrap the endpoint
98
+ with **`defineRestNode`** — declare `api` (base URL, path, method), a `credentials` slot, and the
99
+ `request`/`response` mappers, then use `node.create(...)` in the builder. This is strongly preferred over
100
+ a raw `fetch`: you get a typed node, a declared credential slot, a shared HTTP session, and SSRF guards
101
+ for free. Deeper skill: **`rest-node`** (full API + a complete example).
102
+
103
+ ## Route 4 — hand-rolled `Callback` (last resort)
104
+
105
+ Only when none of routes 1–3 can express the call — e.g. a multi-step JSON-RPC handshake, a streaming
106
+ protocol, or an SDK with no REST surface — hand-roll the call inside a `Callback`, resolving auth via
107
+ `ctx.getCredential<T>(slotKey)` for a slot a credentialed node owns. State why routes 1–3 didn't apply.
108
+ A plain `Callback` slot is not itself bindable — see `workflow-dsl` (Credentials) for the pattern.
109
+
110
+ ## Iterative / fuzzy matching: prefer an AIAgent with that integration's nodes as tools
111
+
112
+ When a step must **match real-world text against integration records** (fuzzy company names, partial product codes, contacts that may or may not exist), a deterministic node chain fans out, loses the canonical payload, and fabricates ids on no-match. Instead, author an `AIAgent` whose `tools` are that integration's nodes wrapped via `AgentToolFactory.asTool(node.create(...), { mapInput, mapOutput })` — the agent calls one search per entity, reasons over the results, links matched ids, and explicitly reports what it cannot resolve. See the `odoo` skill for the full node-backed tool pattern (and the `ai-agent` skill for `AgentToolFactory.asTool` mechanics).
113
+
114
+ ## Anti-patterns
115
+
116
+ - **Don't hand-roll `fetch`/JSON-RPC when a route above works.** A specialized node, MCP server, or
117
+ `defineRestNode` is always preferred — the raw `Callback` is the last resort, not the default.
118
+ - **Don't skip discovery.** Run `search_skills` and `list_mcp_servers` _before_ concluding "there's no
119
+ node/server for this" — guessing leads straight to an unnecessary hand-rolled call.
120
+ - **Don't put credentials in code.** Every route declares a bindable slot; the operator binds the
121
+ instance via the canvas. Never inline secrets or hard-code tokens.
122
+ - **Never wire to fabricated external resource IDs.** Gmail label IDs, Drive folder IDs, ERP record IDs,
123
+ and any other external-system identifiers must come from config, a credential, or a prior lookup node.
124
+ If the ID isn't guaranteed to exist at runtime, **omit the feature or flag it for human setup** —
125
+ shipping a workflow wired to a hard-coded ID not guaranteed to exist is worse than a missing feature.
126
+ Use a placeholder comment (`// TODO: bind real label id from config`) or a config field instead.
127
+
128
+ ## Build what works; report what's missing — and never claim what you didn't wire
129
+
130
+ **If you don't have all the required information at build time, do NOT invent it.** Build everything
131
+ that DOES work with what you know, and explicitly surface every gap so the concierge can ask the user
132
+ for the missing detail.
133
+
134
+ **Don't claim/comment functionality you didn't wire.** If a comment says "includes order_line
135
+ commands" but the code doesn't actually build them, remove the comment. A misleading comment is
136
+ worse than silence: it tells the next agent (and the operator) the step works when it doesn't.
137
+ The rule: every claim in a comment must be true of the code immediately below it. If you INTEND
138
+ to wire something but haven't, either wire it now OR omit it and call `record_decision` to surface
139
+ the gap — never write a claim that the code doesn't back.
140
+
141
+ **A `callableTool` whose `execute` is a stub is the same defect as claiming unwired functionality.**
142
+ A `callableTool` with `execute: async () => ({ ok: true })`, a `// TODO` for the real call, or
143
+ any `execute` body that doesn't perform the intended external operation is a runtime lie — it builds
144
+ and typechecks but does nothing. For any real external or ERP operation, use a NODE-BACKED tool
145
+ (`AgentToolFactory.asTool(theNode.create(...), { mapInput, mapOutput })`) so the specialized node
146
+ performs the call. If you genuinely cannot wire the operation (missing credential, unreachable
147
+ endpoint), omit the tool entirely and call `record_decision` to surface the gap — never ship a
148
+ stubbed `callableTool` standing in for a real integration call.
149
+
150
+ **What counts as "invented":** a Gmail label ID that doesn't come from the user, an ERP record ID you
151
+ don't know, a folder path you assume, a queue name you guessed. Any identifier that will fail at
152
+ runtime if the real account/system is different.
153
+
154
+ **What to do instead:**
155
+
156
+ 1. **Use runtime-resolved references when the node supports it.** Gmail's `addLabels`/`removeLabels`
157
+ (on `ModifyGmailLabels`) and `labelIds` (on `OnNewGmailTrigger`) accept display names — the
158
+ plugin resolves them at runtime. Prefer those over guessed IDs. Check the target node's type
159
+ definition before assuming you need a raw ID.
160
+
161
+ 2. **If no runtime-resolved reference exists,** build the surrounding workflow but declare the missing
162
+ value as a named required constant at the top of the file with a `// TODO(setup): <what's needed>`
163
+ comment, so the gap is unmissable:
164
+
165
+ ```text
166
+ // TODO(setup): replace with the real Jira project key from the customer's account
167
+ const JIRA_PROJECT_KEY = "TODO_REPLACE";
168
+ ```
169
+
170
+ 3. **Call `record_decision` to surface the gap** to the concierge and the build report:
171
+
172
+ ```text
173
+ record_decision({
174
+ choice: "Left JIRA_PROJECT_KEY as TODO_REPLACE — user must supply their Jira project key",
175
+ why: "No project key was provided; inventing one would break every run on the real account.",
176
+ })
177
+ ```
178
+
179
+ 4. **Do NOT omit the entire feature.** Build the node/step with the placeholder; the concierge can
180
+ bind or configure the real value once the user provides it. An incomplete-but-correct skeleton is
181
+ always better than a fully wired but runtime-broken workflow.
182
+
183
+ The eval scoring criteria: a workflow that builds AND explicitly reports missing info is CORRECT. A
184
+ workflow that fabricates values that will fail at runtime is WRONG regardless of how "complete" it looks.
185
+
186
+ ## Read next when needed
187
+
188
+ - `odoo` / `gmail` / `msgraph` — route 1 specialized-node skills.
189
+ - `mcp-capabilities` + `ai-agent` — route 2: discover MCP servers and drive them from `AIAgent`.
190
+ - `rest-node` — route 3: wrap a plain REST endpoint with `defineRestNode`.
191
+ - `workflow-dsl` — the surrounding builder, and the `Callback` + `ctx.getCredential` pattern for route 4.
@@ -0,0 +1,86 @@
1
+ ---
2
+ name: credential-development
3
+ description: Authors a Codemation credential type with defineCredential(...) — public config, secret material, a typed createSession(...), and a test(...) probe — plus how nodes request it via named slots. Use when creating or updating custom credentials or credential-aware nodes.
4
+ tags: credential, oauth, plugin
5
+ ---
6
+
7
+ # Codemation Credential Development
8
+
9
+ A credential type is a schema + runtime adapter: it declares `public` config (e.g. base URL), `secret` material (e.g. tokens), a `createSession(...)` factory that returns the typed object nodes consume, and a `test(...)` function for pre-activation validation. Nodes declare named credential slots; operators bind concrete instances to those slots in the UI. The binding key is `(workflowId, nodeId, slotKey)`.
10
+
11
+ ## Quickstart
12
+
13
+ `defineCredential` takes a `public`/`secret` field map (or a Zod object), `createSession(args)` returning the runtime session, and `test(args)` returning a `CredentialHealth`. `args` is `{ publicConfig, material }`. This API-key type injects the key as a header and probes the endpoint in `test`:
14
+
15
+ ```ts
16
+ import { defineCredential } from "@codemation/core";
17
+ import type { CredentialSession, HttpCredentialDelta } from "@codemation/core-nodes";
18
+
19
+ export const weatherApiKeyCredentialType = defineCredential({
20
+ key: "example.weather-api-key",
21
+ label: "Weather API Key",
22
+ public: {
23
+ keyHeader: { label: "Header name", type: "string" as const, placeholder: "X-Api-Key" },
24
+ },
25
+ secret: {
26
+ apiKey: { label: "API Key", type: "password" as const, required: true },
27
+ },
28
+ createSession(args): CredentialSession {
29
+ const apiKey = String(args.material.apiKey ?? "");
30
+ const headerName = String(args.publicConfig.keyHeader ?? "X-Api-Key").trim() || "X-Api-Key";
31
+ return {
32
+ applyToRequest: (): HttpCredentialDelta => ({ headers: { [headerName]: apiKey } }),
33
+ };
34
+ },
35
+ async test(args) {
36
+ const apiKey = String(args.material.apiKey ?? "").trim();
37
+ if (!apiKey) {
38
+ return { status: "failing", message: "API key is empty.", testedAt: new Date().toISOString() };
39
+ }
40
+ const resp = await fetch("https://api.example.com/me", { headers: { "X-Api-Key": apiKey } });
41
+ return resp.status === 401 || resp.status === 403
42
+ ? {
43
+ status: "failing",
44
+ message: `API returned ${resp.status} — key is invalid.`,
45
+ testedAt: new Date().toISOString(),
46
+ }
47
+ : { status: "healthy", message: "API key accepted.", testedAt: new Date().toISOString() };
48
+ },
49
+ });
50
+ ```
51
+
52
+ Register it with `defineCodemationApp({ credentials: [...] })` or `definePlugin({ credentials: [...] })`, then wire it onto a node — for `HttpRequest`, `credentialSlot: { name: "apiKey", acceptedTypes: [weatherApiKeyCredentialType] }`.
53
+
54
+ ## What `test()` does and why it matters
55
+
56
+ Every credential type must implement `test(args)`. It is called:
57
+
58
+ - When the operator clicks **Connect** in the credential dialog (validates before saving).
59
+ - Before a workflow activates (blocks activation on failing credentials).
60
+
61
+ `test()` receives the same `{ publicConfig, material }` args as `createSession()`. It must return `{ status: "healthy" | "failing", message, testedAt }`. A "failing" result blocks workflow activation and surfaces the `message` to the operator — use it to give actionable guidance ("API key is empty", "Endpoint returned 401 — key is invalid").
62
+
63
+ Implement `test()` as a cheap probe against the real service when possible (e.g. a `/health` or `/me` call). At minimum, validate that required secret fields are non-empty. Do NOT call `createSession()` from inside `test()` — test independently so credential issues are caught before runtime.
64
+
65
+ ## Authoring rules
66
+
67
+ 1. Start with `defineCredential(...)`.
68
+ 2. Keep `public` versus `secret` fields intentional.
69
+ 3. Make `createSession(...)` return the typed runtime object the node actually needs.
70
+ 4. Implement `test(...)` so failure states are explicit before workflow activation.
71
+ 5. Register credential types at the app or plugin boundary, not inside random workflow files.
72
+
73
+ ## Decision branches & gotchas
74
+
75
+ **Node integration:** helper-defined nodes declare credentials directly in the `credentials` field; class-based nodes use lower-level credential requirement APIs when needed.
76
+
77
+ **Binding stability:** the `nodeId` defaults to a slug of the node's `name` label. Renaming a credential-using node's label silently changes its id and orphans the binding in the UI. To prevent this, set an explicit `id:` on credential-using node configs so the id is decoupled from the label.
78
+
79
+ ## Anti-patterns
80
+
81
+ - Do not hard-code secrets in node implementation — use credential slots.
82
+ - Do not register credential types inside workflow files — use the app or plugin composition root.
83
+
84
+ ## Read next when needed
85
+
86
+ - Read `references/credential-patterns.md` for schema, registration, and slot guidance.