Keen Extension (Keen Builder)
The Keen VS Code extension — Keen Builder — is where you design, test, and ship flows. It gives you a visual canvas (powered by React Flow) where you wire nodes together into pipelines, plus tooling to debug them against a live sandbox.
Sections
- Project structure
- The flow canvas
- Node panel
- Building your first flow
- Debugging
- Deploying
- Node reference — one file per node type
Project structure
A Keen project on disk looks like:
<project>/
├── .vscode/
│ ├── keen-project.json ← holds the debug node IDs
│ └── launch.json ← VS Code debugger attach config
├── src/
│ ├── agents/ ← agent configurations
│ ├── flows/ ← flow JSON files (what you edit on the canvas)
│ │ ├── <Project>.flow.json
│ │ └── <Project>.flow.map.json
│ └── scripts/ ← custom JS files used by Script Nodes
│ └── tools/ ← JS files that act as reusable tools
└── dist/
├── flows/ ← compiled flow JS (generated on save)
└── scripts/ ← compiled scripts
You edit src/. The extension regenerates dist/ on every save.
.vscode/launch.json
Standard VS Code attach configs that connect the source-level debuggers to the Keen runtime in your sandbox. Keen ships two Chrome DevTools Protocol (CDP) debug proxies — one for flow nodes and one for script nodes — so the extension generates an attach config for each:
{
"version": "0.2.0",
"configurations": [
{
"name": "Attach Script",
"type": "node",
"request": "attach",
"address": "localhost",
"port": 7100,
"timeout": 30000,
"skipFiles": ["<node_internals>/**"],
"sourceMaps": true,
"localRoot": "${workspaceFolder}/src",
"remoteRoot": "/app/projects/<projectId>"
},
{
"name": "Attach Flow",
"type": "node",
"request": "attach",
"address": "localhost",
"port": 7101,
"timeout": 30000,
"skipFiles": ["<node_internals>/**"],
"sourceMaps": true,
"localRoot": "${workspaceFolder}/src",
"remoteRoot": "/app/projects/<projectId>"
}
]
}
- Port
7100→ the Script debugger (pause inside your ownsrc/scripts/*.js). - Port
7101→ the Flow debugger (pause on a node boundary as the graph executes).
remoteRoot may differ per sandbox — the extension generates these for you on project init. See Debugging for how the two proxies work.
The flow canvas
The canvas is the React Flow viewport. You drag nodes onto it from the Node Panel, wire them with edges, and the engine executes the graph at runtime.
Edit menu
| Action | What it does |
|---|---|
| Group selection | Combines a set of selected nodes into a group |
| Ungroup selection | Splits a selected group back into individual nodes |
| Copy selection | Copies the selected nodes to memory with new IDs |
| Paste selection | Drops the copied selection back onto the canvas |
| Undo | Reverts the last canvas edit |
| Redo | Re-applies an undone edit |
Action menu (5 icons)
| Icon | Action |
|---|---|
| Sun/moon | Switch theme — dark / light |
| Plus magnifier | Zoom in |
| Minus magnifier | Zoom out |
| Dashed square | Fit view (frame all nodes) |
| Side panel | Open / close the Node Panel |
Node panel
The right-side panel groups nodes into two categories:
Constructal nodes
Core building blocks for any flow:
Helper nodes
For composition and parallelism:
A flow terminates at a single End node — there are no separate Stop or Exit nodes (they were removed). See the full node reference for every type.
Building your first flow
- Set up the agent in
/org— give it a name, a Start Flow file name, and a Start Node label. (See Organization.) - Create the flow file — in
src/flows/, create<StartFlow>.flow.json. The file name must match the Start Flow value in/org. - Drop a Start Node — open the Node Panel, drag a Start Flow node onto the canvas. Its label must match the agent's Start Node label, capitalized, single word.
- Wire the rest — drag in additional nodes (Agent, Script, Condition, etc.) and connect them via the dots on each node (
next_1,next_2, …). - Save — the extension generates
dist/flows/<StartFlow>.flow.jsautomatically. - Trigger from Chat — send a message to the agent in the Chat. The flow runs.
Node IDs and types
Every node has a unique ID prefixed by its type:
{
"id": "startNode_6N8CbNmtrtYU5BRbKhmkK7o_8lto34IN",
"type": "startNode_",
"data": { "label": "startNode_", "settings": { "label": "Start" } }
}
The prefix in the ID and the type field tell the runtime how to execute the node. Don't hand-edit IDs.
Output edges
Each node has output dots: next_1, next_2, next_3, … The engine picks one based on what the node decides at runtime, then walks to the connected target. Some nodes use named edges instead of numbers (e.g. Script Node has next_1 and error).
If an edge has no connection, the flow ends there.
Debugging
Keen gives you two source-level debuggers, both attaching from VS Code over the configs in launch.json. Each is fronted by its own CDP debug proxy, and the two own mutually exclusive breakpoint surfaces:
| Depth | How | Attach port |
|---|---|---|
| Node / graph level | select node(s), press F9 → they highlight green; pause on a node and inspect live dictionary / session, step node-to-node. | 7101 (Flow proxy) |
| Inside your code | set a line breakpoint in a src/scripts/*.js file; break inside exec, step, watch, walk the call stack across imports. | 7100 (Script proxy) |
Under the hood, each engine runs the node/script logic in a disposable per-request V8 isolate that carries a live inspector. Because those isolates are created and destroyed per request, they can't be published on a normal --inspect port. Each engine therefore exposes a persistent inspector bridge (internally on port :9000) that multiplexes every transient isolate's session onto one stable endpoint, and the CDP proxy republishes that bridge to VS Code on 7100 / 7101. The Flow proxy filters frames so you only ever pause on the flow debugger's debux.js probe; the Script proxy filters so you only pause in your own project scripts (never the engine's source or its system/* resources).
Per-node debugging (F9)
Select one or more nodes on the canvas and press F9. The selected nodes turn green and become debug points. When the flow runs, execution pauses on entry to each green node so you can inspect state in the VS Code debugger.
The list of currently green nodes is persisted to .vscode/keen-project.json, so debug points survive editor restarts.
Per-script debugging
Open the JS file in src/scripts/, click in the gutter to set a breakpoint. When a Script Node runs that file, the debugger stops there.
Inspecting state
Once the debugger pauses, you can inspect:
dictionary— the per-flow state objectprops— values passed in via the Script Node's Engine Key|Value Pairs- The
system/*modules you've imported
Deploying
Saving in the extension regenerates dist/. The compiled JSON looks like:
{
"startNode_wUOcS-_Rl_ED3wvDTBXIEzDUTr6hwCIh": {
"next_1": ["assignmentNode_Jh0KW15H45bk-Wo4WFxgGAeyjKj9Y6dW"],
"data": {
"label": "startNode_",
"settings": { "label": "Start" },
"caller": "Project.flow.json"
}
},
"agentNode_broadcaster__receiver_3T3yhl5pGpjp_yEORKyKg_jVaoHTQyFR": {
"next_1": ["endNode_3zN7i65Tpp72LJPWzt0LhVb4JzNwj7Dl"],
"data": {
"label": "agentNode_broadcaster__receiver_",
"settings": {
"agentName": "Guru",
"prompts": [{ "role": "system", "content": "You are dev assistant." }],
"llm": {
"company": "google",
"model": "gemini-2.5-pro",
"modelId": 4914,
"parameters": { "temperature": 0.8, "top_p": 0.8, "maxTokens": 65536 }
},
"filePaths": [],
"specialRules": []
},
"caller": "Project.flow.json"
}
}
}
Each node is keyed by its full id; its outgoing edges are arrays of target node ids per handle (next_1, next_2, error), and the data block carries label, settings, and caller (the originating flow file). An End Node compiles to the terminator sentinel "next_1": [1] — the literal 1 means "the flow ends here".
Each compiled file is what the engine actually loads. You don't need to edit these by hand — the extension keeps them in sync with your canvas.
At run time the client drives the flow node-by-node through the engine: each flow node executes as one runNode() call in a disposable, memory-capped isolated-vm isolate (20 MB), and each Script Node runs its exec() in its own 30 MB isolate. The isolates are created and disposed per request, so a runaway node or script can't take down the engine.
Special node roles
Nodes are tagged in their internal label as receiver, broadcaster, or both:
- Receiver — accepts only IN connections (terminal-shaped)
- Broadcaster — has only OUT connections (source-shaped)
- Broadcaster/Receiver — both
Example: agentNode_broadcaster__receiver_ participates in both directions.
You don't normally need to think about this — the canvas only lets you make valid connections — but it explains some of the labels you'll see in the JSON.