Artilier
Features Pricing FAQ Skins
Get Artilier

Creating Artilier skins

Last updated: 2026-06-11

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.

What v2 brings. v2 lets you ship a skin with no 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.
Skins are user-installed third-party content. Artilier doesn't host, review, distribute, or endorse the skins you author or install. They live only on the user's own machine, in %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.

Where skins live

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.

The fastest start: scaffold one

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.


Tier 1 — Just a manifest and clips (no code)

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.

  • When the agent is Working, work.webm plays.
  • For every other status, the "*" catch-all plays idle.webm.
  • If a status matches neither a specific key nor "*", the card simply shows its status border.

The states block

states maps a status to an animation. The seven session statuses, in lifecycle order:

StatusMeaning
StartingProcess is up; the first SessionStart hook event hasn't arrived yet (a brief transient).
RunningProcess is up and idle — the transient between OS-process-up and the first hook event.
WorkingClaude is actively producing output.
WaitingClaude is idle at the prompt.
WaitingForPermissionClaude has paused to request tool-use permission.
StoppedThe session is not running.
ErroredThe 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:

FieldTypeDefaultNotes
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.

Customising the overlay

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.


Tier 2 — Your own layout with data bindings (still no JavaScript)

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>
AttributeWhat 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 nameResolves to
nameSession display name.
projectworkingDirectoryDisplay — typically the last two path segments.
branchCurrent git branch, or null.
worktreeActive worktree path, or null.
contextObject: { percent (0–1), isStale, label }.
permissionModeObject: { glyph, text, severity }.
modelSelected 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.

Stage element and status-driven CSS

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); }

Tier 3 — Add a little code

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).


Tweaks: user-adjustable settings

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" }
]

Manifest reference

FieldRequiredNotes
schemayes"agentboard-skin/2".
idyesLowercase kebab-case; must match the folder name.
nameyesDisplay name.
versionyesMAJOR.MINOR.PATCH.
sizeyes{ "width", "height" }, each 50–500 px. This is the card's actual rendered footprint on the board canvas; the board doesn't scale it.
statesfor code-free skinsPer-status animations (see above). Optional if you ship a custom index.html.
overlaynoOverlay fields and position.
tweaksnoUser-adjustable settings.

Tips

  • Keep clips short and loopable; long videos use more memory per card.
  • Test every status in the preview before sideloading — especially Errored and the "*" fallback.
  • If your skin doesn't appear after Refresh, check the Failing skins list in the style editor; it names the exact JSON path and reason (e.g. states.Working.animations[1] file missing).
  • Sharing. There's no marketplace yet — zip the skin folder, share it, and the recipient unzips into %APPDATA%\Artilier\skins\ and clicks Refresh.
Legal notice Privacy Terms Cancel Credits Contact © 2026 Comitatus Software AG