blokctl 0.7.0 → 1.1.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/dist/commands/create/project.js +44 -23
- package/dist/commands/create/utils/Examples.d.ts +4 -4
- package/dist/commands/create/utils/Examples.js +90 -27
- package/dist/commands/generate/WorkflowGenerator.js +4 -4
- package/dist/commands/generate/WorkflowGenerator.test.js +2 -2
- package/dist/commands/generate/prompts/create-fn-node.system.js +10 -17
- package/dist/commands/generate/prompts/create-workflow.system.js +114 -375
- package/dist/commands/generate/validators/WorkflowValidator.js +80 -5
- package/dist/commands/generate/validators/WorkflowValidator.test.js +66 -0
- package/package.json +2 -2
|
@@ -1,21 +1,25 @@
|
|
|
1
|
-
const node_file = `import
|
|
1
|
+
const node_file = `import { dirname, join } from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import ApiCall from "@blokjs/api-call";
|
|
2
4
|
import IfElse from "@blokjs/if-else";
|
|
5
|
+
import { discoverNodes } from "@blokjs/runner";
|
|
3
6
|
import type { NodeBase } from "@blokjs/shared";
|
|
4
|
-
import ChainInit from "./nodes/chain-init/index";
|
|
5
|
-
import ChainVerify from "./nodes/chain-verify/index";
|
|
6
7
|
import ExampleNodes from "./nodes/examples/index";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
};
|
|
8
|
+
|
|
9
|
+
// Published nodes (npm) + the example bundle are registered explicitly below.
|
|
10
|
+
// Your OWN nodes under 'nodes/<name>/index.ts' are AUTO-DISCOVERED and registered
|
|
11
|
+
// by their defineNode({ name }) — you never edit this file to add a node.
|
|
12
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const local = await discoverNodes(join(here, "nodes"));
|
|
14
|
+
|
|
15
|
+
const explicit: NodeBase[] = [ApiCall, IfElse, ...(Object.values(ExampleNodes) as NodeBase[])];
|
|
16
|
+
|
|
17
|
+
// Map keys are cosmetic — the runner registers each node under its own node.name
|
|
18
|
+
// (the canonical 'use:' ref). Duplicate refs throw at startup.
|
|
19
|
+
const nodes: { [key: string]: NodeBase } = {};
|
|
20
|
+
for (const node of [...explicit, ...local]) {
|
|
21
|
+
nodes[(node as { name: string }).name] = node;
|
|
22
|
+
}
|
|
19
23
|
|
|
20
24
|
export default nodes;
|
|
21
25
|
`;
|
|
@@ -349,7 +353,52 @@ Blok is a **multi-trigger, multi-runtime workflow framework**. A workflow is a d
|
|
|
349
353
|
The 9 trigger types: \`http\`, \`worker\`, \`cron\`, \`pubsub\`, \`sse\`, \`websocket\`, \`webhook\`, \`mcp\`, \`grpc\`.
|
|
350
354
|
The 8 runtimes: \`typescript\` (in-process), \`go\`, \`rust\`, \`java\`, \`csharp\`, \`php\`, \`ruby\`, \`python3\`.
|
|
351
355
|
|
|
352
|
-
The canonical
|
|
356
|
+
The canonical TypeScript form is the **typed-handle DSL** from \`@blokjs/core\`: \`workflow(name, { version, trigger }, (entry) => { ... })\`, where each \`step()\` returns a typed handle you reference directly — **no \`$\`, no \`js/\`, no raw \`ctx\` strings.** The object-style \`workflow({ name, version, trigger, steps: [...] })\` from \`@blokjs/helper\` and JSON workflows are equivalent (all three compile to the same IR) and remain fully supported. The same shape works for all 9 triggers — only the \`trigger:\` block changes.
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## 0. THE TYPED-HANDLE DSL (\`@blokjs/core\`) — preferred for TypeScript
|
|
361
|
+
|
|
362
|
+
\`\`\`ts
|
|
363
|
+
import { workflow, step, branch, forEach, switchOn, tryCatch, http, tpl, gt } from "@blokjs/core";
|
|
364
|
+
|
|
365
|
+
export default workflow("order-intake", { version: "1.0.0", trigger: http.post("/orders") }, (req) => {
|
|
366
|
+
// step(id, node, inputs) → a TYPED handle shaped like the node's output.
|
|
367
|
+
const order = step("validate", validateOrder, { qty: req.body.qty });
|
|
368
|
+
|
|
369
|
+
// Reference a handle field anywhere downstream — it records a ref the runner resolves.
|
|
370
|
+
step("summary", summarize, { line: tpl\`order of \${order.qty} item(s)\` });
|
|
371
|
+
|
|
372
|
+
// Control flow: a comparator (gt/eq/lt/...) builds the condition; arms are callbacks.
|
|
373
|
+
branch("lane", gt(order.qty, 10), {
|
|
374
|
+
then: () => { step("bulk", routeOrder, { lane: "bulk" }); },
|
|
375
|
+
else: () => { step("standard", routeOrder, { lane: "standard" }); },
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
\`\`\`
|
|
379
|
+
|
|
380
|
+
**Trigger config:** \`http.{get,post,put,delete,patch,any}(path?, opts?)\` for HTTP (omit \`path\` for file-based routing). For any other trigger pass the raw block as \`trigger:\` — e.g. \`{ worker: { queue: "jobs" } }\`, \`{ cron: { schedule: "0 2 * * *" } }\`, \`{ webhook: { source: "stripe" } }\`.
|
|
381
|
+
|
|
382
|
+
**The entry handle** (the callback's argument) is the trigger payload — typed and conventionally named per trigger: \`http\`→\`req\`, \`webhook\`→\`event\`, \`cron\`→\`tick\`, \`worker\`→\`job\`, \`pubsub\`→\`msg\`, \`grpc\`→\`rpc\` (others get a loose handle). Read \`req.body\`, \`req.params.id\`, \`req.query.q\`, \`req.headers["x-…"]\`.
|
|
383
|
+
|
|
384
|
+
**Handles & persistence:**
|
|
385
|
+
- \`const h = step("id", node, inputs)\` — every step auto-persists to \`ctx.state["id"]\` on success; \`h\` / \`h.field\` is how you reference it later (never \`$.state.id\`).
|
|
386
|
+
- 4th arg \`opts\`: \`{ as: "name" }\` roots the handle at \`state["name"]\`; \`{ spread: true }\` flattens \`result.data\` into state (mutually exclusive with \`as\`); \`{ ephemeral: true }\` skips persistence (handle then unreadable downstream); also \`idempotencyKey\`, \`retry\`, \`maxDuration\`, \`type: "runtime.<lang>"\`.
|
|
387
|
+
- \`tpl\`…\${h.field}…\`\` builds a string with embedded handle refs.
|
|
388
|
+
- Comparators (typed): \`eq, ne, gt, gte, lt, lte, not\`. A bare boolean handle is a truthiness check.
|
|
389
|
+
- \`defineNode\` is re-exported from \`@blokjs/core\` — author nodes exactly as before; just RETURN output (never write \`ctx.state\`/\`ctx.vars\`).
|
|
390
|
+
|
|
391
|
+
**Control-flow primitives** (all from \`@blokjs/core\`):
|
|
392
|
+
- \`branch(id, cond, { then: () => {…}, else?: () => {…} })\` — \`cond\` is a comparator or a boolean handle.
|
|
393
|
+
- \`forEach(iterable, (item, index) => { step(…) }, { as?, mode?, concurrency? })\` — \`iterable\` is a handle; \`item\`/\`index\` are per-iteration handles; \`mode: "parallel"\` bounds with \`concurrency\` (default 10).
|
|
394
|
+
- \`switchOn(discriminant, { cases: [{ when: "push", do: () => {…} }], default?: () => {…} }, { id })\` — \`when\` labels are STATIC literals (a handle never matches).
|
|
395
|
+
- \`tryCatch(id, { try: () => {…}, catch: (error) => { step("log", logger, { msg: error.message }); }, finally?: () => {…} })\` — \`error\` is a typed handle (\`.message\`/\`.name\`/\`.stack\`/\`.code\`/\`.stepId\`).
|
|
396
|
+
|
|
397
|
+
**The four footguns** (full detail: \`docs/d/primitives/handles-and-footguns.mdx\`):
|
|
398
|
+
1. **Arm-scoped handles don't escape their arm.** A handle minted inside a \`branch\`/\`switchOn\`/\`tryCatch\`/\`forEach\` arm is unreadable outside it. Return a value from the control-flow step, or have both arms write one \`as:\` key.
|
|
399
|
+
2. **Ephemeral handles are unreadable.** Reading an \`{ ephemeral: true }\` step's handle resolves to \`undefined\`.
|
|
400
|
+
3. **Never reuse a step \`id\`** — including across mutually-exclusive arms. Ids are a flat per-workflow map; the runner throws at load time. Use \`as:\` when two arms must write the same downstream key.
|
|
401
|
+
4. **Never name a \`forEach\` \`as:\`/\`asIndex\` after an existing step id** (or another loop's \`as\`) — they share \`ctx.state\`; the runner throws at load time.
|
|
353
402
|
|
|
354
403
|
---
|
|
355
404
|
|
|
@@ -858,7 +907,7 @@ Process-global middleware: \`WorkflowRegistry.getInstance().setGlobalMiddleware(
|
|
|
858
907
|
Always \`export default defineNode(...)\`. Never class-based \`BlokService\`. Zod input/output are mandatory.
|
|
859
908
|
|
|
860
909
|
\`\`\`ts
|
|
861
|
-
import { defineNode } from "@blokjs/
|
|
910
|
+
import { defineNode } from "@blokjs/core";
|
|
862
911
|
import { z } from "zod";
|
|
863
912
|
|
|
864
913
|
export default defineNode({
|
|
@@ -1211,12 +1260,12 @@ Plus the cross-runtime rules: **the wrong input source** (typed sidecar nodes re
|
|
|
1211
1260
|
|
|
1212
1261
|
## 8. TESTING
|
|
1213
1262
|
|
|
1214
|
-
Use the \`@blokjs/
|
|
1263
|
+
Use the \`@blokjs/core/testing\` utilities with Vitest (same package you author against).
|
|
1215
1264
|
|
|
1216
1265
|
**Unit-test a node** with \`NodeTestHarness\`:
|
|
1217
1266
|
|
|
1218
1267
|
\`\`\`ts
|
|
1219
|
-
import { NodeTestHarness } from "@blokjs/
|
|
1268
|
+
import { NodeTestHarness } from "@blokjs/core/testing";
|
|
1220
1269
|
import myNode from "../src/nodes/my-node";
|
|
1221
1270
|
|
|
1222
1271
|
const harness = new NodeTestHarness(myNode);
|
|
@@ -1228,7 +1277,7 @@ harness.assertOutput(result, { user: { id: "abc-123" } });
|
|
|
1228
1277
|
**Integration-test a workflow** with \`WorkflowTestRunner\`:
|
|
1229
1278
|
|
|
1230
1279
|
\`\`\`ts
|
|
1231
|
-
import { WorkflowTestRunner } from "@blokjs/
|
|
1280
|
+
import { WorkflowTestRunner } from "@blokjs/core/testing";
|
|
1232
1281
|
|
|
1233
1282
|
const runner = new WorkflowTestRunner({ verbose: true, mockAllNodes: true });
|
|
1234
1283
|
runner.registerNode("validate", ValidateNode);
|
|
@@ -1334,7 +1383,7 @@ Per-step reliability lives on the step: \`idempotencyKey\` (cache by \`(workflow
|
|
|
1334
1383
|
Always \`export default defineNode(...)\` (TS) — never class-based \`BlokService\`. Zod input/output are mandatory. Never write \`ctx.state\` from a node — return your output and let the runner persist it (use \`ctx.publish(name, value)\` for a true side-channel). No \`any\` types — use \`z.unknown()\`.
|
|
1335
1384
|
|
|
1336
1385
|
\`\`\`typescript
|
|
1337
|
-
import { defineNode } from "@blokjs/
|
|
1386
|
+
import { defineNode } from "@blokjs/core";
|
|
1338
1387
|
import { z } from "zod";
|
|
1339
1388
|
|
|
1340
1389
|
export default defineNode({
|
|
@@ -1388,7 +1437,21 @@ Registration is **manual** in non-TS runtimes — importing the module runs the
|
|
|
1388
1437
|
|
|
1389
1438
|
## 4. Generating Workflows
|
|
1390
1439
|
|
|
1391
|
-
|
|
1440
|
+
**Preferred (TypeScript): the typed-handle DSL from \`@blokjs/core\`.** \`workflow(name, { version, trigger }, (entry) => { ... })\` — each \`step(id, node, inputs)\` returns a typed handle you reference directly; no \`$\`, no \`js/\`, no raw \`ctx\` strings. \`http.{get,post,…}(path?)\` builds the trigger; other triggers pass the raw block (\`{ worker: { queue } }\`, \`{ cron: { schedule } }\`, …). Entry name per trigger: \`http\`→\`req\`, \`worker\`→\`job\`, \`cron\`→\`tick\`, \`webhook\`→\`event\`, \`pubsub\`→\`msg\`, \`grpc\`→\`rpc\`.
|
|
1441
|
+
|
|
1442
|
+
\`\`\`typescript
|
|
1443
|
+
import { workflow, step, branch, gt, http } from "@blokjs/core";
|
|
1444
|
+
|
|
1445
|
+
export default workflow("Process Order", { version: "1.0.0", trigger: http.post("/orders") }, (req) => {
|
|
1446
|
+
const order = step("validate", orderValidator, { order: req.body }); // typed handle
|
|
1447
|
+
step("save", orderStore, { data: order }); // reference it directly
|
|
1448
|
+
branch("big", gt(order.total, 100), { then: () => { step("vip", flagVip, { id: order.id }); } });
|
|
1449
|
+
});
|
|
1450
|
+
\`\`\`
|
|
1451
|
+
|
|
1452
|
+
Control flow (all from \`@blokjs/core\`): \`branch(id, cond, {then,else?})\`, \`forEach(iterable, (item,i)=>{}, {as?,mode?})\`, \`switchOn(disc, {cases:[{when,do}],default?}, {id})\`, \`tryCatch(id, {try,catch:(err)=>{},finally?})\`; comparators \`eq/ne/gt/gte/lt/lte/not\`; \`tpl\`…\${h.x}…\`\` for strings. The **four footguns**: arm-scoped handles don't escape their arm; ephemeral handles read \`undefined\`; never reuse a step \`id\`; never name a \`forEach\` \`as:\` after an existing id. See \`docs/d/primitives/handles-and-footguns.mdx\`.
|
|
1453
|
+
|
|
1454
|
+
**Equivalent object-style form** (\`@blokjs/helper\`, also valid; JSON mirrors it): one object literal, \`steps: [...]\`, reference outputs with \`$.state.<id>\` / \`$.req.body\`.
|
|
1392
1455
|
|
|
1393
1456
|
\`\`\`typescript
|
|
1394
1457
|
import { workflow, $ } from "@blokjs/helper";
|
|
@@ -1461,7 +1524,7 @@ branch({ id: "route",
|
|
|
1461
1524
|
- Do NOT generate class-based \`BlokService\` nodes or use \`any\` types — always \`defineNode()\` (TS) / \`@node\` (Python) with Zod/Pydantic schemas.
|
|
1462
1525
|
- Do NOT use ESLint/Prettier — this project uses Biome. Do NOT edit auto-generated files in \`.blok/runtimes/\`.
|
|
1463
1526
|
`;
|
|
1464
|
-
const function_first_node_file = `import { defineNode } from "@blokjs/
|
|
1527
|
+
const function_first_node_file = `import { defineNode } from "@blokjs/core";
|
|
1465
1528
|
import { z } from "zod";
|
|
1466
1529
|
|
|
1467
1530
|
/**
|
|
@@ -1486,13 +1549,13 @@ export default defineNode({
|
|
|
1486
1549
|
// Execute function - type-safe with inferred types from Zod schemas
|
|
1487
1550
|
async execute(ctx, input) {
|
|
1488
1551
|
// Your business logic here
|
|
1489
|
-
// - ctx.vars: Access workflow variables
|
|
1490
1552
|
// - ctx.request: Access HTTP request data
|
|
1491
1553
|
// - ctx.logger: Log messages
|
|
1492
1554
|
// - ctx.env: Access environment variables
|
|
1493
|
-
|
|
1494
|
-
//
|
|
1495
|
-
ctx.
|
|
1555
|
+
//
|
|
1556
|
+
// Just RETURN your output — the runner auto-persists it to
|
|
1557
|
+
// ctx.state["{{NODE_NAME}}"], readable from any later step's handle.
|
|
1558
|
+
// Do NOT write to ctx.state / ctx.vars inside a node.
|
|
1496
1559
|
|
|
1497
1560
|
// Return type-safe output (validated automatically)
|
|
1498
1561
|
return {
|
|
@@ -101,11 +101,11 @@ export default class WorkflowGenerator {
|
|
|
101
101
|
"```",
|
|
102
102
|
"",
|
|
103
103
|
"Please fix these errors and regenerate the workflow JSON. Common fixes:",
|
|
104
|
-
"- Ensure every step
|
|
104
|
+
"- Ensure every step has a unique `id` and a `use` (or a `branch`) — inputs live inline; there is NO `nodes` map",
|
|
105
105
|
"- Ensure the trigger has exactly one trigger type with valid configuration",
|
|
106
|
-
"- Ensure
|
|
107
|
-
"-
|
|
108
|
-
"-
|
|
106
|
+
"- Ensure branch `when` is a raw `ctx.*` expression (never `js/` or `$.`)",
|
|
107
|
+
"- Reference earlier outputs with `$.state.<id>` and request data with `$.req.*`",
|
|
108
|
+
"- Use a single step with `branch: { when, then, else }` for conditionals (not a `conditions` array)",
|
|
109
109
|
].join("\n");
|
|
110
110
|
return feedback;
|
|
111
111
|
}
|
|
@@ -70,8 +70,8 @@ describe("WorkflowGenerator", () => {
|
|
|
70
70
|
});
|
|
71
71
|
it("should include common fix suggestions", () => {
|
|
72
72
|
const result = createFeedback("Test", "{}", ["Missing node entry"]);
|
|
73
|
-
expect(result).toContain("
|
|
74
|
-
expect(result).toContain("else
|
|
73
|
+
expect(result).toContain("unique `id`");
|
|
74
|
+
expect(result).toContain("branch: { when, then, else }");
|
|
75
75
|
});
|
|
76
76
|
});
|
|
77
77
|
});
|
|
@@ -9,7 +9,7 @@ What to return:
|
|
|
9
9
|
1. Proper imports:
|
|
10
10
|
* \`z\` from \`zod\`
|
|
11
11
|
* \`Context\` from \`@blokjs/shared\`
|
|
12
|
-
* \`defineNode\` from \`@blokjs/
|
|
12
|
+
* \`defineNode\` from \`@blokjs/core\`
|
|
13
13
|
2. A clear and structured \`input\` schema using Zod (z.object with proper types).
|
|
14
14
|
3. A matching \`output\` schema using Zod.
|
|
15
15
|
4. A single exported node instance created via \`defineNode\` with:
|
|
@@ -26,11 +26,11 @@ Constraints:
|
|
|
26
26
|
* The Zod \`output\` schema must fully describe the object returned by \`execute\`.
|
|
27
27
|
* Inside \`execute(ctx, input)\`:
|
|
28
28
|
* Use the strongly-typed \`input\`, which is automatically inferred from the Zod schema.
|
|
29
|
-
* Use \`ctx\` to access request data, configuration, and
|
|
29
|
+
* Use \`ctx\` to access request data, configuration, and logging when needed:
|
|
30
30
|
* \`ctx.request.body\`, \`ctx.request.query\`, \`ctx.request.params\` for HTTP data
|
|
31
|
-
* \`ctx.vars\` for reading/writing values shared between nodes
|
|
32
31
|
* \`ctx.logger\` for logging
|
|
33
32
|
* \`ctx.env\` for environment variables
|
|
33
|
+
* Do **not** write to \`ctx.state\`/\`ctx.vars\` from a node — just RETURN your output; the runner auto-persists it to \`ctx.state[<step-id>]\` for downstream steps. Upstream values arrive as \`input.*\` (mapped in the workflow), not by reading \`ctx.vars\`.
|
|
34
34
|
* Do **not** construct or return \`BlokResponse\` here; just return a plain object matching the output schema. The wrapper created by \`defineNode\` will call \`setSuccess\` / \`setError\` and handle \`GlobalError\`.
|
|
35
35
|
* On validation errors or runtime errors, you do NOT manually throw \`GlobalError\`; throw/rethrow normal errors. The \`defineNode\` wrapper will catch them and map them to \`GlobalError\` consistently with proper error codes:
|
|
36
36
|
* Zod validation errors → 400 Bad Request
|
|
@@ -52,7 +52,7 @@ Real-World Examples to Guide You:
|
|
|
52
52
|
\`\`\`typescript
|
|
53
53
|
import type { Context } from "@blokjs/shared";
|
|
54
54
|
import { z } from "zod";
|
|
55
|
-
import { defineNode } from "@blokjs/
|
|
55
|
+
import { defineNode } from "@blokjs/core";
|
|
56
56
|
|
|
57
57
|
export default defineNode({
|
|
58
58
|
name: "api-call",
|
|
@@ -102,10 +102,6 @@ export default defineNode({
|
|
|
102
102
|
|
|
103
103
|
ctx.logger.log(\`Request completed in \${duration.toFixed(2)}ms with status \${response.status}\`);
|
|
104
104
|
|
|
105
|
-
if (ctx.vars) {
|
|
106
|
-
ctx.vars["api-response"] = { status: response.status, data };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
105
|
return {
|
|
110
106
|
status: response.status,
|
|
111
107
|
statusText: response.statusText,
|
|
@@ -122,7 +118,7 @@ export default defineNode({
|
|
|
122
118
|
\`\`\`typescript
|
|
123
119
|
import type { Context } from "@blokjs/shared";
|
|
124
120
|
import { z } from "zod";
|
|
125
|
-
import { defineNode } from "@blokjs/
|
|
121
|
+
import { defineNode } from "@blokjs/core";
|
|
126
122
|
|
|
127
123
|
export default defineNode({
|
|
128
124
|
name: "fetch-user",
|
|
@@ -149,11 +145,8 @@ export default defineNode({
|
|
|
149
145
|
// Simulate database fetch
|
|
150
146
|
const user = await fetchUserFromDatabase(input.userId, input.includeMetadata);
|
|
151
147
|
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
ctx.vars["current-user"] = user;
|
|
155
|
-
}
|
|
156
|
-
|
|
148
|
+
// Just return — the runner persists this to ctx.state["fetch-user"]
|
|
149
|
+
// for any downstream step that maps it in as an input.
|
|
157
150
|
return { user };
|
|
158
151
|
},
|
|
159
152
|
});
|
|
@@ -176,7 +169,7 @@ Template to follow (adapt and fill based on the user's request):
|
|
|
176
169
|
|
|
177
170
|
import type { Context } from "@blokjs/shared";
|
|
178
171
|
import { z } from "zod";
|
|
179
|
-
import { defineNode } from "@blokjs/
|
|
172
|
+
import { defineNode } from "@blokjs/core";
|
|
180
173
|
|
|
181
174
|
/**
|
|
182
175
|
* [Brief description of what this node does]
|
|
@@ -210,11 +203,11 @@ export default defineNode({
|
|
|
210
203
|
//
|
|
211
204
|
// Common patterns:
|
|
212
205
|
// - Read HTTP params: const id = ctx.request.params.id;
|
|
213
|
-
// - Read
|
|
214
|
-
// - Write for future nodes: ctx.vars["this-node-key"] = someValue;
|
|
206
|
+
// - Read upstream values via input.* (mapped from prior steps in the workflow) — NOT ctx.vars
|
|
215
207
|
// - Use input.* fields that match the input schema (TypeScript infers the type automatically)
|
|
216
208
|
// - Log messages: ctx.logger.log("Processing request");
|
|
217
209
|
// - Access environment: const apiKey = ctx.env.API_KEY;
|
|
210
|
+
// - Do NOT write ctx.state/ctx.vars here — just return; the runner persists your output.
|
|
218
211
|
|
|
219
212
|
// TODO: Implement business logic here
|
|
220
213
|
|