Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions docs/how-to-create-a-canvas.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# How to Create a Canvas

## Purpose

Canvas files define visual node-based programs where nodes execute code and connect via edges to pass data. Each node can:
- Execute TypeScript/JavaScript code
- Receive input from connected nodes
- Emit data along labeled edges
- Aggregate inputs from multiple sources
- Maintain state across invocations

## Format (JSON)

Canvas files are JSON with two main arrays:

```json
{
"nodes": [
{
"id": "unique-node-id",
"type": "text",
"text": "node content",
"x": 0,
"y": 0,
"width": 250,
"height": 100
}
],
"edges": [
{
"id": "unique-edge-id",
"fromNode": "source-node-id",
"fromSide": "bottom",
"toNode": "target-node-id",
"toSide": "top",
"label": "optional-edge-label"
}
]
}
```

## Node Instruction Format

Nodes execute code written in TypeScript/JavaScript code blocks:

### Basic Code Execution
````markdown
```ts
// Access input from incoming edges
console.log(input)

// Return value sent along default edges
return 'result'
```
````

### Emissions (Named Outputs)
````markdown
```ts
// Emit values along labeled edges
emit('label-name', 'value')

// Default return still works
return 'main-result'
```
````

### Aggregation
````markdown
```ts
// Wait for all incoming edges and collect their values
const values = aggregate().list()
return values // Returns array of all inputs
```
````

### State Management
````markdown
```ts
// Access persistent state across invocations
this.counter = (this.counter || 0) + 1
return this.counter
```
````

### Special Syntax
- **Markdown blocks**: Text nodes without code blocks are treated as markdown
- **Variables**: `{{expression}}` syntax for templating (e.g., `**log**: {{input}}`)
- **Special prefixes**:
- `**log**: message` - Log output
- `**emit**: label` - Emit along labeled edge
- `**on**: event-name` - Event trigger nodes
19 changes: 13 additions & 6 deletions packages/canvas-engine/src/runtime/exec-canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,19 @@ export async function execCanvas(inital_canvas: ExecutableCanvas, gctx: GlobalCo
.find((frame) => {
const ancestors = frame.chart.node_ancestors.get(frame.node.id)!
const not_on_stack = stack.every((frame) => !ancestors.has(frame.node.id))

return not_on_stack
&& active_frames
.every((f) => {
return f.frame === frame || !ancestors.has(f.frame.node.id)
})
const no_active_ancestors = active_frames
.every((f) => {
return f.frame === frame || !ancestors.has(f.frame.node.id)
})

// Also check that no other frames for the SAME node are still executing
// The aggregate should only complete when ALL frames for this node have finished their initial execution
const no_sibling_frames = active_frames
.every((f) => {
return f.frame === frame || f.frame.node !== frame.node
})

return not_on_stack && no_active_ancestors && no_sibling_frames
})


Expand Down