# Canvas Actions

> Create, manage, and interact with canvas HTML pages. Write visual UIs that users see in the AI Maestro dashboard, receive structured interactions when users click/submit/select, and update pages in response. Full lifecycle management for agent-rendered canvases.

- **Type:** Skill
- **Install:** `agentstack add skill-23blocks-os-ai-maestro-plugins-canvas-actions`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [23blocks-OS](https://agentstack.voostack.com/s/23blocks-os)
- **Installs:** 0
- **Category:** [Data & Analytics](https://agentstack.voostack.com/c/data-and-analytics)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [23blocks-OS](https://github.com/23blocks-OS)
- **Source:** https://github.com/23blocks-OS/ai-maestro-plugins/tree/main/plugins/ai-maestro/skills/canvas-actions
- **Website:** https://ai-maestro.23blocks.com

## Install

```sh
agentstack add skill-23blocks-os-ai-maestro-plugins-canvas-actions
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Agent Canvas

Create visual, interactive HTML pages that users see in the AI Maestro dashboard. Embed live data, receive structured actions when users interact, and update pages in response.

## When to use this skill

Use canvas whenever the output is better experienced visually than as terminal text.

### Proactive triggers -- create a canvas when:

- **User asks to see data visually**: "show me", "display", "visualize", "report on", "dashboard for", "chart of", "table of", "summary of"
- **User asks for a form or config UI**: "let me configure", "settings for", "create a form", "let me pick", "I want to choose"
- **User asks for an approval or review flow**: "let me approve", "review these", "I need to decide", "show me the options"
- **User asks you to build something interactive**: "build me a", "create a page", "make a UI", "wizard for", "control panel"
- **Your output has structured data**: test results, API responses, file listings, metrics, logs, comparisons -- anything that would benefit from sorting, filtering, or visual hierarchy
- **Your output has actions the user should take**: approve/reject, select from options, configure settings, trigger operations
- **You are presenting a status or progress report**: build status, deployment state, system health, task progress

### Do NOT use canvas for:

- Quick one-line answers
- Code that should go in a file
- Terminal commands the user should run
- Simple text responses

**Default behavior: When in doubt, create a canvas.** A visual, interactive page is almost always more useful than a wall of terminal text. The user can always read it in the Canvas tab of the dashboard.

## Architecture: Data-Driven Interactive Pages

**CRITICAL: Canvas pages must be data-driven, not static HTML.**

Never hardcode data into HTML elements. Always embed data as JSON in a `` block and render it with JavaScript. This makes pages interactive (sortable, filterable, searchable) instead of dead static text.

### The pattern: Embedded JSON + JS rendering

```html

    
    Test Results
    /* styles here */

    

    
    
    {
        "generatedAt": "2026-05-18T15:30:00Z",
        "summary": { "total": 142, "passed": 135, "failed": 5, "skipped": 2 },
        "tests": [
            { "name": "auth.login", "status": "passed", "duration": 230, "suite": "auth" },
            { "name": "auth.logout", "status": "passed", "duration": 45, "suite": "auth" },
            { "name": "api.users.create", "status": "failed", "duration": 1200, "suite": "api", "error": "Timeout exceeded" },
            { "name": "api.users.list", "status": "passed", "duration": 89, "suite": "api" }
        ]
    }
    

    
    
        const DATA = JSON.parse(document.getElementById('page-data').textContent);

        // State
        let filter = 'all';
        let sortBy = 'name';
        let sortDir = 'asc';
        let search = '';

        function render() {
            let tests = [...DATA.tests];

            // Filter
            if (filter !== 'all') tests = tests.filter(t => t.status === filter);
            if (search) tests = tests.filter(t => t.name.toLowerCase().includes(search.toLowerCase()));

            // Sort
            tests.sort((a, b) => {
                const val = a[sortBy] > b[sortBy] ? 1 : -1;
                return sortDir === 'asc' ? val : -val;
            });

            const app = document.getElementById('app');
            app.innerHTML = `
                Test Results
                Generated ${new Date(DATA.generatedAt).toLocaleString()}

                
                    ${DATA.summary.total} Total
                    ${DATA.summary.passed} Passed
                    ${DATA.summary.failed} Failed
                    ${DATA.summary.skipped} Skipped
                

                
                    
                    
                        All
                        Passed
                        Failed
                        Skipped
                    
                

                
                    
                        
                            Test
                            Suite
                            Status
                            Duration
                        
                    
                    
                        ${tests.map(t => `
                            
                                ${t.name}
                                ${t.suite}
                                ${t.status}
                                ${t.duration}ms
                            
                        `).join('')}
                    
                

                ${tests.length === 0 ? 'No tests match your filters.' : ''}
            `;
        }

        render();
    

```

### Why this pattern matters

| Approach | Result |
|----------|--------|
| Static HTML `` with hardcoded rows | Dead page. User can only read. No sorting, no filtering, no search. |
| Embedded JSON + JS rendering | **Interactive page.** User can sort columns, filter by status, search by name. Same data, 10x more useful. |

### Data block rules

1. **Always use ``** for the data block. This prevents execution and is parseable.
2. **Put ALL data in one JSON block.** Don't scatter data across multiple variables.
3. **Include metadata** in the data: timestamps, totals, source info. The page should explain itself.
4. **Keep the render function pure.** It reads from `DATA` + state variables, rebuilds the DOM. No side effects.
5. **Make every list sortable and filterable.** If you have 5+ items, add search. If items have categories/statuses, add a filter dropdown. If items have numeric fields, make columns sortable.

### Data block examples by content type

**API response data:**
```html

{
    "endpoint": "/api/v1/users",
    "method": "GET",
    "status": 200,
    "responseTime": 142,
    "headers": { "content-type": "application/json", "x-request-id": "abc123" },
    "body": { "users": [...], "total": 50, "page": 1 }
}

```

**File listing / directory tree:**
```html

{
    "root": "/Users/project/src",
    "totalFiles": 42,
    "totalSize": 284000,
    "files": [
        { "path": "index.ts", "size": 1200, "modified": "2026-05-18T10:00:00Z", "type": "typescript" },
        { "path": "utils/helpers.ts", "size": 3400, "modified": "2026-05-17T09:00:00Z", "type": "typescript" }
    ]
}

```

**Metrics / monitoring:**
```html

{
    "collectedAt": "2026-05-18T15:30:00Z",
    "services": [
        { "name": "api-gateway", "status": "healthy", "uptime": 99.97, "latency": 45, "requests": 12400 },
        { "name": "auth-service", "status": "degraded", "uptime": 98.5, "latency": 230, "requests": 8200 }
    ],
    "alerts": [
        { "id": "a1", "severity": "warning", "message": "Auth latency above threshold", "since": "2026-05-18T14:00:00Z" }
    ]
}

```

**Comparison / diff data:**
```html

{
    "left": { "label": "v1.2.0", "date": "2026-05-10" },
    "right": { "label": "v1.3.0", "date": "2026-05-18" },
    "changes": [
        { "file": "src/auth.ts", "type": "modified", "additions": 42, "deletions": 15 },
        { "file": "src/new-feature.ts", "type": "added", "additions": 120, "deletions": 0 }
    ],
    "summary": { "filesChanged": 12, "additions": 340, "deletions": 89 }
}

```

### Interactive features to always include

| Data shape | Interactive features |
|------------|---------------------|
| List/table (5+ rows) | Sort by columns, search, filter by category/status |
| Metrics/numbers | Color coding (green/yellow/red), thresholds, visual bars |
| Status items | Filter by status, group by category, dismiss/acknowledge buttons |
| Timeline/log entries | Newest-first sort, search, level filter (info/warn/error) |
| Config/settings | Editable fields, save button via `maestro.send('submit', ...)` |
| Approval items | Approve/reject buttons via `maestro.send('click', ...)`, comment field |

### Combining data + actions

Pages can be both data-driven AND interactive with `maestro.send()`:

```html

{
    "pendingApprovals": [
        { "id": "pr-42", "title": "Add OAuth support", "author": "alice", "files": 8, "additions": 340 },
        { "id": "pr-43", "title": "Fix login bug", "author": "bob", "files": 2, "additions": 15 }
    ]
}

    const DATA = JSON.parse(document.getElementById('page-data').textContent);

    function render() {
        document.getElementById('app').innerHTML = DATA.pendingApprovals.map(pr => `
            
                ${pr.title}
                by ${pr.author} | ${pr.files} files | +${pr.additions}
                
                    Approve
                    Reject
                
            
        `).join('');
    }

    render();

```

## Your Canvas Directory

Every agent has a canvas directory where HTML files are stored:

```
~/.aimaestro/agents//canvas/
├── dashboard.html          # Your pages go here
├── reports/
│   └── weekly.html         # Subdirectories supported
└── interactions/            # User actions land here (auto-created)
    └── 2026-05-18T15-30-00-000Z-uuid.json
```

Find your agent ID:
```bash
# From environment (set by AI Maestro)
echo $AIM_AGENT_ID

# Or find it in the registry
cat ~/.aimaestro/agents/registry.json | jq '.agents[] | select(.name == "your-agent-name") | .id'
```

## Creating Canvas Pages

Write self-contained HTML files to your canvas directory. The dashboard renders them in a sandboxed iframe with the Canvas tab.

```bash
cat > ~/.aimaestro/agents/$AIM_AGENT_ID/canvas/page.html 

    
    Page Title
    
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; padding: 24px; }
        /* ... your styles ... */
    

    

    
    { /* your data here */ }
    

    
        const DATA = JSON.parse(document.getElementById('page-data').textContent);
        // render logic
    

HTMLEOF
```

### maestro.send() API

Use `maestro.send(action, element, data)` to send user interactions back to your agent. The `maestro` object is automatically injected by the dashboard; you don't need to define it.

```javascript
maestro.send(action, element, data)
```

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `action` | string | Yes | What happened (see Standard Actions below) |
| `element` | string | No | Which UI element was acted on (id, name, or label) |
| `data` | object | No | Arbitrary key-value payload |

### Standard Actions

| Action | Use case | Example |
|--------|----------|---------|
| `click` | Button press, link activation | `maestro.send('click', 'deploy-btn', { env: 'prod' })` |
| `submit` | Form submission | `maestro.send('submit', 'config-form', { name: 'api', timeout: 30 })` |
| `change` | Input value changed | `maestro.send('change', 'search-input', { value: 'query text' })` |
| `select` | Option selected | `maestro.send('select', 'priority', { value: 'high' })` |
| `toggle` | Boolean switch | `maestro.send('toggle', 'dark-mode', { enabled: true })` |
| `dismiss` | Modal/alert dismissed | `maestro.send('dismiss', 'alert-42', { acknowledged: true })` |
| `navigate` | Tab/page switch in canvas | `maestro.send('navigate', 'settings-tab', { tab: 'advanced' })` |
| `custom` | Anything else | `maestro.send('custom', 'drag-drop', { from: 'A', to: 'B' })` |

Custom actions beyond this list are fine. The `action` field is freeform.

## Canvas Rules

HTML must be **self-contained**:
- Inline all CSS (in `` tags or inline styles)
- Inline all JavaScript (in `` tags or inline handlers)
- Embed images as base64 data URIs or use emoji/Unicode
- No external stylesheets, scripts, or images (CDN links will be blocked by the sandbox)
- No relative asset paths

The iframe sandbox allows `allow-scripts` only:
- JavaScript works
- No form submission to external URLs (use `event.preventDefault()` + `maestro.send()`)
- No popups, no same-origin access, no top-level navigation
- No `alert()`, `confirm()`, or `prompt()` (use in-page UI instead)

## Managing Canvas Files

### Organize with subdirectories
```bash
mkdir -p ~/.aimaestro/agents/$AIM_AGENT_ID/canvas/reports
mkdir -p ~/.aimaestro/agents/$AIM_AGENT_ID/canvas/forms
mkdir -p ~/.aimaestro/agents/$AIM_AGENT_ID/canvas/dashboards
```

### List files (API)
```bash
curl -s http://localhost:23000/api/agents/$AIM_AGENT_ID/canvas
```
Returns:
```json
{
  "files": [
    { "name": "dashboard.html", "path": "dashboard.html", "size": 4200, "modifiedAt": "2026-05-18T15:00:00.000Z" },
    { "name": "weekly.html", "path": "reports/weekly.html", "size": 8100, "modifiedAt": "2026-05-18T14:00:00.000Z" }
  ]
}
```

### Read a file (API)
```bash
curl -s "http://localhost:23000/api/agents/$AIM_AGENT_ID/canvas?file=dashboard.html"
```

### Update a file
Overwrite it. The dashboard picks up changes when the user refreshes or re-selects the file.

### Delete a file
```bash
rm ~/.aimaestro/agents/$AIM_AGENT_ID/canvas/old-page.html
```

### Delete all interactions (clean slate)
```bash
rm -f ~/.aimaestro/agents/$AIM_AGENT_ID/canvas/interactions/*.json
```

## Receiving Interactions

When a user interacts with your canvas page, two things happen:

1. **JSON file stored** at `~/.aimaestro/agents//canvas/interactions/-.json`
2. **Terminal notification** appears in your session: `[CANVAS] file.html: User action 'element' on file.html with data: {...}`

### Interaction file format

```json
{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "timestamp": "2026-05-18T15:30:00.000Z",
  "canvasFile": "dashboard.html",
  "action": "submit",
  "element": "approve-button",
  "data": { "comments": "Looks good", "rating": 5 },
  "summary": "User submit 'approve-button' on dashboard.html with data: {\"comments\":\"Looks good\",\"rating\":5}"
}
```

### Reading interactions

```bash
# List all interaction files (newest first)
ls -1r ~/.aimaestro/agents/$AIM_AGENT_ID/canvas/interactions/

# Read the most recent interaction
ls -1r ~/.aimaestro/agents/$AIM_AGENT_ID/canvas/interactions/ | head -1 | \
  xargs -I{} cat ~/.aimaestro/agents/$AIM_AGENT_ID/canvas/interactions/{}

# Read all interactions via API
curl -s http://localhost:23000/api/agents/$AIM_AGENT_ID/canvas/interactions

# Read with limit
curl -s "http://localhost:23000/api/agents/$AIM_AGENT_ID/canvas/interactions?limit=10"
```

### Processing [CANVAS] notifications

When you see a `[CANVAS]` notification, you should act on it. The notification format is:

```
[CANVAS] : User  '' on  with data: {}
```

**Your workflow:**
1. Parse the notification or read the latest interaction file for full details
2. Decide what to do based on action type + element + data
3. Execute the action (run commands, update files, call APIs, send AMP messages, etc.)
4. Optionally update the canvas page to reflect the new state

### Response patterns by action type

**click** -- Execute the operation the button represents:
```
[CANVAS] panel.html: User click 'run-tests' on panel.html with data: {"suite":"unit"}
-> Run the test suite, then update the canvas with results
```

**submit** -- Process the form data:
```
[CANVAS] config.html: User submit 'config-form' on config.html with data: {"endpoint":"https://api.example.com","timeout":30}
-> Save the configuration, confirm success on canvas
```

**select** -- Apply the selection:
```
[CANVAS] dashboard.html: User select 'time-range' on dashboard.html with data: {"value":"7d"}
-> Regenerate the dashboard with 7-day data, update canvas
```

**toggle** -- Enable or disable the feature:
```
[CANVAS] settings.html: User toggle 'auto-deploy' on settings.html with data: {"enabled":true}
-> Enable auto-deploy in your configuration
```

**dismiss** -- Acknowledge and clean up:
```
[CANVAS] alerts.html: User dismiss 'alert-memory' on alerts.html with data: {"acknowledged":true}
-> Mark alert as seen, no further action needed
```

## Security

- Never put credentials, tokens, or secrets in `data` payloads (stored as plaintext JSON)
- Canvas files

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [23blocks-OS](https://github.com/23blocks-OS)
- **Source:** [23blocks-OS/ai-maestro-plugins](https://github.com/23blocks-OS/ai-maestro-plugins)
- **License:** MIT
- **Homepage:** https://ai-maestro.23blocks.com

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-23blocks-os-ai-maestro-plugins-canvas-actions
- Seller: https://agentstack.voostack.com/s/23blocks-os
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
