@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
|
@@ -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
|
|
4
|
-
compatibility: Designed for Codemation apps and plugins that author workflows
|
|
3
|
+
description: Guides Codemation workflow authoring. Use when creating or updating workflow definitions in `src/workflows` — manual-trigger flows via `workflow("...").manualTrigger(...)`, or cron/webhook/other triggers via `createWorkflowBuilder({id, name}).trigger(...)`.
|
|
4
|
+
compatibility: Designed for Codemation apps and plugins that author workflows.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Codemation Workflow DSL
|
|
8
8
|
|
|
9
9
|
## Use this skill when
|
|
10
10
|
|
|
11
|
-
|
|
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.
|
|
19
|
-
3.
|
|
20
|
-
4.
|
|
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.
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
20
|
+
The `.map`, `.if`, `.switch`, `.split`, `.agent`, `.node`, `.then` helpers are available because `manualTrigger(...)` returns a `WorkflowChain`.
|
|
19
21
|
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
|
|
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:
|