Build a card.Put it on every canvas.
A card is a small web app that lives on the NeuroSquad canvas next to your agents. Publish it on GitHub and anyone installs it by pasting the address. It can listen to agents, give them tools, send them prompts and trade typed data with other cards over arrows — with exactly the permissions the user grants.
Plain HTML and JavaScript, or React with Vite. The SDK has no runtime dependencies and is MIT-licensed.
Paste owner/repo
A card works alongside your agents
A card sees which agents are on the canvas and what they are doing. Connect one with an arrow, and the card can react to its turns, give it a tool or hand it the next task.
Reacts when an agent finishes
The card subscribes to agent events: status changes, the start and end of a turn, new output. Here the test radar reruns the suite every time the connected agent ends a turn.
card.agents.onTurn(({ agentId, phase }) => { if (phase === 'end') runTests()})Cards talk over arrows, in typed data
A card declares its inputs and outputs with a type: text, Markdown, tasks, tables, events — or a type of your own with a schema. Draw an arrow and the value flows. The app checks it on both ends and converts it where a built-in card expects something else.
Turn arrows on and off, then run the tests.
Built-in cards take part
A note appends Markdown, a checklist adds tasks, a task board adds cards, an agent takes a prompt, a terminal runs a command. Your card talks to all of them the same way.
Types, not guesswork
Well-known types (ns:text, ns:markdown, ns:tasks, ns:table, ns:event…) or your-card/your-type with a JSON schema. The app validates every value before it arrives.
Requests, not only streams
An input can answer: ports.request asks a connected card and waits for its reply. A retained output hands its last value to a card the moment you connect it.
Community code, on a short leash
Before anything installs you see who made it, where it comes from and at which commit, and in plain words what it will be able to do. After that, the app enforces exactly that.
Install Test radar?
Community code, not made or checked by NeuroSquad. Install cards only from people you trust.
Anything inside the card is the card’s own. NeuroSquad never asks for passwords or keys there.
This card will be able to
Run commands in terminals connected to it
High riskType and run commands in terminal cards you connect to it with an arrow — anything you could run yourself.
Read and change cards connected to it
MediumNotes, checklists, task boards and stickies you connect to it with an arrow.
Offers tools to agents you connect it to: run_tests
Exchanges data over arrows: 1 in, 1 out
Sealed in its own frame
Each card runs in a sandboxed frame with its own origin. No access to the app, to Node, to other cards, or to your files beyond what you grant.
Never leaves the canvas
No popups, no new windows, no dialogs over the app, no downloads. A card draws only inside its own box and zooms with the canvas.
Only what you grant
Every call is checked in the app against the grant. Optional permissions are asked for later, on the card, and any permission can be revoked.
Arrows are consent
A card reaches another card, an agent or a terminal only if you drew an arrow between them — and only with the matching permission.
Network through a proxy
Only to the hosts the card declared. The app puts the secrets you enter into requests; the card never sees them.
Pinned, never auto-updated
An install is pinned to a commit. A new version shows what changed and which permissions it adds, and waits for your click.
A whole card, one short file
This is a complete card. It runs the tests in a connected terminal, sends the failures down an arrow, offers agents a run_tests tool and reruns when a connected agent finishes. The manifest next to it says what it needs.
1import { connect } from '@neurosquad/card-sdk'2 3const card = await connect()4 5/** Runs the suite in the terminal this card is wired to and sends out what broke. */6async function runTests(filter = ''): Promise<string[]> {7 const shell = card.ports.peers.find((peer) => peer.kind === 'terminal')8 if (!shell) throw new Error('Draw an arrow from this card to a terminal')9 10 await card.setStatus('Running tests…', { busy: true })11 const { output } = await card.terminals.run(shell.cardId, `npm test -- ${filter}`)12 const failed = output.split('\n').filter((line) => line.includes('✗'))13 14 const tone = failed.length ? 'danger' : 'success'15 await card.setStatus(failed.length ? `${failed.length} failed` : 'Passing', { tone })16 // ns:tasks: a checklist adds them, a note gets a Markdown list17 await card.ports.emit('failures', failed.map((text) => ({ text, status: 'error' })))18 return failed19}20 21// Connected agents see this as test_radar_run_tests over MCP22card.tools.handle<{ filter?: string }>('run_tests', async ({ filter }, call) => {23 call.progress('running…')24 const failed = await runTests(filter)25 return failed.length ? failed.join('\n') : 'All tests passed'26})27 28// A connected agent finished its turn: check its work29card.agents.onTurn(({ agentId, phase }) => {30 const wired = card.ports.peers.some((peer) => peer.cardId === agentId)31 if (phase === 'end' && wired) runTests().catch((error) => card.log.error(error))32})33 34// Anything on the "run" input (a button, a schedule) starts a run35card.ports.onMessage(() => void runTests(), { input: 'run' })One connect()
It waits for the app’s handshake and returns a typed client: every method, event and payload is typed.
The manifest is the contract
Permissions, ports, tools and settings are declared up front — that is what the install dialog shows.
Test it without the app
createMockHost() runs the card against an in-memory host that applies the app’s own checks in the app’s order.
What the client can do
card.setStatus()Title, status, badge, overview tile, attentioncard.uiToasts, confirmations and the card’s menucard.storageStorage per card and per packagecard.settingsA settings form the app draws, secrets includedcard.agentsList, watch, read and prompt agentscard.terminalsRun commands in connected terminalscard.portsTyped inputs and outputs over arrowscard.toolsTools for connected agentscard.netHTTP through the app’s proxy, streaming toocard.fsFiles in the workspace foldercard.lifecycleVisibility, pausing, resizingcreateTranslator()English, Russian and Chinese, switching live
Your first card in three commands
You need Node.js 18.17 or newer, and NeuroSquad with developer mode turned on in Settings → Custom cards.
- 1
Create it
$npx @neurosquad/card-sdk create my-cardA working starter: live agent statuses, a scratchpad that is saved, a port and a tool. Add
--template reactfor React and Vite. - 2
Run it live
$cd my-card && npx @neurosquad/card-sdk devLinks the folder to the app once you confirm, reloads the card on every save and streams its log.
- 3
Check it
$npx @neurosquad/card-sdk validateRuns the app’s own validators and prints the install dialog your users will see.
Then share it
Push the folder to GitHub. Anyone installs it by pasting owner/repo into Settings → Custom cards. Updates are never automatic: each new commit is shown with its permission changes and applied with a click.
Looks like whatever you want
The header, the frame and the menu are the app’s. Everything inside is yours: any HTML, CSS, canvas or 3D. An optional kit follows the app’s live theme if you want the native look.
Before you start
Build the card your canvas is missing
Start from a template, run it live in the app, publish it on GitHub.