Build a Canvas
Create an interactive state machine with poses, transitions, and conditions. A canvas defines how your mascot performs resolved app and user behavior.
What is a Canvas?
A canvas is a state machine where each state is a mascot pose (an image or animation) and transitions are animated videos that play when moving between states. You define conditions and inputs that trigger transitions, usually resolved inputs such as behavior::interact, behavior::attention, or node::nodeTime. The Masko embed player reads the canvas configuration and handles playback.
Nodes & Edges
Nodes represent poses or states. Each node has a stable id, an image assetId, and a position on the canvas editor grid. Use assetId from a completed pose in this mascot; example UUIDs below must be replaced with your own. The itemName is the human-readable label.
Edges are transitions between nodes. Each edge connects a source node to a target node and can have:
- conditions - Rules that must be met to trigger the transition (see below).
- priority - When multiple edges can fire, the highest priority wins. Higher number = higher priority.
- speed - Playback speed multiplier (e.g. 1.5 for faster, 0.5 for slower). Default is 1.
- duration - Video duration in seconds. Used for generation.
- description - Animation prompt used when generating the transition video.
- reverse - If true, this edge plays the reverse of another edge's video instead of generating a new one.
Use source: "*" (any state) for edges that can fire from any node - useful for global triggers like an error state or reset.
Conditions & Inputs
Each edge can have an array of conditions. A condition has three fields:
- input - The name of the input to check (e.g.
"behavior::interact","node::nodeTime"). - op - The comparison operator:
"==","!=",">","<",">=","<=". - value - The value to compare against.
The canvas supports these input types:
- boolean - True/false values. Example:
behavior::interact,behavior::working. - number - Numeric values. Example:
node::nodeTime,node::loopCount. - trigger - Fire-once events. New desktop app-control graphs should normally use resolved behavior/action inputs instead.
Create a Canvas
First create an empty project canvas with POST /v1/canvases, using project_id, mascot_id and name as shown in the canvas guide. Save its returned ID and graph hash, then PATCH the graph below. The graph contains nodes, edges, viewport settings and input declarations.
curl -X PATCH https://api.masko.ai/v1/canvases/CANVAS_ID \
-H "Authorization: Bearer masko_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"expected_graph_hash": "GRAPH_HASH_FROM_CREATE",
"graph": {
"nodes": [
{
"id": "idle",
"x": 0,
"y": 0,
"itemName": "Idle",
"assetId": "6aec5b1a-cb7f-5f10-8d6e-235ec16b4d97"
},
{
"id": "waving",
"x": 300,
"y": 0,
"itemName": "Waving",
"assetId": "18d4fd5e-7193-57f1-8af9-f3bdff007783"
}
],
"edges": [
{
"id": "edge_001",
"source": "idle",
"target": "waving",
"duration": 4,
"description": "transitioning from idle to waving",
"conditions": [
{
"input": "behavior::attention",
"op": "==",
"value": true
}
]
},
{
"id": "edge_002",
"source": "waving",
"target": "idle",
"duration": 4,
"description": "returning from wave to idle",
"conditions": [
{
"input": "behavior::rest",
"op": "==",
"value": true
}
],
"reverse": true,
"reverseOfEdgeId": "edge_001"
}
],
"viewport": {
"x": 0,
"y": 0,
"zoom": 0.8
},
"inputs": [
{
"name": "behavior::attention",
"type": "boolean",
"default": false
},
{
"name": "behavior::rest",
"type": "boolean",
"default": true
}
],
"initialNode": "idle"
}
}'Update the Graph
Fetch the canvas first, update the complete graph locally, and send it via PATCH with the returned graph_content_hash as expected_graph_hash. The entire authored graph (nodes, edges, inputs, viewport) is replaced. If the hash is stale, Masko returns 409 and leaves the newer graph untouched.
Delete a Canvas
Delete a canvas. This removes the graph definition; the underlying items and their generated assets stay in the mascot.
Example: Sleep/Wake Canvas
A complete two-state canvas where the mascot sleeps by default and wakes up
when the desktop runtime resolves direct interaction as behavior::interact.
curl -X PATCH https://api.masko.ai/v1/canvases/CANVAS_ID \
-H "Authorization: Bearer masko_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Sleep Wake",
"graph": {
"nodes": [
{
"id": "sleeping",
"x": 0,
"y": 0,
"itemName": "Sleeping",
"assetId": "d81e7218-4d80-548c-bcfc-968ccf1d5d80"
},
{
"id": "awake",
"x": 400,
"y": 0,
"itemName": "Awake",
"assetId": "cf03e485-3d80-54a1-8e5f-4d038116c25c"
}
],
"edges": [
{
"id": "edge_wake",
"source": "sleeping",
"target": "awake",
"duration": 4,
"description": "waking up with a stretch and yawn",
"conditions": [
{
"input": "behavior::interact",
"op": "==",
"value": true
}
],
"priority": 10
},
{
"id": "edge_sleep",
"source": "awake",
"target": "sleeping",
"duration": 4,
"description": "slowly falling asleep, eyes drooping",
"conditions": [
{
"input": "behavior::rest",
"op": "==",
"value": true
}
],
"priority": 10
}
],
"viewport": {
"x": 0,
"y": 0,
"zoom": 0.8
},
"inputs": [
{
"name": "behavior::interact",
"type": "boolean",
"default": false
},
{
"name": "behavior::rest",
"type": "boolean",
"default": true
}
],
"initialNode": "sleeping"
}
}'Validate before generating
curl https://api.masko.ai/v1/canvases/CANVAS_ID/validate \
-H "Authorization: Bearer $MASKO_API_KEY"The response has data.valid, data.issues, and data.summary.errors / warnings. Each issue includes a code, message, severity, and optional node or edge ID. HTTP 200 means the check ran, not that the graph is valid. Fix structural errors before requesting paid generation. A draft without generated clips will also report missing-video; resolve those through generation, then validate again before publishing.
Validation checks the stored graph. Use media readiness separately to determine whether the generated character can play.