26 KiB
GitHub Copilot App Canvas reference
Research date: 2026-08-26
This note explains how GitHub Copilot App canvases work from creation through shutdown and resume. It also records the current experimental wire protocol.
This note describes Copilot. It does not select a Kandev architecture.
Source levels
This note uses these source levels:
| Level | Meaning |
|---|---|
| GitHub documentation | Public product behavior that GitHub documents. |
| Canonical SDK | Current types, tests, and comments in github/copilot-sdk. |
| First-party example | Canvas code in the github/awesome-copilot repository. |
| Third-party example | Public code that uses the SDK but does not define the product contract. |
| Inference | A conclusion that follows from source code but is not a stated contract. |
| Unknown | Behavior that the public sources do not define. |
The Canvas SDK marks its protocol as experimental. Names and payloads can change before the protocol becomes stable.
The protocol details in this note use these source revisions:
github/copilot-sdkat29141a4cc779191f9b292a280daaddd3597cacacgithub/awesome-copilotat71f7c9b1dc5044287b62fc700efc034da4065f87leestott/copilot-canvas-runtimeat3b9bea89b24f8dfc3327773c81a80265da00cdbe
Executive summary
A Copilot canvas is an extension-owned web application. The Copilot host shows the application in its canvas panel.
A person describes the required surface with /create-canvas. The agent writes
the extension code and opens the completed canvas. A person can also install a
prebuilt canvas from a plugin.
The extension registers a canvas declaration and action declarations. Each action includes a name, a description, and an optional JSON Schema.
Opening a canvas does not transfer HTML through the Canvas protocol. The host asks the extension to open an instance. The extension returns a URL, title, and optional status.
The host loads the URL in its web renderer. First-party examples return a loopback URL that serves arbitrary HTML, CSS, and JavaScript.
The extension owns the canvas application and its state. Human controls and agent actions can call the same extension logic. The SDK does not define the web protocol between the iframe and the extension.
The public examples commonly use HTTP commands and Server-Sent Events (SSE). This HTTP and SSE design is an example pattern, not the Canvas SDK protocol.
Core terms
| Term | Meaning |
|---|---|
| Extension | Code that joins a Copilot session and contributes capabilities. |
| Canvas declaration | Metadata and agent actions for one canvas type. |
| Provider | The live connection that owns a canvas declaration and its handlers. |
extensionId |
The runtime identifier for the provider. |
canvasId |
The provider-local identifier for one canvas type. |
instanceId |
A stable identifier for one open canvas instance. |
| Renderer URL | The URL that the provider returns when an instance opens. |
| Canvas action | A named, JSON-shaped capability that an agent or host can invoke. |
| Application state | Data owned by the extension, a file, a service, or another system. |
A canvas declaration is not a canvas instance. One declaration can serve
multiple instances. The instanceId separates their runtime state.
End-to-end lifecycle
1. A person asks an agent to create a canvas
GitHub documents this creation flow:
- A person starts or opens an agent session.
- The person enters
/create-canvasand describes the workflow. - The person describes human controls and agent capabilities.
- The person selects project scope or user scope.
- The agent writes the extension and opens the canvas panel.
- The person asks the agent to revise the interface or capabilities.
Project extensions live under .github/extensions. A repository can share
these extension files with its team.
User extensions live under ~/.copilot/extensions. These extension files stay
personal to the local user.
GitHub states that an extension commonly contains:
package.jsonfor metadata and dependenciesextension.mjsfor behavior and capabilities- optional JSON artifacts for persistent application data
The extension source is the canvas definition. The agent creates or changes that source. The person directs and reviews the result.
2. Copilot discovers and starts the extension
The extension entry file calls joinSession. It supplies one or more canvases
that were created with createCanvas.
The SDK sends canvas declarations during session.create or session.resume.
Handler functions stay in the extension process. The wire declaration contains
only metadata and schemas.
The renderer connection must request canvas support. The current SDK calls this
option requestCanvasRenderer. This option exposes these agent tools:
list_canvas_capabilitiesopen_canvasinvoke_canvas_action
The separate requestExtensions option exposes extension management and
extension dispatch features.
3. A person or agent opens an instance
The caller supplies a canvasId, an instanceId, and optional input. The
caller can also supply an extensionId when several providers use the same
canvasId.
The runtime resolves the declaration and calls the provider's canvas.open
handler. The provider receives session, provider, canvas, and instance
identifiers. It also receives the open input and limited host context.
The provider starts or reuses the web application. The provider returns a URL, a title, and an optional status.
The host records the open instance and loads the URL in the canvas panel. The current SDK describes this URL as the URL for a web-rendered canvas.
Reopening the same instanceId focuses the existing panel. Renderer reload is
a host concern. The current provider API has no separate focus or reload
callback.
sequenceDiagram
participant Caller as Person or agent
participant Runtime as Copilot runtime
participant Provider as Extension provider
participant Web as Canvas web app
participant Panel as Canvas panel
Caller->>Runtime: open_canvas(canvasId, instanceId, input)
Runtime->>Provider: canvas.open(provider context)
Provider->>Web: Start or reuse application server
Web-->>Provider: Renderer URL
Provider-->>Runtime: URL, title, status
Runtime-->>Panel: Open instance and load URL
Panel->>Web: GET renderer URL
Web-->>Panel: HTML, CSS, JavaScript
4. A person changes the canvas
The Canvas SDK does not prescribe this path. Each extension defines it.
First-party examples use normal browser communication:
- A person clicks a control in the canvas page.
- The page sends an HTTP request to the extension server.
- The server validates and applies the operation.
- The server changes the shared application state.
- The server sends an SSE update to the page.
- The page shows the new state.
Some canvas pages also ask the agent to do work. The first-party Color Orb example sends a new session prompt after a person clicks its request button.
5. An agent changes the canvas
The agent first discovers the declared actions and their input schemas. It then
calls invoke_canvas_action for an open instance.
The runtime routes the invocation to the live provider. The provider receives
the instanceId, action name, and JSON input. The SDK dispatches the request to
the action handler that createCanvas registered.
The handler changes the same application state that the human path uses. The handler can then notify the page through SSE, WebSocket, polling, or another extension-defined channel.
The action handler returns any JSON value. The runtime returns that value to the agent.
sequenceDiagram
participant Agent
participant Runtime as Copilot runtime
participant Provider as Extension provider
participant State as Shared application state
participant Page as Canvas page
Agent->>Runtime: invoke_canvas_action(instanceId, actionName, input)
Runtime->>Provider: canvas.action.invoke(provider context)
Provider->>State: Apply action
Provider-->>Page: Extension-defined update, commonly SSE
Provider-->>Runtime: JSON result
Runtime-->>Agent: Action result
6. The host closes an instance
The user, agent, or host can close an instance. The runtime calls the
provider's canvas.close handler.
The extension can close web servers, SSE clients, browser contexts, and other
instance resources. The onClose callback is a notification. Its return value
is ignored, and callback errors are logged.
State lifetime is an extension decision. A provider can erase state on close, retain state in memory, or save state to a file.
7. The provider disconnects or reloads
The protocol defines session.canvas.unavailable for a provider disconnect.
This event is transient.
The host keeps the panel mounted and shows a reconnecting state. A later
session.canvas.opened event with the same instanceId supplies a fresh URL.
First-party examples also guard against orphaned pages. The Chat Cards example limits SSE retries because a hidden WebView can outlive its extension process.
8. A session resumes
The runtime has durable and transient canvas records.
session.canvas.recorded stores enough information to restore an open instance.
It omits the transient renderer URL and availability state.
session.canvas.removed records that an instance closed. It supersedes the
earlier recorded event for that instance.
The SDK accepts an openCanvases snapshot during session.resume. The runtime
can reopen or reattach those instances without creating new identities.
This mechanism restores panel identity and open input. It does not persist the canvas application's domain state. The extension must persist that state.
The two protocol layers
Copilot canvases have two separate protocol layers.
| Layer | Participants | Standardized by GitHub | Typical transport |
|---|---|---|---|
| Canvas control protocol | Host, runtime, provider, agent | Yes, but experimental | JSON-RPC and session events |
| Canvas application protocol | Canvas page and extension application | No | HTTP, SSE, WebSocket, polling, or custom code |
The first layer discovers and controls a canvas instance. The second layer implements the actual application.
This separation is the source of the canvas's flexibility. The SDK does not limit the application to a fixed widget or block schema.
Canvas declaration
The current Node SDK uses this declaration shape:
{
"id": "agentic-kanban",
"displayName": "Agentic Kanban",
"description": "A shared board for people and agents.",
"inputSchema": {
"type": "object",
"properties": {
"project": { "type": "string" }
}
},
"actions": [
{
"name": "move_card",
"description": "Move one card to another column.",
"inputSchema": {
"type": "object",
"properties": {
"cardId": { "type": "string" },
"columnId": { "type": "string" }
},
"required": ["cardId", "columnId"]
}
}
]
}
The SDK removes action handler functions before it serializes the declaration. The functions remain in the provider process.
Canvas action names must not start with canvas.. GitHub reserves this prefix
for lifecycle operations.
Runtime JSON-RPC methods
Host or SDK to runtime
| Method | Purpose | Main input | Result |
|---|---|---|---|
session.canvas.list |
List available declarations. | sessionId |
Canvas declarations and provider IDs |
session.canvas.listOpen |
List live open instances. | sessionId |
openCanvases |
session.canvas.open |
Open or focus an instance. | Canvas, provider, instance, input | Open instance snapshot |
session.canvas.close |
Close an instance. | instanceId |
No result |
session.canvas.action.invoke |
Invoke an action. | Instance, action, input | Any JSON value |
Runtime to provider
| Method | Purpose | Main input | Result |
|---|---|---|---|
canvas.open |
Ask the provider to open an instance. | Session, provider, canvas, instance, input, context | URL, title, status |
canvas.close |
Notify the provider that an instance closed. | Session, provider, canvas, instance, context | No result |
canvas.action.invoke |
Route one action to the provider. | Session, provider, canvas, instance, action, input, context | Any JSON value |
The SDK routes provider callbacks by sessionId and canvasId. It routes an
action again by action name.
Main protocol shapes
Open request from a caller
{
"extensionId": "project:my-extension",
"canvasId": "agentic-kanban",
"instanceId": "board-1",
"input": { "project": "release-2026-08" }
}
extensionId is optional when canvasId is unique across providers.
Open callback to the provider
{
"sessionId": "session-id",
"extensionId": "project:my-extension",
"canvasId": "agentic-kanban",
"instanceId": "board-1",
"input": { "project": "release-2026-08" },
"host": {
"capabilities": { "canvases": true }
},
"session": {
"workingDirectory": "/path/to/worktree"
}
}
Open result from the provider
{
"url": "http://127.0.0.1:43127/",
"title": "Agentic Kanban",
"status": "ready"
}
The URL, title, and status are optional in the SDK type. A web-rendered canvas needs a URL.
Action request
{
"sessionId": "session-id",
"extensionId": "project:my-extension",
"canvasId": "agentic-kanban",
"instanceId": "board-1",
"actionName": "move_card",
"input": {
"cardId": "card-7",
"columnId": "done"
}
}
The action result can be any valid JSON value.
Errors
An extension can throw CanvasError(code, message). The Node SDK converts the
error to a JSON-RPC internal error with structured data:
{
"code": "extension_error_code",
"message": "Safe error message"
}
If an action has no handler, the SDK uses
canvas_action_no_handler. If another handler throws an ordinary error, the
SDK uses canvas_handler_error.
The SDK does not define a shared application conflict model. An extension must define its own conflict, retry, and recovery behavior.
Session events
The current SDK defines these canvas events:
| Event | Persistence | Purpose |
|---|---|---|
session.canvas.registry_changed |
Transient | Replace the available declaration list. |
session.canvas.opened |
Transient | Supply the live URL and open-instance metadata. |
session.canvas.closed |
Transient | Remove one instance from the live open list. |
session.canvas.unavailable |
Transient | Mark an open instance as disconnected. |
session.canvas.recorded |
Durable | Record an open instance for cold resume. |
session.canvas.removed |
Durable | Record that an instance closed. |
The transient opened event can contain:
instanceIdextensionIdextensionNamecanvasIdicontitlestatusurlinput
The durable recorded event contains identifiers, open input, and an optional
title. It does not contain the URL or availability state.
The iframe and application protocol
The Canvas protocol returns a URL. It does not return an HTML string, a React tree, or a fixed component description.
First-party examples serve complete HTML documents with custom CSS and JavaScript. The examples include dashboards, games, diagrams, an animation studio, a website studio, a Kanban board, and a browser control surface.
The protocol therefore permits the full interface range of the host's web renderer. The public sources do not publish a smaller canvas widget schema.
The first-party Chromium Control Canvas states that the Copilot App uses a
WebKit WKWebView. That example opens a separate Chromium window when it needs
the Chromium engine.
Common first-party web pattern
Most current first-party examples use this pattern:
Canvas page Extension process
----------- -----------------
GET / -----> Return HTML, CSS, and JavaScript
GET /events -----> Keep an SSE connection open
POST /api/<operation> -----> Apply a human operation
<----- Send state or event data through SSE
The URL and endpoint names are extension-defined. An extension can use a different protocol.
Loopback server protections in first-party examples
Several first-party examples add protections around their loopback servers:
- Bind only to
127.0.0.1. - Select an ephemeral or private port.
- Create a random capability token for each server or instance.
- Put the token in the renderer URL or generated page.
- Require the token on state and mutation endpoints.
- Pin the HTTP
Hostheader to stop DNS rebinding. - Reject cross-site mutation requests.
- Limit request body sizes and accepted fields.
- Close SSE clients and sockets during teardown.
- Redact credentials and typed text from logs.
These controls are example responsibilities. The Canvas SDK does not define them as a standard application protocol.
State and persistence
Copilot does not require one canvas state model. The extension selects it.
The third-party Multi-Agent Dev example stores state in a process map. It deletes that state when the instance closes.
The first-party Daily Focus Board stores domain state in a JSON file. Human HTTP operations and agent actions both call the same mutation functions.
The first-party Chat Cards extension retains deck state after panel close. It stops only the page server and SSE connections, then saves state to disk.
The first-party Site Studio saves its dashboard state as an artifact. It restarts a per-instance server when the canvas opens.
These examples show three independent lifetimes:
- The canvas panel can open and close.
- The extension process can connect, reload, and disconnect.
- The application state can be ephemeral or durable.
The Canvas protocol coordinates the first two lifetimes. The extension owns the third lifetime.
Detailed public sample flow
The leestott/copilot-canvas-runtime sample uses one extension.mjs file.
This sample gives a small end-to-end example of the two protocol layers.
Provider setup
- The extension creates a process-level map of instance state.
- The extension defines seven agent actions with JSON Schemas.
- The extension registers one canvas with
createCanvas. - The extension joins the Copilot session with
joinSession.
Open
- Copilot calls the extension's
openhandler. - The handler uses
ctx.instanceIdas the map key. - The handler starts an HTTP server on
127.0.0.1and an ephemeral port. - The handler returns the loopback URL and title.
- The canvas panel loads the URL.
Web endpoints
| Endpoint | Purpose |
|---|---|
GET / |
Return the full HTML document. |
GET /events |
Open an SSE stream and send the current state. |
POST /trigger |
Apply one human control action. |
GET /state |
Return the current instance state as JSON. |
Shared state flow
Human controls call POST /trigger. Agent calls enter through declared Canvas
actions. Both paths change the same object in the instance-state map.
After a change, the extension writes the full state to each SSE client. The canvas page parses the event and redraws its HTML.
Close
The onClose handler closes SSE responses and the loopback server. It then
deletes the instance state.
This cleanup behavior belongs to the sample. It is not a Copilot requirement.
Sharing and collaboration
Copilot documentation uses "shared" for the person-agent work surface and for project extension definitions.
Project scope shares extension code through the repository. User scope keeps the extension code local.
The supplied Canvas documentation does not define concurrent multi-user editing of one live instance. It also does not define roles, invitations, or presence.
Local Copilot sessions can be shared separately with view-only access. Session sharing does not define canvas editing rights.
Publicly unknown behavior
The reviewed public sources do not define these host details:
- the iframe
sandboxattribute - the renderer Content Security Policy
- URL scheme or origin restrictions
- credential and cookie behavior inside the renderer
- permission prompts for individual canvas actions
- network policy for extension processes
- filesystem isolation for extension processes
- remote canvas rendering in cloud sessions
- concurrent access by several people
- standard revisions, conflicts, or transactions for application state
- a standard database or file format for canvas state
- mobile renderer support
These items need direct product testing or additional GitHub documentation. They cannot be inferred from the SDK types alone.
Confirmed conclusions
- The agent authors a canvas extension from a human prompt.
- A prebuilt plugin can also contribute canvases.
- A canvas declaration contains metadata, open input, and agent actions.
- Each open canvas has a stable
instanceId. - The provider returns a renderer URL instead of HTML through JSON-RPC.
- Current examples serve arbitrary HTML, CSS, and JavaScript at that URL.
- Human and agent paths can operate the same extension-owned state.
- GitHub standardizes lifecycle and action dispatch, not the iframe protocol.
- Application persistence is separate from panel resume metadata.
- The protocol remains experimental.
Sources
GitHub product documentation
- Working with canvas extensions in the GitHub Copilot app
- About the GitHub Copilot app
- Customizing the GitHub Copilot app
- Managing agent sessions
Canonical Copilot SDK
- Node Canvas API
- Generated JSON-RPC types and methods
- Generated Canvas session events
- Node Canvas end-to-end tests
- Canvas extensibility pull request
- Canvas SDK parity and stability issue
First-party examples
- GitHub Awesome Copilot canvas extensions
- Daily Focus Board
- Chat Cards
- Diagram Viewer
- Site Studio
- Chromium Control Canvas
- Color Orb