Skills
Agent skills for Claude Code that teach an AI agent to run Particles UI workflows correctly. Copy a skill's markdown into your project and your agent gains the knowledge automatically.
To install: copy the markdown below into .claude/skills/<name>/SKILL.md at your project root, then ask your agent to perform the task.
push-tokens
Push a local design-token file into Particles Studio with the CLI so tokens land with the correct type, tier, Figma scopes, and a usage description, per file format (DTCG/JSON, Tokens Studio, Style Dictionary, Particles JSON, CSS, SCSS, JS/TS). Use when asked to push/import/upload/migrate/bootstrap tokens into a Particles project or branch, or to sync a token file up to Studio.
.claude/skills/push-tokens/SKILL.md
---
name: push-tokens
description: Push a local design-token file into Particles Studio with the CLI so tokens land with the correct type, tier, Figma scopes, and a usage description, per file format (DTCG/JSON, Tokens Studio, Style Dictionary, Particles JSON, CSS, SCSS, JS/TS). Use when asked to push/import/upload/migrate/bootstrap tokens into a Particles project or branch, or to sync a token file up to Studio.
---
# Push tokens to Particles Studio
The `particles token-studio push` command imports a local token file into a project branch.
The command infers the format **from the file extension** and the backend normaliser then maps it
to Particles tokens. Getting the push "correct" means each token lands with the right **type**,
**tier**, **scopes**, and a **usage description** — so choose the format for the fidelity you need,
name and describe tokens well, and validate *before* you push, because the CLI dry-run reports
**counts only**, not per-token results, so several silent failures are invisible in its output.
## The one-line command
```bash
particles token-studio push --file <path> [--branch <name>] [--tier <tier>] [--dry-run]
```
- `--branch` defaults to `main`. It does **NOT** fall back to `particles-ui.json`'s `branch`. If you
want a non-main branch you must pass `--branch` explicitly (and the branch must already exist —
create it with `particles token-studio branch create <name>`).
- `--tier` is `primitive | semantic | composition`, default `primitive`. It is only a **fallback** for
tokens whose format doesn't carry a tier (see the fidelity table).
- Import is **additive-only**: tokens are added, overwritten (value change), or skipped (unchanged).
It **never deletes** tokens that aren't in the file.
- **Type, tier, groupPath, and scopes are set ONLY when a token is first inserted.** An overwrite
updates `valueRaw` *only* — re-pushing the same token name will **not** correct a wrong type, tier,
or scope. To fix those you must edit the token in Studio (or delete it and re-add). Get type +
naming right *before* the first push.
## Workflow (follow in order)
1. **Check prerequisites.**
```bash
particles auth status # must be logged in
```
- There must be a `particles-ui.json` with a `projectId` in the cwd (or an ancestor dir), or pass
`--config <path>`. If missing, run `particles init` first.
- The account needs the `tokens:write` permission on the project (Admin, or Designer/Developer
with write access).
2. **Pick the right format for the source data.** See **Format → fidelity** below. The extension you
give `--file` decides how it's parsed — rename or convert the file if its extension doesn't match
its real format.
3. **Pre-validate types AND naming (critical — the CLI will not warn you, and you can't fix it later).**
- **Type:** every token's declared type runs through `normaliseTokenType`, which **silently coerces
any unrecognised type to `color`**. Read the file and confirm every `$type` / `type` is in the
supported set (or a known alias) below. A wrong type also produces wrong **scopes** (scopes are
derived from type), so this one mistake corrupts two fields at once.
- **Scopes follow from type + name.** Particles auto-infers each token's Figma `scopes` from its
**type** plus keywords in its **name/groupPath** — the file never carries scopes. For `color` and
`spacing` tokens the *name* decides the scope, so name them with the right keyword (see
**Types & scopes** below). Tokens whose names lack a keyword fall back to a broad default
(`ALL_FILLS` / `GAP`), which is usually not what you want for borders, text, icons, or sizes.
- **Describe every token.** Give every token a `$description` (DTCG / Tokens Studio) or
`description` (Particles JSON) saying **what it's for / when to use it** — not what its value is.
Good: `"Primary action button background; use for the main CTA on a page."` Bad: `"Blue 500."`
The description is imported and shown in Studio and the Figma plugin to guide consumers. Like
type and scopes it is set **only on first insert** — an overwrite won't add or change it — and
**CSS / SCSS / JS / TS / CSV carry no descriptions at all** (see **Descriptions** below).
4. **Dry-run and read the counts.**
```bash
particles token-studio push --file ./tokens.json --dry-run
```
Output is **counts only**:
```
Added: 12
Overwritten: 3
Skipped: 1
Skipped (non-primitive): 4 # only printed when > 0
```
What the counts catch / miss:
- **Caught:** fewer `Added` than tokens in the file (silently dropped); `Skipped (non-primitive) > 0`
(the project is a Foundation/Brand "primitives-only" project and dropped your semantic/composition
tokens); unexpected `Overwritten` (you're clobbering existing values).
- **NOT caught:** type coercion to `color`, tier collapse, composition flattening. These never show
up in counts — that's why step 3 matters.
5. **Apply** once the dry-run counts match your expectation (drop `--dry-run`):
```bash
particles token-studio push --file ./tokens.json --branch main
```
6. **Verify** what actually landed (counts can't confirm types/tiers/scopes). Round-trip export and
diff:
```bash
particles token-studio export --format json --branch main --output /tmp/after.json
```
The `json` format includes `type` and `tier` per token — compare them against your source.
**Scopes are not included in any export format** — confirm those in Particles Studio
(Project → Tokens → token detail) if scope correctness is critical.
## Format → fidelity
| Extension | Detected as | Tier carried? | Composition? | Notes |
|---|---|---|---|---|
| `.json` (array w/ `valueRaw`) | **Particles JSON** | ✅ per-token `tier` | ✅ (string value) | Native export. Best for full-fidelity round-trips. Foreign `parentId`s are silently dropped (IDOR guard). |
| `.json` (`$value`+`$type`, has `$schema`) | **W3C / DTCG** | ❌ → all get `--tier` | ✅ object `$value` preserved | Best interoperable format; preserves composition. |
| `.json` (`$value`/`value`, `$metadata`/`$values`, or bare nested) | **Tokens Studio** | ❌ → all get `--tier` | partial | Default fallback for unrecognised JSON objects. |
| `.json` (Style Dictionary nested `{value,type}`) | via Tokens Studio walk | ❌ → all get `--tier` | partial | `value` (no `$`) is read. |
| `.css` | **CSS custom properties** | ✅ inferred | ✅ `.class{}` / `@utility` blocks | `:root{}` / `@theme{}` vars → primitive; `var(--…)` refs or `semantic` in the name → semantic. |
| `.scss` | **SCSS variables** | ✅ inferred | ✅ | `$name: value;` lines reuse the CSS path; the `$tokens:()` map is skipped. |
| `.js` / `.ts` | **JS/TS theme object** | ❌ → all get `--tier` | ❌ **flattened to leaf tokens (lossy)** | Must `export const theme = { … }`. Evaluated in a 5s VM sandbox; `as const` / `export type` stripped. |
### Choosing correctly
- **Mixed primitive + semantic + composition design system?** It **cannot** round-trip through a single
DTCG / Tokens-Studio / JS / TS push — those formats carry no tier, so every token collapses to the one
`--tier` value. Use one of:
- **Particles JSON** (per-token `tier`), or
- **CSS / SCSS** (tier inferred from `var()` references and `semantic` naming), or
- **Split into separate pushes**, one per tier: `--tier primitive`, then `--tier semantic`, etc.
- **Composition tokens matter?** Use **DTCG** (object `$value` preserved) or **CSS** (`.class{}` /
`@utility` blocks). Avoid JS/TS — composition objects flatten to individual leaf tokens.
- **Just bootstrapping primitives?** Any format works with the default `--tier primitive`.
## Supported token types (for step 3 pre-validation)
Canonical types (used as-is, case-insensitive):
```
color, typography, spacing, radii, shadow, elevation, opacity, border,
duration, motion, grid, lineHeight, letterSpacing, fontWeight, breakpoint, zIndex
```
Recognised aliases (auto-mapped):
```
text|fontSizes|fontWeights|fontFamilies|lineHeights|letterSpacing|paragraphSpacing → typography
sizing → spacing borderRadius → radii borderWidth|borders → border
boxShadow|shadows → shadow time|animation → duration
```
**Anything not in either list is silently coerced to `color`.** Flag such types to the user and fix the
source before pushing.
## Types & scopes
Scopes are the Figma `VariableScope`s a token is allowed to bind to (e.g. only fills, only corner
radius). Particles **infers them automatically at import** from the token's `type` plus keywords in
its full name (`groupPath.name`). The file never supplies scopes — so the only levers you have are
**getting the type right** and **naming tokens with the right keyword**.
**`color` — scope is chosen by the first matching name keyword:**
| Name contains (case-insensitive) | Inferred scopes |
|---|---|
| `background`, `bg`, `surface`, `backdrop`, `canvas`, `fill` | `FRAME_FILL`, `SHAPE_FILL` |
| `text`, `foreground`, `fg`, `label`, `caption`, `content`, `on-`, `oncolor` | `TEXT_FILL` |
| `border`, `stroke`, `outline`, `ring`, `divider`, `separator`, `line` | `STROKE_COLOR` |
| `shadow`, `effect`, `scrim`, `overlay` | `EFFECT_COLOR` |
| `icon`, `glyph`, `symbol` | `SHAPE_FILL` |
| _(no keyword matched)_ | `ALL_FILLS` (default) |
**`spacing` — scope is chosen by the first matching name keyword:**
| Name contains | Inferred scopes |
|---|---|
| `gap`, `gutter`, `spacing`, `space`, `padding`, `margin`, `inset` | `GAP` |
| `width`, `height`, `size`, `dimension`, `min`, `max` | `WIDTH_HEIGHT` |
| _(no keyword matched)_ | `GAP` (default) |
**All other types get a fixed scope from their type** (name is ignored):
`radii → CORNER_RADIUS`, `opacity → OPACITY`, `lineHeight → LINE_HEIGHT`,
`letterSpacing → LETTER_SPACING`, `fontWeight → FONT_WEIGHT`, and
`duration / zIndex / elevation / grid / motion / breakpoint → ALL_SCOPES`.
Types with no mapping (`typography`, `shadow`, `border`) get **no scopes** (stored as null) — that is
expected; bind them in Figma manually if needed.
**Naming guidance to get correct scopes** (matters most for `color` and `spacing`):
- A border color named `color.brand` infers `ALL_FILLS` — wrong. Name it `color.border.default` or
`border-strong` so it infers `STROKE_COLOR`.
- A text color should include `text`/`fg`/`on-` (e.g. `text.primary`, `on-surface`) → `TEXT_FILL`.
- A surface/background color should include `background`/`surface`/`bg` → `FRAME_FILL` + `SHAPE_FILL`.
- A fixed dimension named `spacing.4` infers `GAP`; if it's really a width/height, name it
`size.icon.md` or include `width`/`height` → `WIDTH_HEIGHT`.
- `groupPath` counts toward the match, so `border` as a group (`border.default`) works as well as a
name keyword.
## Descriptions
Every token should carry a description of **what it's used for** (its intent / when to reach for it),
which is imported and surfaced in Studio and the Figma plugin. A token's value already says what it
*is* — the description must say what it's *for*.
- **Write intent, not the value.** ✅ `"Default page surface; sits behind cards and content."`
❌ `"#ffffff"` / `"Gray 50."`
- **Set it before the first push** — descriptions are insert-only, so an overwrite never adds or
updates one. Backfilling later means editing in Studio.
**Which formats carry a description, and how to write it:**
| Format | Field | Example |
|---|---|---|
| DTCG / W3C `.json` | `$description` | `{ "$value": "#3b82f6", "$type": "color", "$description": "Primary brand color; main CTA fills and active states." }` |
| Tokens Studio `.json` | `$description` | same `$description` key as DTCG |
| Particles JSON `.json` | `description` | `{ "name": "primary", "type": "color", "valueRaw": "#3b82f6", "description": "Primary brand color; main CTA fills." }` |
| CSS / SCSS / JS / TS / CSV | _(none)_ | These formats **cannot** carry descriptions — tokens import with `description = null`. Use DTCG if descriptions matter. |
**Recommended default format for AI pushes: DTCG (`.json`)** — it's the only widely-interoperable
format that carries `$type`, `$description`, and object composition values together. Reach for
Particles JSON when you also need per-token tiers.
## Errors
| Message | Cause / fix |
|---|---|
| `Not logged in. Run 'particles auth login'.` | Run `particles auth login`. |
| `No project configured. Run 'particles init' first.` | No `particles-ui.json` with `projectId`; run `particles init` or pass `--config`. |
| `Cannot read file: <path>` | `--file` path is wrong. |
| `File is not valid JSON` | A non-css/scss/js/ts file failed `JSON.parse`. |
| `Could not evaluate JS/TS theme file` / `must export a 'theme' object` | The `.js`/`.ts` file doesn't `export const theme = {…}`, or it threw / timed out in the sandbox. |
| `Permission denied: tokens:write is required` | Account lacks write access; ask a project admin. |
| `You are not a member of this project's organization.` | Wrong account / project. |
## Quick recipes
```bash
# DTCG/W3C JSON — safest interop, preserves composition (all one tier)
particles token-studio push --file ./tokens.json --dry-run
particles token-studio push --file ./tokens.json
# Full-fidelity Particles JSON (per-token tiers) to a feature branch
particles token-studio branch create import-2026
particles token-studio push --file ./particles-export.json --branch import-2026
# Mixed tiers via DTCG split into two pushes
particles token-studio push --file ./primitives.json --tier primitive
particles token-studio push --file ./semantic.json --tier semantic
# CSS with var() semantics — tier inferred automatically
particles token-studio push --file ./globals.css
```