@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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: codemation-framework-concepts
3
- description: Explains Codemation package boundaries, runtime concepts, observability shape, and the normal consumer mental model. Use when the user asks where code belongs across `@codemation/core`, `@codemation/host`, `@codemation/next-host`, `@codemation/cli`, workflows, plugins, credentials, activation, telemetry, or runtime modes.
3
+ description: Explains Codemation package boundaries, runtime concepts, observability shape, and the normal consumer mental model. Use when the user asks where code belongs across `@codemation/core`, `@codemation/host`, `@codemation/next-host`, `@codemation/cli`, workflows, plugins, credentials, activation, telemetry, or runtime modes. Read this first when starting any Codemation task — it points at the right skill for the work.
4
4
  compatibility: Designed for Codemation apps, plugins, and framework contributors.
5
5
  ---
6
6
 
@@ -30,6 +30,7 @@ Do not use this skill as a substitute for detailed CLI, workflow DSL, or plugin
30
30
  - activation is framework-managed and happens in the UI
31
31
  - telemetry is observability-first: traces, spans, artifacts, and metric points are framework-owned runtime data
32
32
  - run retention and telemetry retention can differ, so trend data can outlive raw run state
33
+ - **workflow testing** is a first-class primitive: a `TestTrigger` node yields one item per test case, the orchestrator dispatches a workflow run per case with `executionOptions.testContext` set, and `Assertion` nodes (`emitsAssertions: true`) record per-run results into `TestAssertion` rows; the canvas exposes a Tests tab parallel to Live and Executions
33
34
 
34
35
  ## Runtime rule of thumb
35
36
 
@@ -38,6 +39,16 @@ Do not use this skill as a substitute for detailed CLI, workflow DSL, or plugin
38
39
  3. Keep workflow code stable while the runtime shape grows around it.
39
40
  4. Treat telemetry as part of the runtime contract, not as ad-hoc node-local logging.
40
41
 
42
+ ## Where to go next
43
+
44
+ - Authoring workflows → `codemation-workflow-dsl`
45
+ - Building a reusable node → `codemation-custom-node-development`
46
+ - Building a credential type → `codemation-credential-development`
47
+ - Packaging as a plugin → `codemation-plugin-development`
48
+ - Calling an MCP server from a workflow → `codemation-mcp-capabilities`
49
+ - CLI commands / dev loop → `codemation-cli`
50
+
41
51
  ## Read next when needed
42
52
 
43
53
  - Read `references/architecture-map.md` for package ownership and runtime-mode guidance.
54
+ - Use the `codemation-workflow-dsl` skill (and its `references/workflow-testing.md`) for hands-on test authoring with TestTrigger / IsTestRun / Assertion.
@@ -0,0 +1,85 @@
1
+ ---
2
+ name: codemation-mcp-capabilities
3
+ description: Discover MCP servers registered on the Codemation control plane. Use before authoring agent workflows that reference mcpServers to find available server ids and their credential requirements.
4
+ compatibility: Requires an installation paired with a connected control plane (Sprint 2+).
5
+ ---
6
+
7
+ # Codemation MCP Capabilities
8
+
9
+ ## Use this skill when
10
+
11
+ Use this skill before writing `agent({ mcpServers: ["..."] })` to discover what server ids are
12
+ available and what credential types they require. Without it, you'd have to guess server ids or
13
+ ask the user.
14
+
15
+ ## How to search
16
+
17
+ Call `GET /api/registry/capabilities?query=<search term>` on the control-plane API.
18
+ The endpoint is session-authenticated (the control-plane session cookie is forwarded automatically
19
+ when called from within the workspace's paired context).
20
+
21
+ ```
22
+ GET /api/registry/capabilities?query=gmail
23
+ ```
24
+
25
+ Response shape (array of capability objects):
26
+
27
+ ```json
28
+ [
29
+ {
30
+ "kind": "mcp-server",
31
+ "id": "gmail",
32
+ "displayName": "Gmail",
33
+ "description": "Read, send, and manage Gmail messages and labels.",
34
+ "acceptedCredentialTypes": ["oauth.google.gmail"]
35
+ }
36
+ ]
37
+ ```
38
+
39
+ An empty query string returns all registered servers.
40
+
41
+ ## Response fields
42
+
43
+ | Field | Type | Notes |
44
+ | ------------------------ | -------- | --------------------------------------------------------------------- |
45
+ | `kind` | string | Always `"mcp-server"` for now. Future: `"node"`, `"credential-type"` |
46
+ | `id` | string | Stable slug — add this string to the agent's mcpServers array |
47
+ | `displayName` | string | Human-readable name for UI or explanations |
48
+ | `description` | string | What the server does |
49
+ | `acceptedCredentialTypes`| string[] | Credential type ids accepted by this server (empty = no credential) |
50
+
51
+ ## Credential types
52
+
53
+ - **`"oauth.google.gmail"`** — user must connect a Google account credential instance via the
54
+ credential dialog before the workflow runs. The same credential instance can be shared between
55
+ a `GmailTrigger` node and the Gmail MCP server.
56
+ - **`"bearer_token"`** etc. — user configures a static credential via the credential dialog.
57
+ - **empty array** — no credential required. The server is usable immediately.
58
+
59
+ ## Using results in workflow config
60
+
61
+ The `id` field from the response is added to the agent's `mcpServers` array. Each entry
62
+ surfaces a credential slot on the materialized MCP connection node (same shape as
63
+ ChatModel and Tool connection nodes); the user picks a specific credential instance via
64
+ the canvas credential dropdown — same flow as a trigger credential. A user may have
65
+ multiple instances of the same type (personal vs work Gmail); the dropdown surfaces all
66
+ matching instances.
67
+
68
+ ```ts
69
+ new AIAgent({
70
+ name: "Gmail reader",
71
+ mcpServers: ["gmail"],
72
+ // ...
73
+ });
74
+ ```
75
+
76
+ Bind the credential instance via the UI before activation; there is no inline credential
77
+ field on the workflow definition.
78
+
79
+ ## Example flow
80
+
81
+ 1. User asks: "Build a workflow that reads Gmail and summarises unread messages."
82
+ 2. Call `GET /api/registry/capabilities?query=gmail` → find `id: "gmail"`, `acceptedCredentialTypes: ["oauth.google.gmail"]`.
83
+ 3. Report back: "Gmail MCP is available. The user will need to bind a `oauth.google.gmail` credential instance."
84
+ 4. In the workflow, use `mcpServers: ["gmail"]`.
85
+ 5. The user binds their credential instance via the canvas credential dropdown before activating.
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Reference: using an MCP server in a workflow agent node.
3
+ *
4
+ * Before writing this, call GET /api/registry/capabilities?query=<name> to confirm
5
+ * the server id and credential type. Then list the server id under `mcpServers`.
6
+ *
7
+ * Cron / webhook workflows use createWorkflowBuilder({id, name}).trigger(new XxxTrigger(...))
8
+ * and chain with .then(new SomeNodeConfig(...)). The fluent .map/.if/.agent helpers are
9
+ * only available via workflow("id").manualTrigger(...). See codemation-workflow-dsl skill.
10
+ *
11
+ * `mcpServers` is a plain array of server ids. Each declared server surfaces a credential
12
+ * slot on the materialized MCP connection node (same shape as ChatModel/Tool connection
13
+ * nodes). The user binds a credential instance via the canvas credential dropdown before
14
+ * activation — same flow as trigger credentials.
15
+ */
16
+
17
+ import { AIAgent, CronTrigger, createWorkflowBuilder } from "@codemation/core-nodes";
18
+
19
+ // Example: cron-triggered agent that uses the Gmail MCP server.
20
+ // The "gmail" id comes from the registry (acceptedCredentialTypes: ["oauth.google.gmail"]).
21
+ // The user must have connected their Google account and bound the credential before this runs.
22
+
23
+ export const summariseEmailsWorkflow = createWorkflowBuilder({
24
+ id: "wf.summarise-emails",
25
+ name: "Summarise unread emails",
26
+ })
27
+ .trigger(new CronTrigger("Weekdays at 09:00", { schedule: "0 9 * * 1-5", timezone: "UTC" }))
28
+ .then(
29
+ new AIAgent({
30
+ name: "Summarise",
31
+ mcpServers: ["gmail"],
32
+ messages: [
33
+ {
34
+ role: "system",
35
+ content: [
36
+ "You are an email assistant. Read the user's unread Gmail messages from the last 24 hours.",
37
+ "Summarise each one in one sentence. Output as a bullet list.",
38
+ "Do not draft or send any replies.",
39
+ ].join("\n"),
40
+ },
41
+ ],
42
+ }),
43
+ )
44
+ .build();
@@ -45,6 +45,49 @@ Do not use this skill for ordinary consumer workflow-only changes unless the wor
45
45
 
46
46
  Import **`WorkflowTestKit`** from **`@codemation/core/testing`**. Use **`registerDefinedNodes([...])`** for `defineNode` packages, then **`runNode({ node: yourNode.create(...), items })`** or **`run({ workflow, items })`** for fuller graphs. Prefer this for fast node tests; use **`codemation dev:plugin`** when you need the UI and persistence.
47
47
 
48
+ ## Declaring MCP servers from a plugin (`mcpServers?`)
49
+
50
+ A plugin can declare MCP servers that the framework merges into its in-memory catalog at startup. Use this for providers that need non-standard auth, custom adapter logic, or are shipping alongside custom nodes. For standard SaaS providers (OAuth via broker, plain bearer/API key), prefer the control-plane registry instead — no plugin code required.
51
+
52
+ ### When to use plugin-declared MCP servers
53
+
54
+ - The provider's MCP server has non-standard auth the generic credential types cannot express.
55
+ - The plugin already ships custom nodes for the same provider and wants to co-locate the MCP declaration.
56
+ - Self-hosted deployments where no control-plane registry is available.
57
+
58
+ ### Required fields
59
+
60
+ ```ts
61
+ import { definePlugin } from "@codemation/host/authoring";
62
+ import type { McpServerDeclaration } from "@codemation/core";
63
+
64
+ const myServer: McpServerDeclaration = {
65
+ id: "my-service", // globally unique slug: /^[a-z0-9-]+$/
66
+ displayName: "My Service",
67
+ description: "Provides MCP tools for My Service.",
68
+ transport: "http",
69
+ url: "https://mcp.my-service.com",
70
+ // Credential types this server accepts. Users bind a credential instance
71
+ // per slot via the UI. Omit (or set to []) for servers requiring no auth.
72
+ acceptedCredentialTypes: ["my-service.bearer-token"],
73
+ };
74
+
75
+ export default definePlugin({
76
+ mcpServers: [myServer],
77
+ // credentials, nodes, register, etc.
78
+ });
79
+ ```
80
+
81
+ ### Merge precedence
82
+
83
+ The framework merges from three sources in this order (last-write-wins on `id` collisions):
84
+
85
+ 1. **Plugin** (lowest) — code in `codemation.plugin.ts`
86
+ 2. **`codemation.config.ts`** — dev/self-host declarations
87
+ 3. **Control-plane registry** (highest) — managed-mode fast lane; shadows plugin declarations to fix descriptions without a plugin release
88
+
89
+ A warning is logged when a higher-priority source shadows a plugin declaration. This is intentional.
90
+
48
91
  ## Read next when needed
49
92
 
50
93
  - Read `references/plugin-structure.md` for package layout and node-versus-credential guidance.
@@ -33,6 +33,37 @@ That file is the plugin repository's source composition root. Consumers should d
33
33
  - start with `defineCredential(...)`
34
34
  - build typed sessions in `createSession(...)`
35
35
  - implement `test(...)` so operators can validate configuration before activation
36
+ - for OAuth2 redirect flows, use the URL-template variant (`auth: { kind: "oauth2", providerId, authorizeUrl, tokenUrl, scopes }`) with `{publicFieldKey}` placeholders — no core or host edits needed per provider. See the credential-development skill for details.
37
+
38
+ ## Binary payloads — never put bytes on the item JSON
39
+
40
+ **Rule:** if a node produces or fetches binary content (file attachments, image bytes, audio, PDFs, downloads, etc.), the bytes go through the framework's binary storage via `ctx.binary.attach(...)`. They MUST NOT be placed on the item's JSON payload.
41
+
42
+ The runtime persists each item's JSON into the runs table for telemetry, replay, and debugging. Putting megabyte-scale base64 strings in there bloats the database, slows queries, and makes telemetry unreadable. The binary system exists exactly for this: blobs live in object storage; the item JSON only carries a `BinaryAttachment` reference (`{ id, storageKey, mimeType, size, ... }`) under `item.binary[<slot-name>]`.
43
+
44
+ ```ts
45
+ // Inside execute(items, ctx) on a node that has fetched a file:
46
+ const stored = await ctx.binary.attach({
47
+ name: "report.pdf", // slot name (also the key under item.binary)
48
+ body: Buffer.from(bytes), // Buffer / Uint8Array / Readable
49
+ mimeType: "application/pdf",
50
+ filename: "report.pdf", // hint for downloads
51
+ });
52
+ const enriched = ctx.binary.withAttachment(item, "report.pdf", stored);
53
+ ```
54
+
55
+ Notes:
56
+
57
+ - Attachment **metadata** (id, name, contentType, size) belongs on the item JSON — it is small and useful for branching. Only the **bytes** must go through `ctx.binary`.
58
+ - For triggers, fetch metadata cheaply in `runCycle` (e.g. Graph's `$expand=attachments($select=id,name,contentType,size)`) and defer the byte download to `execute()` so persisted run state stays tiny on every poll.
59
+ - Two attachments with the same filename within one item collide on `item.binary[name]`; suffix the slot name (`report-2.pdf`) to keep both.
60
+
61
+ ## Polling-trigger guidance
62
+
63
+ - the engine ships a generic polling-trigger runtime in `@codemation/core` exposed via `ctx.polling` on the trigger setup context
64
+ - call `ctx.polling.start({ intervalMs, runCycle })` from your trigger node's `setup()` — the runtime handles the loop, overlap guard, dedup window (`ctx.polling.dedup.merge(...)`), state persistence, and cleanup
65
+ - on the first cycle, baseline-skip (record current ids, emit nothing) so the workflow does not flood with the existing backlog when the trigger is first set up
66
+ - implement `TestableTriggerNode.getTestItems(ctx)` to power the workflow UI's **Test** button — return the most recent N items without consulting or mutating polling state, so users can preview live data without waiting
36
67
 
37
68
  ## Publishability
38
69
 
@@ -1,39 +1,202 @@
1
1
  ---
2
2
  name: codemation-workflow-dsl
3
- description: Guides Codemation workflow authoring with the fluent Workflow DSL. Use when creating or updating `workflow("...")` definitions, triggers, `.map(...)`, `.node(...)`, branch flow, item handling, or `.build()` chains in `src/workflows`.
4
- compatibility: Designed for Codemation apps and plugins that author workflows with the fluent DSL.
3
+ description: Guides Codemation workflow authoring. Use when creating or updating workflow definitions in `src/workflows` — manual-trigger flows via `workflow("...").manualTrigger(...)`, or cron/webhook/other triggers via `createWorkflowBuilder({id, name}).trigger(...)`.
4
+ compatibility: Designed for Codemation apps and plugins that author workflows.
5
5
  ---
6
6
 
7
7
  # Codemation Workflow DSL
8
8
 
9
9
  ## Use this skill when
10
10
 
11
- Use this skill for authoring or reviewing workflow definitions built with `workflow("...")`.
11
+ Authoring or reviewing workflow definitions under `src/workflows/`.
12
12
 
13
13
  Do not use this skill for CLI-only troubleshooting or deep host architecture questions unless they directly affect workflow authoring.
14
14
 
15
+ ## Discovering nodes and patterns
16
+
17
+ **Always call `find_examples` first** when you need to learn how to use a node or build a workflow pattern.
18
+
19
+ ### Why examples are the canonical reference
20
+
21
+ Examples in the catalog typecheck, lint, and are verified by CI. They show the exact import paths, constructor signatures, and DSL shape that work in a real project — more efficiently than reading schema definitions or grepping framework source.
22
+
23
+ ### When to call `find_examples` first
24
+
25
+ - Before writing any workflow that uses an unfamiliar node.
26
+ - When you need a pattern (polling, branching, sub-workflow, agent with tools, etc.) and aren't sure of the exact API.
27
+ - As your first step — before `read_skill`, before `search_capabilities`, before reading any file.
28
+
29
+ ### Query patterns
30
+
31
+ Call `find_examples` in two ways:
32
+
33
+ ```ts
34
+ // By node name:
35
+ find_examples({ query: "HttpRequest" });
36
+ find_examples({ query: "AIAgent" });
37
+ find_examples({ query: "CronTrigger" });
38
+
39
+ // By use case / intent:
40
+ find_examples({ query: "poll API and write to database" });
41
+ find_examples({ query: "AIAgent multi-step pipeline" });
42
+ find_examples({ query: "gmail trigger classify email" });
43
+ ```
44
+
45
+ Mix both: `find_examples({ query: "AIAgent gmail classify" })` works too.
46
+
47
+ ### Install state in results
48
+
49
+ Every search result includes `installed: boolean` and `requiresInstall: string[]`. Use these to plan installs (`install_package`) before adapting an example. If `installed` is `false` or `requiresInstall` is non-empty, call `install_package` for each missing package before writing any workflow code that imports them.
50
+
51
+ ### When find_examples returns zero hits
52
+
53
+ Stop. Do not improvise from memory. Do one of:
54
+
55
+ 1. **Ask the user**: "I don't have an example for `<query>`. Would you like me to adapt the closest match (`<nearest>`) or should a proper example be added first?"
56
+ 2. **Adapt the closest near-miss** — only with the user's explicit confirmation that the approach is reasonable.
57
+
58
+ Do not attempt to infer node behavior by grepping framework source code (e.g. `node_modules/@codemation/*`). Examples convey the same information more efficiently and are authoritative.
59
+
60
+ ## When no example matches — the self-solving fallback chain
61
+
62
+ If `find_examples` returns no good match for your query, **do not ask the user**. The user is non-technical and can't help you pick between framework primitives. Solve it using this fixed chain:
63
+
64
+ ### Tier 1 — Retry with intent variations
65
+
66
+ Re-query with the underlying intent: a different verb, a more generic term, the closest standard pattern. Example: no hit for `"google sheets append row"` → retry `"http POST bearer credential"` or `"REST API call with credential"`.
67
+
68
+ ### Tier 2 — Custom REST node (preferred for HTTP APIs)
69
+
70
+ If the task is "call an external HTTP API," use `defineRestNode`. Always works.
71
+
72
+ `find_examples({ query: "defineRestNode" })` → returns the canonical templates:
73
+
74
+ - `custom-rest-node-simple.example.ts` — basic shape
75
+ - `custom-rest-node-with-credential.example.ts` — with bearer/OAuth credential slot
76
+
77
+ Adapt these to the specific endpoint + payload shape needed.
78
+
79
+ ### Tier 3 — Raw HttpRequest (inline, one-off)
80
+
81
+ If the call is one-shot inline in a workflow and you don't need to define a reusable node, use the `HttpRequest` config class.
82
+
83
+ `find_examples({ query: "HttpRequest" })` → `node-httprequest.example.ts`
84
+
85
+ ### Tier 4 — defineNode (non-HTTP custom logic)
86
+
87
+ If the task isn't an HTTP call (data transformation, business logic, anything stateful), use `defineNode`.
88
+
89
+ `find_examples({ query: "defineNode template" })` → `custom-node-template.example.ts`
90
+
91
+ ### What NOT to do
92
+
93
+ - Do NOT ask the user "should I use HttpRequest or defineRestNode?" — they can't help; pick using the chain.
94
+ - Do NOT grep `node_modules/@codemation/*` for node implementations — the templates above are the canonical reference.
95
+ - Do NOT invent a custom solution outside this chain.
96
+
97
+ ### Surfacing what you did
98
+
99
+ After building, your final message to the concierge should state the technique used, e.g.:
100
+
101
+ > "Built using `defineRestNode` for the Google Sheets append call (no first-class Sheets node yet)."
102
+
103
+ This is informational, not a request for approval.
104
+
105
+ ## There are TWO authoring APIs — pick by trigger type
106
+
107
+ | Trigger | API to use | Import | Available chain helpers |
108
+ | ----------------------------------------------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
109
+ | **Manual** (one-shot, optionally seeded with default items) | `workflow("id").manualTrigger(...)` | `import { workflow } from "@codemation/host"` | Full fluent sugar: `.map`, `.if`, `.switch`, `.split`, `.agent`, `.node`, `.then`, `.build` |
110
+ | **Cron, Webhook, Test, or any non-manual trigger** | `createWorkflowBuilder({ id, name }).trigger(new XxxTrigger(...))` | `import { createWorkflowBuilder, CronTrigger, WebhookTrigger } from "@codemation/core-nodes"` | Low-level `.then(new SomeNodeConfig(...))` only — **no** `.map`/`.if`/`.agent`/`.node` sugar |
111
+
112
+ **Why two APIs?** `workflow("...")` returns a `WorkflowAuthoringBuilder` that _only_ exposes `.name()` and `.manualTrigger(...)`. Once you call `.manualTrigger(...)`, you get a `WorkflowChain` that has all the fluent helpers. For any other trigger, you must use the lower-level `createWorkflowBuilder({id, name}).trigger(new Trigger(...))` path — the result is a `ChainCursor` whose only chain method is `.then(new NodeConfig(...))`. You compose by passing node config classes directly: `new Callback(...)`, `new HttpRequest(...)`, `new AIAgent(...)`, `new If(...)`, `new Split(...)`, etc.
113
+
114
+ If you find yourself wanting `.map` or `.if` on a cron workflow, you have two options: (a) accept the verbose `.then(new Callback(...))` style, or (b) wrap the cron-trigger cursor explicitly: `new WorkflowChain(builder.trigger(new CronTrigger(...)))` — but this is rare in practice; production cron workflows use plain `.then(new ConfigClass(...))`.
115
+
15
116
  ## Core mental model
16
117
 
17
118
  1. A workflow definition describes how items move from a trigger through downstream steps.
18
- 2. The fluent authoring chain is the normal starting point for Codemation apps.
19
- 3. Finish fluent workflow definitions with `.build()`.
20
- 4. Activations are **batch-shaped** (`Items`); many steps use **per-item** execution (`execute`, including helper **`defineNode`**) with optional **`inputSchema`** and **`itemExpr`** on config fields. Batch reshape steps (split/filter/aggregate, **`defineBatchNode`**) work on the whole batch.
21
- 5. Fluent callback helpers follow the runtime item contract: `.map(...)`, `.if(...)`, and `.switch({ resolveCaseKey })` receive `(item, ctx)`, so row fields live under `item.json` and earlier completed outputs are available through `ctx.data`.
119
+ 2. 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.
120
+ 3. Fluent callback helpers (manual-trigger only) follow the runtime item contract: `.map(...)`, `.if(...)`, and `.switch({ resolveCaseKey })` receive `(item, ctx)`. Row fields live under `item.json`; earlier completed outputs are available through `ctx.data`.
121
+ 4. Finish every workflow definition with `.build()`.
22
122
 
23
123
  ## Authoring rules
24
124
 
25
- 1. Prefer the fluent `workflow(...)` chain for app-local workflow files.
125
+ 1. **Pick the API by trigger type** (see table above). Don't try to call `.trigger(...)` on the `workflow(...)` builder it doesn't exist there.
26
126
  2. Keep workflow files focused on orchestration and named steps.
27
127
  3. Use custom nodes when a callback grows into reusable product logic.
28
128
  4. Distinguish **batch activations** from **per-item node bodies**: custom nodes from **`defineNode`** implement **`execute`** per item unless you chose **`defineBatchNode`** for batch **`run`**.
129
+ 5. **Collection nodes (`collectionInsertNode`, `collectionGetNode`, `collectionListNode`, etc.) use `.then(node.create(...))` instead of `.node(label, node, opts)`.** TypeScript's inference can't bridge the recursive `ParamDeep` constraint when the node config contains `z.record(...)` fields. See `node-collection-crud.example.ts` for the canonical pattern.
130
+
131
+ ## Node ids and stability
132
+
133
+ 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"`.
134
+
135
+ `.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.
136
+
137
+ 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:
138
+
139
+ ```ts
140
+ .node("Send notification", SendEmailNodeConfig, {
141
+ id: "send-notification", // stable even if the label is later renamed
142
+ // ...
143
+ })
144
+ ```
145
+
146
+ ### Collision gotcha — set explicit ids on every node
147
+
148
+ Auto-derived ids can also **collide** when a trigger and a downstream node share a label. Example:
149
+
150
+ ```ts
151
+ // ❌ Auto-derived ids collide: both slugify to "classify-feedback"
152
+ workflow("wf.feedback")
153
+ .manualTrigger("Classify feedback", {
154
+ /* ... */
155
+ })
156
+ .agent("Classify feedback", {
157
+ /* ... */
158
+ })
159
+ .build(); // throws WorkflowDefinitionError: duplicate nodeId "classify-feedback"
160
+
161
+ // ✅ Explicit id on the AIAgent disambiguates
162
+ workflow("wf.feedback")
163
+ .manualTrigger("Classify feedback", {
164
+ /* ... */
165
+ })
166
+ .agent("Classify feedback", { id: "classify-feedback-agent" /* ... */ })
167
+ .build();
168
+ ```
169
+
170
+ **Recommendation: always set an explicit `id:` on every node.** It's a few extra characters that buys you:
171
+
172
+ 1. Stable credential bindings across label renames (above)
173
+ 2. No collision build errors when refactoring labels
174
+ 3. Stable references for any downstream code that addresses nodes by id (e.g. pinned-output state, test-suite assertions, audit-log entries)
175
+
176
+ The slug-derived default exists for quick prototyping; production workflows should declare ids.
29
177
 
30
178
  ## Typical flow
31
179
 
32
- 1. Start with `workflow("wf.example.id")`.
33
- 2. Name the workflow with `.name(...)`.
34
- 3. Add a trigger such as `.manualTrigger(...)`.
35
- 4. Add transformations or nodes in execution order.
36
- 5. End with `.build()`.
180
+ **Manual trigger (fluent):**
181
+
182
+ 1. `workflow("wf.example.id")`.
183
+ 2. `.name("Display name")` (optional defaults to the id).
184
+ 3. `.manualTrigger("Start", { /* default item json */ })`.
185
+ 4. Chain transformations: `.map(...)`, `.if(...)`, `.switch(...)`, `.split(...)`, `.agent(...)`, `.node(...)`, `.then(...)`.
186
+ 5. `.build()`.
187
+
188
+ **Cron / webhook (low-level):**
189
+
190
+ 1. `createWorkflowBuilder({ id: "wf.example.id", name: "Display name" })`.
191
+ 2. `.trigger(new CronTrigger("Label", { schedule, timezone }))` or `.trigger(new WebhookTrigger("Label", { endpointKey, methods }))`.
192
+ 3. Chain with `.then(new SomeNodeConfig(...))` repeatedly. Common configs: `Callback`, `HttpRequest`, `AIAgent`, `If`, `Split`, `Merge`, `SubWorkflow`.
193
+ 4. `.build()`.
194
+
195
+ ## Built-in triggers
196
+
197
+ - **`ManualTrigger`** — one-shot manual run, optionally seeded with default items. Use the fluent shortcut: `workflow("id").manualTrigger(name, items?)`. The shortcut internally wires up `createWorkflowBuilder(...).trigger(new ManualTrigger(...))` and wraps the result in `WorkflowChain` so you get the full fluent sugar.
198
+ - **`WebhookTrigger`** — fires on an incoming HTTP request. Construct with `new WebhookTrigger(name, { endpointKey, methods })`. Attach via `createWorkflowBuilder({id, name}).trigger(new WebhookTrigger(...))`.
199
+ - **`CronTrigger`** — fires on a cron schedule. Construct with `new CronTrigger(name, { schedule, timezone? })`. Attach via `createWorkflowBuilder({id, name}).trigger(new CronTrigger(...))`. 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.
37
200
 
38
201
  ## Agent tools (callable helpers)
39
202
 
@@ -49,6 +212,67 @@ Do not use this skill for CLI-only troubleshooting or deep host architecture que
49
212
  - Use fluent `.map((item, ctx) => ...)` when workflow data itself needs reshaping before the agent step.
50
213
  - `model` may be a provider string such as `"openai:gpt-4o-mini"` or a `ChatModelConfig`.
51
214
 
215
+ ## Workflow testing nodes
216
+
217
+ Codemation ships first-class **workflow tests**: each test case is one full workflow run, persisted with assertion records. Three nodes from `@codemation/core-nodes`:
218
+
219
+ 1. **`TestTrigger`** — drop alongside live triggers. Author callback `generateItems(ctx)` returns an `AsyncIterable<Item>`; the orchestrator dispatches one workflow run per yielded item with `executionOptions.testContext` set. `triggerKind: "test"` is set automatically — live activation skips it.
220
+ 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).
221
+ 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.
222
+
223
+ Authors invoke a TestSuiteRun from the canvas **Tests tab** or via `POST /api/workflows/:id/test-suite-runs`. The orchestrator caps concurrency (default 4, configurable per trigger) and aggregates results into `succeeded | failed | partial | cancelled | errored`.
224
+
225
+ Custom nodes can also read `ctx.testContext?.{testSuiteRunId, testCaseIndex}` directly — useful for synthetic outputs in test mode without `IsTestRun` branching.
226
+
227
+ ## Binary slots across SubWorkflow boundaries
228
+
229
+ `item.binary` (the map of named `BinaryAttachment` records) is carried transparently through SubWorkflow boundaries in both directions:
230
+
231
+ - **Parent → child**: binary slots attached before the SubWorkflow node are visible inside the child run. `ctx.binary.openReadStream(attachment)` works in the child because both runs share the same `BinaryStorage`.
232
+ - **Child → parent**: slots attached inside the child are returned with the item and visible in the parent's continuation nodes.
233
+
234
+ This requires no special configuration in production — the shared `BinaryStorage` DI singleton is what makes cross-run byte reads possible.
235
+
236
+ ### SubWorkflow + binary example (manual trigger)
237
+
238
+ ```ts
239
+ import { workflow } from "@codemation/host";
240
+ import { Callback, SubWorkflow } from "@codemation/core-nodes";
241
+
242
+ // Manual-trigger flow — uses the fluent `.map`/`.then` sugar.
243
+ export default workflow("wf.parent")
244
+ .manualTrigger<{ url: string }>("Start", { url: "" })
245
+ // Attach a binary slot before the sub-workflow:
246
+ .map(async (item, ctx) => {
247
+ const att = await ctx.binary.attach({
248
+ name: "doc",
249
+ body: Buffer.from("..."),
250
+ mimeType: "application/pdf",
251
+ filename: "doc.pdf",
252
+ });
253
+ return ctx.binary.withAttachment(item, "doc", att);
254
+ })
255
+ // Sub-workflow receives item with binary["doc"] populated:
256
+ .then(new SubWorkflow("ParseDoc", { workflowId: "wf.child" }))
257
+ // Continuation: both parent "doc" slot and any child-added slots are visible here.
258
+ .map((item) => item)
259
+ .build();
260
+ ```
261
+
52
262
  ## Read next when needed
53
263
 
54
264
  - Read `references/builder-patterns.md` for item-flow rules and fluent authoring patterns.
265
+ - Read `references/workflow-testing.md` for TestTrigger / IsTestRun / Assertion authoring with full examples.
266
+ - Read `references/complete-example.md` for a single dense end-to-end workflow example that exercises most authoring features (CronTrigger, map, if, agent, callableTool, itemExpr, ctx.data, ctx.binary, node with explicit id, build).
267
+
268
+ ## Verifying your workflow
269
+
270
+ After writing or modifying a workflow file, call `verify_workflow({ path })` instead of running `pnpm typecheck` yourself. The tool runs typecheck + lint + DSL build + structure dump in one round-trip and returns a structured envelope:
271
+
272
+ ```ts
273
+ verify_workflow({ path: "src/workflows/my-workflow.ts" });
274
+ // → { ok: true, data: { typecheck: "ok", lint: "ok", build: "ok", structure: { id, name, trigger, nodes, edges, activation } } }
275
+ // → { ok: false, error: "...", data: { typecheck: {...}, lint: {...}, build: {...}, structure: null }, hint: "..." }
276
+ ```
277
+
278
+ A failed `ok: false` result includes a `hint` field that points at the specific fix needed. Fix the reported errors and call `verify_workflow` again — do not report done until `ok: true`.
@@ -1,13 +1,15 @@
1
+ Load this when you need item-flow rules, the two-API decision, and fluent authoring patterns.
2
+
1
3
  # Builder Patterns
2
4
 
3
- ## Standard workflow shape
5
+ ## Manual-trigger workflow (fluent — full sugar available)
4
6
 
5
7
  ```ts
8
+ import { workflow } from "@codemation/host";
9
+
6
10
  export default workflow("wf.example.id")
7
11
  .name("Example")
8
- .manualTrigger("Start", {
9
- step: "start",
10
- })
12
+ .manualTrigger("Start", { step: "start" })
11
13
  .map("Transform", (item, _ctx) => ({
12
14
  ...item.json,
13
15
  transformed: true,
@@ -15,21 +17,80 @@ export default workflow("wf.example.id")
15
17
  .build();
16
18
  ```
17
19
 
18
- ## Use the fluent DSL by default
20
+ The `.map`, `.if`, `.switch`, `.split`, `.agent`, `.node`, `.then` helpers are available because `manualTrigger(...)` returns a `WorkflowChain`.
19
21
 
20
- - import `workflow` from `@codemation/host`
21
- - keep the file under `src/workflows`
22
- - export the built workflow definition as the default export when following starter patterns
22
+ ## Cron-triggered workflow (low-level — `.then(new NodeConfig(...))` only)
23
+
24
+ ```ts
25
+ import { Callback, CronTrigger, createWorkflowBuilder } from "@codemation/core-nodes";
26
+
27
+ export default createWorkflowBuilder({
28
+ id: "wf.nightly.id",
29
+ name: "Nightly job",
30
+ })
31
+ .trigger(new CronTrigger("Nightly", { schedule: "0 3 * * *", timezone: "Europe/Amsterdam" }))
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
+ )
39
+ .build();
40
+ ```
41
+
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.
43
+
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
66
+
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.
23
71
 
24
72
  ## Item rules
25
73
 
26
74
  - workflow data flows as items
27
- - items usually carry `json` data and optional `binary` data
75
+ - items usually carry `json` data and optional `binary` data (**storage-backed attachments** via node **`ctx.binary.attach`**, not huge base64 strings in **`json`** — base64 in **`json`** inflates the persisted run payload in the DB; binaries stay as **references**)
28
76
  - runtime nodes receive batches of items, not just one record
29
77
  - author workflow steps with batching in mind
30
78
  - fluent `.map(...)`, `.if(...)`, and `.switch({ resolveCaseKey })` callbacks receive `(item, ctx)`
31
79
  - read row fields from `item.json` and earlier completed outputs from `ctx.data`
32
80
 
81
+ ## Node id assignment
82
+
83
+ When no `id:` is provided, the builder slugifies the node's `name` label: lowercase, non-alphanumeric runs replaced with `-`, leading/trailing `-` stripped. Two nodes with the same effective label produce the same slug and `.build()` throws `WorkflowDefinitionError`. Fix: provide a unique `id:` on the colliding node configs.
84
+
85
+ Credential bindings are stored as `(workflowId, nodeId, slotKey)`. Changing a node's label changes its slug-derived id and the binding appears unbound. For credential-using nodes, either keep the label stable or set an explicit `id:`:
86
+
87
+ ```ts
88
+ .node("Send email", SendEmailNodeConfig, {
89
+ id: "send-email", // stable even after a label rename
90
+ credentials: { smtp: mySmtpCredential },
91
+ })
92
+ ```
93
+
33
94
  ## When to move beyond callbacks
34
95
 
35
96
  Promote inline callbacks into custom nodes when: