Artilier paints each session card with a skin. A skin can show a different
animation for each state your agent is in — working, waiting for permission,
errored — with the session's name, project, branch and context rendered on top.
For the common case you write no HTML, CSS, or JavaScript — just a
manifest.json and your clips.
This guide covers the current skin format, agentboard-skin/2.
index.html and no
JavaScript — just a manifest.json, a states map, and your
clips. The host generates the page, injects the window.AgentBoard runtime,
and renders an overlay over your animation. v2 also adds the states map
(per-status animations with cycle modes), drops the v1 renders /
fallback fields, and supports .webm / .mp4 /
.html clips. Existing agentboard-skin/1 skins keep working
byte-for-byte; use v2 for anything new.
%APPDATA%\Artilier\skins\, placed there by the user. The
skin author is responsible for what the skin renders; users install third-party skins
at their own risk. Artilier provides the rendering surface — the content of any skin
is the author's own.
Artilier is Windows-only today (macOS and Linux builds are on the roadmap). Drop a skin folder into your Artilier skins directory:
%APPDATA%\Artilier\skins\<your-skin-id>\ Open the style editor in Artilier and click Refresh to pick it up. If a skin has a problem, it appears in the Failing skins list with the exact field and reason — fix it and refresh again.
A scaffold writes a complete, working skin you can preview immediately. The scaffold and
preview harness live in the AgentVisualizer source tree under
Source/src/AgentBoard.Skins.Web/ — they are not bundled with the
installer today, so you need a checkout of the source repo to run them. You'll also need
Node.js on your machine.
cd Source/src/AgentBoard.Skins.Web
node tools/new-skin.js my-anim
This creates my-anim/ with a manifest.json, an
idle.html placeholder animation, a copy of the SDK runtime
(agentboard-sdk.js), and a browser preview. Serve the folder and open the
preview:
cd my-anim
npx serve
# open preview.html in your browser The preview lets you step through every session status and tweak values with mock data — using the same runtime the app uses, so what you see in the browser is what you get in Artilier.
A complete skin can be nothing but a manifest and the animation files it names:
my-anim/
manifest.json
work.webm
idle.webm {
"schema": "agentboard-skin/2",
"id": "my-anim",
"name": "My Anim",
"version": "1.0.0",
"size": { "width": 320, "height": 200 },
"states": {
"Working": { "animations": ["work.webm"] },
"*": { "animations": ["idle.webm"] }
}
} Artilier generates the page for you: your animation fills the card, and an overlay shows the session's name, project, branch and context bar.
work.webm plays."*" catch-all plays idle.webm."*", the card simply shows its status border.states block states maps a status to an animation. The seven session statuses, in
lifecycle order:
| Status | Meaning |
|---|---|
Starting | Process is up; the first SessionStart hook event hasn't arrived yet (a brief transient). |
Running | Process is up and idle — the transient between OS-process-up and the first hook event. |
Working | Claude is actively producing output. |
Waiting | Claude is idle at the prompt. |
WaitingForPermission | Claude has paused to request tool-use permission. |
Stopped | The session is not running. |
Errored | The session is in a failure state. |
* | Catch-all — matched when no exact key matches. |
Status keys are validated against this set. A typo like "working" (lowercase)
or an unknown name like "Thinking" causes the whole skin to be rejected with
the offending JSON path surfaced in Failing skins — not silently ignored.
Each entry takes the following fields:
| Field | Type | Default | Notes |
|---|---|---|---|
animations | array (at least one) | — | Filenames, or objects { "file", "duration" }. |
cycle | "sequence" / "timed" / "per-entry" | "sequence" | How multiple clips advance. |
interval | number, milliseconds | 5000 | Used only by "timed". |
random | boolean | false | Pick the next clip at random. |
Clip formats: .webm and .mp4 (video), or
.html (a small HTML/CSS animation). Every file you reference must exist
in the folder — the validator checks file existence and refuses skins with missing clips.
HTML clips at a glance. An HTML clip is a self-contained HTML document. The
runtime fetches it and injects it into the stage as innerHTML, so
top-level <style> blocks apply and CSS animations run — but
<script> tags inside the clip do not execute (innerHTML never
runs scripts). For logic, use Tier 3 hooks in the parent page.
Asset references inside the clip (e.g. background-image: url('bg.png')) and
inside your index.html resolve relative to the skin folder. A minimal HTML
clip:
<!doctype html>
<html><head><meta charset="utf-8"><style>
html, body { margin: 0; height: 100%; overflow: hidden; }
.bg {
position: absolute; inset: 0;
background: linear-gradient(120deg, #1e2030, #3a2a4d, #1e2030);
background-size: 200% 200%;
animation: drift 6s ease-in-out infinite;
}
@keyframes drift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
</style></head><body><div class="bg"></div></body></html> Multiple clips per state. List more than one and choose how they cycle:
"Working": {
"animations": ["work-a.webm", "work-b.webm"],
"cycle": "sequence"
} sequence (default) — play each clip once, then move to the next,
looping the list. Videos advance when they finish. HTML clips advance after their
duration (give an HTML clip a duration or it loops forever):
"animations": [{ "file": "intro.html", "duration": 1500 }, "loop.webm"] timed — switch clips every interval milliseconds; each
clip loops in between.
per-entry — keep one clip looping the whole time in this status;
show the next one only the next time the agent re-enters the status. Useful for
"celebration on first enter, then steady loop" effects.
A state with a single clip just loops it. Broken or missing clips are skipped automatically — a bad file never breaks the card.
Leave overlay out and you get the default strip (name, project, branch,
context). To control it:
"overlay": { "fields": ["name", "context"], "position": "top" } fields — any of name, project, branch,
worktree, context, permissionMode, model.
position — top, bottom, or none
(no overlay).
Overlay colours follow the app theme automatically, and users get a colour picker for the overlay text in the style editor — no work on your part.
Want full control of the markup but not the plumbing? Add an index.html and
tag elements with binding attributes. Artilier fills them in live:
<!doctype html>
<html>
<head><style>/* your styles */</style></head>
<body>
<div data-stage></div>
<h1 data-bind="name"></h1>
<p data-bind="project"></p>
<p data-bind-hidden="!branch" data-bind="branch"></p>
</body>
</html> | Attribute | What it does |
|---|---|
data-bind | Sets the element's text from a value. |
data-bind-hidden |
Hides the element when a value is empty. Prefix with ! to invert
(!branch → hide when there's no branch).
|
data-bind-attr | Sets an attribute: data-bind-attr="title:project". Comma-separate
multiple pairs. |
data-bind-style | Sets a CSS variable: data-bind-style="--pct:context.percent" — then
consume it in CSS, e.g. width: calc(var(--pct, 0) * 100%); on a
progress-bar fill. |
Values you can bind. Session fields by friendly name (the alias table):
| Friendly name | Resolves to |
|---|---|
name | Session display name. |
project | workingDirectoryDisplay — typically the last two path segments. |
branch | Current git branch, or null. |
worktree | Active worktree path, or null. |
context | Object: { percent (0–1), isStale, label }. |
permissionMode | Object: { glyph, text, severity }. |
model | Selected model (e.g. "sonnet" or "claude-sonnet-4-6"), or null. |
Use sub-paths for nested values: context.percent,
permissionMode.text. Tweaks and theme bind by raw dotted path:
tweaks.<your-key>, theme.foreground,
theme.statuses.Working. The raw contract is also reachable via
session.<raw-field> for the handful of fields outside the alias set
(id, colorHue, statusChangedAt,
justCleared, notifyOnFinish, slotChord,
effort).
The theme object: isDark, mode, background,
foreground, subtle, accent, critical,
and statuses — a map of each status name to a 6-digit hex colour.
Include a <div data-stage> and your states animations play
inside it, exactly as in Tier 1. The runtime also keeps body[data-status]
and body[data-density] in sync (style off them in CSS) and exposes the
current status colour as the CSS custom property --ab-status:
/* Tint a border by the live status, no JS needed: */
body { border: 2px solid var(--ab-status); }
/* Behave differently per status: */
body[data-status="WaitingForPermission"] .accent { animation: pulse 1s infinite; }
body[data-status="Errored"] .stage { filter: saturate(0); } When you need logic, use the runtime hooks. Everything else still happens for you.
<script>
AgentBoard.onRender(function ({ session, theme, tweaks }) {
// Runs after bindings are applied, on every update.
document.body.classList.toggle('busy', session.status === 'Working');
});
AgentBoard.onPulse(function () {
// Fires when a session finishes (the "finish pulse" cue).
});
</script>
Handy helpers under AgentBoard.helpers:
statusColor(status),
hexToRgba(hex, alpha),
lastTwoSegments(path),
formatPercent(value),
setText(el, text),
setHidden(el, hidden).
Declare tweaks in your manifest and Artilier renders controls for them in the
style editor; the chosen values arrive as tweaks.<key> in your bindings
and hooks.
"tweaks": [
{ "type": "toggle", "key": "show-branch", "label": "Show branch", "default": true },
{ "type": "color", "key": "accent", "label": "Accent", "default": "#D97757" },
{ "type": "slider", "key": "speed", "label": "Speed", "min": 0.5, "max": 2, "step": 0.1, "default": 1 },
{ "type": "segment", "key": "density", "label": "Density", "options": ["compact","regular","comfy"], "default": "regular" }
] | Field | Required | Notes |
|---|---|---|
schema | yes | "agentboard-skin/2". |
id | yes | Lowercase kebab-case; must match the folder name. |
name | yes | Display name. |
version | yes | MAJOR.MINOR.PATCH. |
size | yes | { "width", "height" }, each 50–500 px. This is the card's actual rendered footprint on the board canvas; the board doesn't scale it. |
states | for code-free skins | Per-status animations (see above). Optional if you ship a custom index.html. |
overlay | no | Overlay fields and position. |
tweaks | no | User-adjustable settings. |
Errored
and the "*" fallback.
states.Working.animations[1] file missing).
%APPDATA%\Artilier\skins\ and clicks Refresh.