A canvas is a small state machine that lives in the mascot's bundle. Each state plays an animation, inputs are values your app sets (booleans, numbers, one-shot triggers), and transitions move between states when their conditions hold, blending poses so nothing snaps. Your code never picks animations directly; it just reports what is happening.
lumorig companion
idle$ claude "fix the flaky auth test"
The ai_agent template, running live. Press a button: the demo sets status 1, 2, 3 or 4 as a build progresses.
How canvases work#
- The canvas starts in its
initialstate. - Whenever an input changes, transitions are checked in order; the first one whose
frommatches the current state (or is*) and whose conditions all hold wins. A transition into the state you are already in is skipped unless it is a trigger. - Triggers are momentary:
fire("greet")is true for that one evaluation, then clears. - A non-looping state plays its clip once, then moves to its
thenstate with a 0.2 s blend. - Each transition's
blend(0–2 seconds) cross-fades from the pose the mascot was in, so interruptions look intentional.
Templates#
Three ready-made graphs cover the common cases. They only use presets, so they work on every mascot without any custom motion. List them with GET/canvas-templates (no key needed).
ai_agent: AI coding agent#
Idle, working, waiting for you, done, failed. Wire it to Claude Code hooks or any agent loop.
| Input | Type | Default |
|---|---|---|
status | number | 0 |
| State | Plays | Then |
|---|---|---|
idle (initial) | idle | loops |
working | working | loops |
waiting | wave | loops |
done | celebrate | once, then idle |
failed | sad | once, then idle |
Transitions: idle when status = 0 · working when status = 1 · waiting when status = 2 · done when status = 3 · failed when status = 4.
chatbot: Chat assistant#
Idle, listening while the user types, thinking, talking while the reply streams.
| Input | Type | Default |
|---|---|---|
listening | boolean | false |
thinking | boolean | false |
talking | boolean | false |
greet | trigger | — |
| State | Plays | Then |
|---|---|---|
idle (initial) | idle | loops |
listening | listen | loops |
thinking | think | loops |
talking | talk | loops |
greeting | wave | once, then idle |
Transitions: greeting when greet fired · talking when talking is true · thinking when thinking is true and talking is false · listening when listening is true and thinking is false and talking is false · idle when listening is false and thinking is false and talking is false.
onboarding: Onboarding guide#
Waves hello, idles alongside the user, celebrates each completed step, and looks sad on errors.
| Input | Type | Default |
|---|---|---|
hello | trigger | — |
success | trigger | — |
error | trigger | — |
| State | Plays | Then |
|---|---|---|
idle | idle | loops |
hello (initial) | wave | once, then idle |
success | celebrate | once, then idle |
error | sad | once, then idle |
Transitions: hello when hello fired · success when success fired · error when error fired.
Create a canvas#
POST/mascots/{id}/canvases
| Name | Type | Description |
|---|---|---|
namerequired | string | 1–60 characters. This is the name you pass to the player's canvas attribute. |
template | string | ai_agent, chatbot or onboarding. |
graph | CanvasGraph | A custom graph (see Graph format). If you send both, the graph wins. |
One of template or graph is required. Canvases are free: no model call, no credits. The response is the canvas with its full graph.
curl -X POST https://lumorig.com/api/v1/mascots/msc_…/canvases -H "Authorization: Bearer $LUMORIG_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "assistant", "template": "chatbot" }'
# 201 → { "id": "cnv_…", "object": "canvas", "name": "assistant", "template": "chatbot", "graph": { … } }Updating and deleting#
| Endpoint | Does |
|---|---|
GET/mascots/{id}/canvases | Every canvas on the mascot. |
PATCH/mascots/{id}/canvases/{canvasId} | Rename (name) or replace the graph. The new graph is validated. |
DELETE/mascots/{id}/canvases/{canvasId} | Remove it from the mascot and its bundle. |
Drive it from your app#
Every canvas ships inside the mascot's bundle, keyed by name. Set the canvas attribute on the player, then call setInput and fire as things happen. Listen for statechange if your UI wants to follow along.
const pip = document.querySelector("lumorig-player");
pip.setAttribute("canvas", "assistant");
pip.fire("greet"); // waves once, back to idle
input.addEventListener("focus", () => pip.setInput("listening", true));
chat.on("request", () => pip.setInput("thinking", true));
chat.on("token", () => pip.setInput("talking", true));
chat.on("done", () => {
pip.setInput("thinking", false);
pip.setInput("talking", false);
pip.setInput("listening", false);
});
pip.addEventListener("statechange", (e) => console.log(e.detail.state));<lumorig-player src="…/bundle" canvas="assistant"></lumorig-player>pip.canvas = "assistant" and pip.setAttribute("canvas", "assistant") are equivalent; the property mirrors the attribute.
AI agents and the terminal#
The ai_agent template takes a single number, status: 0 idle, 1 working, 2 waiting for you, 3 done, 4 failed. That is exactly what the terminal companion sets from Claude Code hooks, and you can set it from any agent loop the same way.
Graph format#
A CanvasGraph is plain JSON. Names (inputs and state ids) are snake_case. This is the onboarding template in full:
{
"canvasVersion": 1,
"inputs": [
{
"name": "hello",
"type": "trigger",
"default": null
},
{
"name": "success",
"type": "trigger",
"default": null
},
{
"name": "error",
"type": "trigger",
"default": null
}
],
"states": [
{
"id": "idle",
"animation": "idle",
"loop": true,
"then": null
},
{
"id": "hello",
"animation": "wave",
"loop": false,
"then": "idle"
},
{
"id": "success",
"animation": "celebrate",
"loop": false,
"then": "idle"
},
{
"id": "error",
"animation": "sad",
"loop": false,
"then": "idle"
}
],
"initial": "hello",
"transitions": [
{
"from": "*",
"to": "hello",
"when": [
{
"input": "hello",
"op": "fired",
"value": null
}
],
"blend": 0.2
},
{
"from": "*",
"to": "success",
"when": [
{
"input": "success",
"op": "fired",
"value": null
}
],
"blend": 0.15
},
{
"from": "*",
"to": "error",
"when": [
{
"input": "error",
"op": "fired",
"value": null
}
],
"blend": 0.2
}
]
}| Name | Type | Description |
|---|---|---|
canvasVersionrequired | 1 | Format version. |
inputs[]required | object[] | name, type (boolean · number · trigger) and default (null for triggers). |
states[]required | object[] | At least one. id, animation (a preset id or a saved animation's name), loop, and then (where a non-looping state goes next, else null). |
initialrequired | string | The state id to start in. |
transitions[]required | object[] | from (a state id or *), to, when (one or more conditions, all must hold) and blend (seconds, 0–2). |
Conditions#
| op | Input type | Holds when |
|---|---|---|
is_true | boolean | The input is true. |
is_false | boolean | The input is false (or unset). |
eq · gt · lt | number | The input equals, exceeds or is below value. |
fired | trigger | The trigger was just fired. |
Validation rules#
Graphs are checked when you create or update a canvas. Problems come back as 400 invalid_request with one line per problem in error.details:
initialmust be one of the states.- Every state's animation must exist on the mascot: a preset id, or the id or name of a saved animation.
- A non-looping state needs
then, andthenmust be a real state. - Every transition's
from(unless*) andtomust be real states, and every condition must name a declared input. firedneeds a trigger input;eq,gtandltneed a number input and a non-nullvalue.