@codemation/agent-skills 0.4.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.
- package/CHANGELOG.md +165 -0
- package/dist/metadata.json +358 -48
- package/package.json +3 -1
- package/skills/builder/ai-agent/SKILL.md +314 -0
- package/skills/builder/ai-agent/references/anti-patterns.md +24 -0
- package/skills/{codemation-cli → builder/cli}/SKILL.md +1 -8
- package/skills/builder/connect-external-systems/SKILL.md +191 -0
- package/skills/builder/credential-development/SKILL.md +86 -0
- package/skills/{codemation-credential-development → builder/credential-development}/references/credential-patterns.md +3 -3
- package/skills/builder/custom-node-development/SKILL.md +61 -0
- package/skills/builder/custom-node-development/references/credential-aware-nodes.md +52 -0
- package/skills/builder/custom-node-development/references/define-batch-node.md +54 -0
- package/skills/{codemation-custom-node-development → builder/custom-node-development}/references/define-node-per-item.md +14 -14
- package/skills/{codemation-custom-node-development → builder/custom-node-development}/references/node-patterns.md +33 -49
- package/skills/builder/document-ai/SKILL.md +167 -0
- package/skills/builder/execution-context/SKILL.md +436 -0
- package/skills/{codemation-framework-concepts → builder/framework-concepts}/SKILL.md +10 -18
- package/skills/builder/gmail/SKILL.md +327 -0
- package/skills/builder/human-in-the-loop/SKILL.md +82 -0
- package/skills/{codemation-mcp-capabilities → builder/mcp-capabilities}/SKILL.md +4 -11
- package/skills/{codemation-mcp-capabilities → builder/mcp-capabilities}/references/agent-with-mcp.ts +1 -1
- package/skills/builder/msgraph/SKILL.md +338 -0
- package/skills/builder/odoo/SKILL.md +498 -0
- package/skills/{codemation-plugin-development → builder/plugin-development}/SKILL.md +4 -7
- package/skills/{codemation-plugin-development → builder/plugin-development}/references/plugin-anatomy.md +36 -15
- package/skills/{codemation-plugin-development → builder/plugin-development}/references/plugin-structure.md +2 -2
- package/skills/builder/rest-node/SKILL.md +148 -0
- package/skills/builder/testing/SKILL.md +142 -0
- package/skills/builder/workflow-dsl/SKILL.md +493 -0
- package/skills/builder/workspace-files/SKILL.md +191 -0
- package/skills/concierge/credentials/SKILL.md +91 -0
- package/skills/concierge/intake-automation-playbook/SKILL.md +78 -0
- package/skills/concierge/scenario-invoice-to-accounting/SKILL.md +48 -0
- package/skills/concierge/scenario-procurement-intake/SKILL.md +58 -0
- package/skills/codemation-ai-agent-node/SKILL.md +0 -66
- package/skills/codemation-ai-agent-node/references/anti-patterns.md +0 -11
- package/skills/codemation-credential-development/SKILL.md +0 -57
- package/skills/codemation-custom-node-development/SKILL.md +0 -61
- package/skills/codemation-custom-node-development/references/credential-aware-nodes.md +0 -38
- package/skills/codemation-custom-node-development/references/define-batch-node.md +0 -38
- package/skills/codemation-document-scanner/SKILL.md +0 -136
- package/skills/codemation-workflow-dsl/SKILL.md +0 -78
- package/skills/codemation-workflow-dsl/references/builder-patterns.md +0 -120
- package/skills/codemation-workflow-dsl/references/complete-example.md +0 -263
- package/skills/codemation-workflow-dsl/references/workflow-testing.md +0 -194
- package/skills/codemation-workspace-files/SKILL.md +0 -142
- /package/skills/{codemation-cli → builder/cli}/references/command-map.md +0 -0
- /package/skills/{codemation-framework-concepts → builder/framework-concepts}/references/architecture-map.md +0 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rest-node
|
|
3
|
+
description: Wrap a plain REST/HTTP service as a reusable workflow node with defineRestNode — declare a base URL, an endpoint + method, a credential slot, and input→request / response→output mappers, then use node.create(...) in a builder chain. Reach for this when a step talks to an external API that has no specialized node and no MCP server.
|
|
4
|
+
compatibility: Codemation core-nodes. Requires @codemation/core-nodes import.
|
|
5
|
+
tags: rest, http, api, integration, defineRestNode, node, credential, connect, external
|
|
6
|
+
uses: "@codemation/core-nodes, @codemation/core, zod"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Codemation REST Node (`defineRestNode`)
|
|
10
|
+
|
|
11
|
+
`defineRestNode` (from `@codemation/core-nodes`) turns one REST endpoint into a thin, reusable workflow
|
|
12
|
+
node — without a hand-rolled `Callback` + raw `fetch`. You declare a base URL, a path + method, an
|
|
13
|
+
optional credential slot, and two small mappers (item input → request, HTTP response → output). It is a
|
|
14
|
+
declarative wrapper over `defineNode`, so the result is used exactly like any built-in node:
|
|
15
|
+
`node.create(config, label, nodeId)` in a `createWorkflowBuilder()` chain.
|
|
16
|
+
|
|
17
|
+
**When to reach for it.** This is route 3 of the integration-routing hierarchy (see
|
|
18
|
+
`connect-external-systems`): use `defineRestNode` for a plain REST/HTTP service when there is **no
|
|
19
|
+
specialized node package** (route 1 — check `search_skills`) and **no MCP server** (route 2 — check
|
|
20
|
+
`list_mcp_servers`). It is the right tool the moment you would otherwise hand-roll `fetch` against a
|
|
21
|
+
JSON API — and it is strictly preferred over that. Only drop to a raw `Callback` when even
|
|
22
|
+
`defineRestNode` can't express the call (see Gotchas).
|
|
23
|
+
|
|
24
|
+
**Discipline:** author straight from this file, then run `verify_workflow` and fix only what it flags.
|
|
25
|
+
Use `workflow-dsl` for the surrounding builder, trigger, and flow-control surface.
|
|
26
|
+
|
|
27
|
+
## The shape — one node per endpoint
|
|
28
|
+
|
|
29
|
+
`defineRestNode({...})` takes:
|
|
30
|
+
|
|
31
|
+
- **`key`** / **`title`** — stable node key (e.g. `"widgets.create"`) and canvas title.
|
|
32
|
+
- **`api`** — `{ baseUrl, path, method? }`. `path` may carry `{name}` placeholders, substituted from
|
|
33
|
+
`input` keys before the request (method defaults to `GET`).
|
|
34
|
+
- **`credentials`** — a slot map, e.g. `{ auth: bearerTokenCredentialType }`. The session applies auth
|
|
35
|
+
to every request (Bearer header, API-key header/query, Basic). The operator binds it via the canvas;
|
|
36
|
+
you only declare the slot. Omit `credentials` for an unauthenticated API.
|
|
37
|
+
- **`inputSchema`** — a Zod schema for per-item input, validated before the request runs.
|
|
38
|
+
- **`request({ input })`** — returns `{ body?, query?, headers?, pathParams? }`. For a JSON body pass
|
|
39
|
+
`{ kind: "json", data: {...} }`.
|
|
40
|
+
- **`response({ json, text, status, ok, input, ... })`** — maps the HTTP result to the node's output
|
|
41
|
+
JSON. Omit it to emit the raw `{ status, ok, json, text, ... }` response context.
|
|
42
|
+
- **`errorPolicy`** — `"throw"` (default — non-2xx throws) or `"passthrough"` (return the result and
|
|
43
|
+
branch on `ok` downstream).
|
|
44
|
+
|
|
45
|
+
Credential types come from `@codemation/core-nodes`: `bearerTokenCredentialType`,
|
|
46
|
+
`apiKeyCredentialType`, `basicAuthCredentialType` (or any custom `defineCredential`).
|
|
47
|
+
|
|
48
|
+
## A complete REST-backed workflow
|
|
49
|
+
|
|
50
|
+
A generic "widgets API": list widgets nightly (API-key auth), fan the array into one item per widget,
|
|
51
|
+
then create a record back in the same API (Bearer auth). Each node is defined once, then used with
|
|
52
|
+
`.create(config, label, nodeId)` — the `config` is `{}` here because the per-item input flows from the
|
|
53
|
+
chain, not static config.
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { z } from "zod";
|
|
57
|
+
import {
|
|
58
|
+
createWorkflowBuilder,
|
|
59
|
+
CronTrigger,
|
|
60
|
+
Split,
|
|
61
|
+
defineRestNode,
|
|
62
|
+
apiKeyCredentialType,
|
|
63
|
+
bearerTokenCredentialType,
|
|
64
|
+
} from "@codemation/core-nodes";
|
|
65
|
+
import type { Item } from "@codemation/core";
|
|
66
|
+
|
|
67
|
+
// List widgets — GET with an API-key credential, no request body.
|
|
68
|
+
const listWidgets = defineRestNode({
|
|
69
|
+
key: "widgets.list",
|
|
70
|
+
title: "List widgets",
|
|
71
|
+
icon: "mdi:widgets",
|
|
72
|
+
api: { baseUrl: "https://api.widgets.example.com", path: "/v1/widgets" },
|
|
73
|
+
credentials: { auth: apiKeyCredentialType },
|
|
74
|
+
response: ({ json }) => ({ widgets: (json as { items: { name: string; color: string }[] }).items }),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Create a widget — POST with a JSON body built from the item input.
|
|
78
|
+
const createWidget = defineRestNode({
|
|
79
|
+
key: "widgets.create",
|
|
80
|
+
title: "Create widget",
|
|
81
|
+
icon: "mdi:widgets",
|
|
82
|
+
api: { baseUrl: "https://api.widgets.example.com", path: "/v1/widgets", method: "POST" },
|
|
83
|
+
credentials: { auth: bearerTokenCredentialType },
|
|
84
|
+
inputSchema: z.object({ name: z.string(), color: z.string() }),
|
|
85
|
+
request: ({ input }) => ({
|
|
86
|
+
body: { kind: "json", data: { name: input.name, color: input.color } },
|
|
87
|
+
}),
|
|
88
|
+
response: ({ json }) => ({ widgetId: (json as { id: string }).id }),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
export default createWorkflowBuilder({ id: "wf.widgets-sync", name: "Sync widgets" })
|
|
92
|
+
.trigger(new CronTrigger("Nightly", { schedule: "0 2 * * *", timezone: "Europe/Amsterdam" }))
|
|
93
|
+
.then(listWidgets.create({}, "List widgets", "list-widgets"))
|
|
94
|
+
.then(
|
|
95
|
+
new Split<{ widgets: { name: string; color: string }[] }, { name: string; color: string }>(
|
|
96
|
+
"Per widget",
|
|
97
|
+
(item: Item<{ widgets: { name: string; color: string }[] }>) => item.json.widgets,
|
|
98
|
+
"per-widget",
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
.then(createWidget.create({}, "Create widget", "create-widget"))
|
|
102
|
+
.build();
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Path placeholders and per-item input
|
|
106
|
+
|
|
107
|
+
`path` `{name}` placeholders are substituted from `input` keys (and any `pathParams` you return from
|
|
108
|
+
`request`). So a per-record GET reads its id straight off the item:
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { z } from "zod";
|
|
112
|
+
import { defineRestNode, bearerTokenCredentialType } from "@codemation/core-nodes";
|
|
113
|
+
|
|
114
|
+
// GET /v1/widgets/{widgetId} — `widgetId` is filled from the item input.
|
|
115
|
+
export const getWidget = defineRestNode({
|
|
116
|
+
key: "widgets.get",
|
|
117
|
+
title: "Get widget",
|
|
118
|
+
api: { baseUrl: "https://api.widgets.example.com", path: "/v1/widgets/{widgetId}" },
|
|
119
|
+
credentials: { auth: bearerTokenCredentialType },
|
|
120
|
+
inputSchema: z.object({ widgetId: z.string() }),
|
|
121
|
+
response: ({ json, status }) => ({ widget: json, status }),
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Gotchas
|
|
126
|
+
|
|
127
|
+
- **`.create({}, label, id)` — config is empty; input flows from the chain.** `defineRestNode` puts
|
|
128
|
+
per-call data on the **item input** (validated by `inputSchema`), not static config — so the first
|
|
129
|
+
`.create` arg is `{}`. The node reads each item's `json` as its `input`. Give every node an explicit
|
|
130
|
+
stable `nodeId` (third arg), same as the rest of the DSL.
|
|
131
|
+
- **The output is the `response` mapper's return — not the input item.** Like `HttpRequest`, a REST node
|
|
132
|
+
replaces the item payload. Carry forward anything you still need before the call, or return it from
|
|
133
|
+
`response`.
|
|
134
|
+
- **Non-2xx throws by default.** Set `errorPolicy: "passthrough"` and branch on `item.json.ok` with an
|
|
135
|
+
`If` when you want to handle failures inline instead of failing the run.
|
|
136
|
+
- **Bind the credential slot before activation.** Declaring `credentials: { auth: ... }` surfaces a
|
|
137
|
+
bindable slot; the operator binds an instance via the canvas dropdown (the concierge handles this —
|
|
138
|
+
separate skill). An unauthenticated API: omit `credentials`.
|
|
139
|
+
- **Last resort is a raw `Callback`.** Only hand-roll `fetch`/JSON-RPC inside a `Callback` when the call
|
|
140
|
+
genuinely can't be expressed as one endpoint + auth + mappers (e.g. multi-step JSON-RPC, streaming).
|
|
141
|
+
See `connect-external-systems` for the full hierarchy.
|
|
142
|
+
|
|
143
|
+
## Read next when needed
|
|
144
|
+
|
|
145
|
+
- `connect-external-systems` — the full routing hierarchy; when `defineRestNode` is (and isn't) the right route.
|
|
146
|
+
- `workflow-dsl` — builder, triggers, flow control, the per-item contract.
|
|
147
|
+
- `custom-node-development` — `defineNode` for logic richer than one REST call (own execute body, ports).
|
|
148
|
+
- `credential-development` — author a custom credential type when Bearer/API-key/Basic don't fit.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: testing
|
|
3
|
+
description: Make a workflow safe to dry-run on real inputs — drive it from a TestTrigger fixture source, guard side effects with IsTestRun, and record pass/fail with Assertion. The dry-run path asserts while the live path performs the real write. Read this to make any side-effecting workflow testable before it goes live.
|
|
4
|
+
compatibility: Designed for Codemation workflows authored with @codemation/core-nodes.
|
|
5
|
+
tags: testing, test, assertion, dry-run, istestrun, testtrigger
|
|
6
|
+
uses: "@codemation/core-nodes, @codemation/core"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Codemation Testing
|
|
10
|
+
|
|
11
|
+
A workflow isn't done until it's testable: a way to run it on real inputs with the side effects guarded, plus assertions on the result. Three primitives do this, and you bake them in from the start:
|
|
12
|
+
|
|
13
|
+
- **`TestTrigger`** — a fixture source that feeds items through the _same_ pipeline. The Tests tab runs one workflow per yielded item, with the test context set. It's a **second, separate trigger export** — the live trigger stays the only `.trigger(...)` on the chain.
|
|
14
|
+
- **`IsTestRun`** — a per-item router on ports `"true"` / `"false"`. It's `true` when the run carries a test context (a TestTrigger or the Tests tab), `false` for every live/manual/cron/webhook activation. Branch the real write off it so a dry run never sends a real email or charges a card.
|
|
15
|
+
- **`Assertion`** — records one or more pass/fail results per item (`score: 1` = pass, `0` = fail) as `TestAssertion` rows the Tests tab aggregates.
|
|
16
|
+
|
|
17
|
+
The pattern: **dry-run asserts, live performs.** Put the real side effect on the `false` branch and the `Assertion` on the `true` branch. See `workflow-dsl` for the surrounding chain.
|
|
18
|
+
|
|
19
|
+
## The nodes — one-liners
|
|
20
|
+
|
|
21
|
+
`IsTestRun` and `TestTrigger` take positional / options args; `Assertion` takes an options object. `IsTestRun` routes on ports, so branch it with `.when`, never a bare `.then`.
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { IsTestRun, Assertion, TestTrigger } from "@codemation/core-nodes";
|
|
25
|
+
|
|
26
|
+
type Draft = { reply: string };
|
|
27
|
+
|
|
28
|
+
// Router — ports "true" (test run) / "false" (live run). Payload is unchanged.
|
|
29
|
+
const guard = new IsTestRun<Draft>("Is this a test run?");
|
|
30
|
+
|
|
31
|
+
// Assertion — one row per returned result; score 1 = pass, 0 = fail.
|
|
32
|
+
const check = new Assertion<Draft>({
|
|
33
|
+
name: "Reply quality",
|
|
34
|
+
assertions: (item) => [
|
|
35
|
+
{ name: "non-empty reply", score: item.json.reply.trim().length > 0 ? 1 : 0, actual: item.json.reply },
|
|
36
|
+
],
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// TestTrigger — a separate fixture source; generateItems is an async generator yielding one item per case.
|
|
40
|
+
const fixtures = new TestTrigger<Draft>({
|
|
41
|
+
name: "Reply fixtures",
|
|
42
|
+
id: "reply-fixtures",
|
|
43
|
+
async *generateItems() {
|
|
44
|
+
yield { json: { reply: "Thanks for your order!" } };
|
|
45
|
+
},
|
|
46
|
+
caseLabel: (item) => item.json.reply, // human label in the Tests tab
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## One realistic complete example
|
|
51
|
+
|
|
52
|
+
Webhook receives an order → `IsTestRun` splits dry-run from live → the `true` branch asserts the payload, the `false` branch performs the real notification. A companion `TestTrigger` is exported separately to feed fixtures through the same pipeline. Keep the fixture json shape identical to the live payload so the pipeline stays coherent. This folds in the former `istestrun` / `testtrigger-assertion` examples.
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import {
|
|
56
|
+
createWorkflowBuilder,
|
|
57
|
+
WebhookTrigger,
|
|
58
|
+
TestTrigger,
|
|
59
|
+
IsTestRun,
|
|
60
|
+
Assertion,
|
|
61
|
+
HttpRequest,
|
|
62
|
+
} from "@codemation/core-nodes";
|
|
63
|
+
|
|
64
|
+
type Order = { orderId: string; customerEmail: string; total: number };
|
|
65
|
+
|
|
66
|
+
// Companion fixture source — a SEPARATE export, not a second .trigger(). Same json shape as the live payload.
|
|
67
|
+
export const orderFixtures = new TestTrigger<Order>({
|
|
68
|
+
name: "Order fixtures",
|
|
69
|
+
id: "order-fixtures",
|
|
70
|
+
async *generateItems() {
|
|
71
|
+
yield { json: { orderId: "fixture-1", customerEmail: "a@example.com", total: 42 } };
|
|
72
|
+
yield { json: { orderId: "fixture-2", customerEmail: "b@example.com", total: 0 } };
|
|
73
|
+
},
|
|
74
|
+
caseLabel: (item) => item.json.orderId,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export default createWorkflowBuilder({ id: "wf.order-confirm", name: "Order confirmation (testable)" })
|
|
78
|
+
.trigger(new WebhookTrigger("Order placed", { endpointKey: "order-placed", methods: ["POST"] }))
|
|
79
|
+
// Guard the side effect: true = dry run (assert only), false = live run (real notification).
|
|
80
|
+
.then(new IsTestRun<Order>("Is this a test run?", "is-test-run"))
|
|
81
|
+
.when({
|
|
82
|
+
true: [
|
|
83
|
+
new Assertion<Order>({
|
|
84
|
+
name: "Order payload",
|
|
85
|
+
id: "assert-order-payload",
|
|
86
|
+
assertions: (item) => [
|
|
87
|
+
{ name: "has email", score: item.json.customerEmail.includes("@") ? 1 : 0, actual: item.json.customerEmail },
|
|
88
|
+
{ name: "positive total", score: item.json.total > 0 ? 1 : 0, actual: item.json.total },
|
|
89
|
+
],
|
|
90
|
+
}),
|
|
91
|
+
],
|
|
92
|
+
false: [
|
|
93
|
+
new HttpRequest<Order>("Send confirmation", {
|
|
94
|
+
method: "POST",
|
|
95
|
+
url: "https://api.example.com/notify/order-confirmation",
|
|
96
|
+
body: { kind: "json", data: { event: "order.confirmed", orderId: "${item.json.orderId}" } },
|
|
97
|
+
id: "send-confirmation",
|
|
98
|
+
}),
|
|
99
|
+
],
|
|
100
|
+
})
|
|
101
|
+
.build();
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Design principle: test on REAL data, not fabricated fixtures
|
|
105
|
+
|
|
106
|
+
For IO/trigger-bound workflows the right test source is **real data in the same shape the live trigger emits** — not something fabricated or pre-processed. The rule: pick the test source by trigger type.
|
|
107
|
+
|
|
108
|
+
- **Mailbox / inbox workflow** — read from a **designated test folder or label** (e.g. `orders-test`). The owner drops ~5 real order emails there; the `TestTrigger` reads them via the integration's read capability. Fabricating email fixtures here is an **anti-pattern**: it proves the fabricated item, not the actual input the workflow will see.
|
|
109
|
+
- **Webhook workflow** — replay real captured payloads (record one live hit, feed it back).
|
|
110
|
+
- **Pure-logic / transform step with no external IO** — hard-coded fixtures are acceptable; there is nothing real to sample.
|
|
111
|
+
|
|
112
|
+
**"Test items raw, processing shared."** The `TestTrigger` must yield items in the same raw shape the live trigger emits — unprocessed, uncanonicalized. All parsing, OCR, recognition, and matching must live _downstream_ of the trigger node and therefore run in both the test path and the live path. If the test source pre-processes or returns an already-canonical shape, the test asserts nothing meaningful.
|
|
113
|
+
|
|
114
|
+
**Place `IsTestRun` after the processing, not before it.** The correct topology is:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
trigger → [OCR / extraction / matching] → IsTestRun → {
|
|
118
|
+
true (test): assert the PROCESSING OUTCOME
|
|
119
|
+
false (live): all side-effects
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
A workflow that places `IsTestRun` right after the trigger and forks before the extraction step is wrong in two ways: the test path never exercises the processing logic, and the only thing left to assert is the raw trigger payload — which proves nothing about whether the workflow actually works. Put `IsTestRun` as late as possible: after all processing, just before the first external write.
|
|
124
|
+
|
|
125
|
+
**Assert the processing outcome, not the raw trigger payload.** The `Assertion` on the `true` branch must check what the processing steps produced — e.g. that an extraction recognised a customer name (non-empty) or pulled at least one line item (length > 0). Asserting that an email has a subject or that a webhook body is non-null proves only that the trigger fired, not that the workflow does anything useful.
|
|
126
|
+
|
|
127
|
+
**`IsTestRun` gates only side-effects — and ALL of them.** Both the live and test paths must flow through the same downstream nodes. The `IsTestRun` branch guards external writes (email replies, label changes, ERP mutations) — it must never be used to skip data or processing steps. Gate every side-effect on the `false` branch together: a single gated label change while the reply or ERP write runs unconditionally defeats the purpose.
|
|
128
|
+
|
|
129
|
+
**If the designated test folder/label was not provided in the build task** → call `report_flag({ kind: "gap" })` and build the rest of what you can. Never fabricate a label id or silently fall back to hard-coded fixtures for an IO-bound workflow.
|
|
130
|
+
|
|
131
|
+
For the concrete mechanism of reading from a Gmail label as a test source, see the `gmail` integration skill.
|
|
132
|
+
|
|
133
|
+
## Gotchas
|
|
134
|
+
|
|
135
|
+
- **One `.trigger()`, the live one.** A `TestTrigger` is a separate top-level `export const`, never a second `.trigger(...)`. The Tests tab discovers it; the live trigger stays in the chain.
|
|
136
|
+
- **`IsTestRun` routes on ports.** Branch its `"true"`/`"false"` ports with `.when`, not a bare `.then`. Put the real write on `false`, the `Assertion` on `true`.
|
|
137
|
+
- **Fixture shape == live payload shape.** If the fixture json doesn't match what the live trigger emits, the shared pipeline won't typecheck or behave the same.
|
|
138
|
+
- **`score` is 1 or 0.** `Assertion` rows are pass (`1`) / fail (`0`); return an array so you can record several checks per item.
|
|
139
|
+
- **Every node must be connected — including TestTrigger.** A node or TestTrigger that is constructed but
|
|
140
|
+
never wired is a defect: normal nodes connect via `.then()`/`.when()`; a `TestTrigger` is "wired" by
|
|
141
|
+
being a top-level `export const` (the Tests tab discovers exports, not orphaned `new TestTrigger(…)`
|
|
142
|
+
calls). If you construct a node and don't connect it, the graph silently ignores it — it will never run.
|