| name | zndraw |
| description | Use when interacting with a ZnDraw molecular visualization server — reading frames, navigating trajectories, selecting atoms, bookmarking, creating plots, running extensions, or performing computational chemistry analysis on atomic structures |
ZnDraw Agent Skill
Overview
ZnDraw is an interactive visualization platform for atomistic simulations. Two interaction tools:
uv run zndraw-cli — Typer CLI, structured stdout. For CRUD on rooms, frames, selections, bookmarks, figures, extensions, ... .
uv run python -c "..." — Last resort for computation requiring numpy/ASE/plotly that no extension can handle. Must use the ZnDraw Python client to connect to the server and modify data. Use uv run --with <dependencies> to include extra packages.
Extensions First — MANDATORY
For ANY action that modifies atoms, creates structures, selects atoms, or runs analysis, you MUST check extensions before writing Python code or using the CLI directly.
The server has extensions (modifiers, selections, analysis) that handle most tasks. Extensions you don't expect exist — e.g. PackBox for building simulation boxes, various selection methods, analysis tools. You cannot know what's available without checking.
Required workflow for every action task:
1. uv run zndraw-cli extensions list # ALWAYS do this first
2. Scan the list — does an extension match the task?
YES → extensions describe NAME → extensions run NAME
NO → Use CLI commands or Python client as fallback
Do NOT skip step 1. Even if you think you know how to do it in Python, an extension may do it better, faster, and with proper server-side integration.
Red flags — STOP and check extensions first:
- You're about to write
uv run python -c "..." for anything other than reading data or making plots
- You're about to use ASE to build/modify structures programmatically
- You think "I'll just write a quick script to..."
- The task involves creating, deleting, duplicating, or transforming atoms
The CLI is self-documenting. Use --help on any command to discover options:
uv run zndraw-cli --help
uv run zndraw-cli frames --help
uv run zndraw-cli frames get 0
Extension names are fully qualified (e.g. @internal:modifiers:Delete). Always quote them in shell commands (@ and : may trigger shell expansion). Never hardcode names — they vary per server.
Connection & Session Setup
Both uv run zndraw-cli and the Python client require a running ZnDraw server. Start one with uv run zndraw if none is running. Verify with uv run zndraw-cli rooms list (exit code 0 = server reachable).
At the start of every session, set the room once. All commands with --room read from ZNDRAW_ROOM automatically — never pass --room manually after this.
uv run zndraw-cli rooms list
export ZNDRAW_ROOM=<room-id>
uv run zndraw-cli frames count
uv run zndraw-cli extensions list
uv run zndraw-cli step get
For explicit server connection: uv run zndraw-cli rooms list --url http://localhost:8000 --token TOKEN
Env vars: ZNDRAW_URL, ZNDRAW_TOKEN, ZNDRAW_ROOM
Authentication
The CLI auto-discovers tokens in this order:
--token flag / ZNDRAW_TOKEN env var, OR --user / ZNDRAW_USER + --password / ZNDRAW_PASSWORD flags (mutually exclusive)
- Stored token in
~/.zndraw/tokens.json (validated on use, removed on 401)
- Guest token (anonymous fallback)
uv run zndraw-cli auth status
uv run zndraw-cli auth login
uv run zndraw-cli auth logout
For admin users who need to act as another user:
uv run zndraw-cli auth status
uv run zndraw-cli admin users list
uv run zndraw-cli admin users login USER_ID
After auth login or admin users login, the token is stored in ~/.zndraw/tokens.json and automatically used for all subsequent CLI commands — no --token needed.
Screenshots, GIFs & Browser Sessions
Screenshots and GIF capture require two prerequisites:
- You must be logged in as the user (not as a guest)
- The user must have the room open in their browser
Auth login is INTERACTIVE — you CANNOT approve it yourself
auth login opens a browser window where the human user must click "Approve". This is a device-code flow — the CLI prints a code and waits up to 5 minutes for browser approval.
You MUST NOT use Playwright, Selenium, or any browser automation to approve the login — no programmatic workarounds.
- Attempt to create sessions or capture screenshots without proper auth
Required workflow for screenshots/GIFs:
uv run zndraw-cli auth status
uv run zndraw-cli auth login
uv run zndraw-cli auth status
uv run zndraw-cli screenshots request
uv run zndraw-cli gif capture --orbit -o out.gif
If auth status shows token_source: "stored", skip steps 2-3 — you're already logged in.
GIF capture tips
- Hide helper geometries before capture — curves, helper lines, etc. are visible in GIFs. Toggle them off first:
uv run zndraw-cli geometries toggle my-curve
uv run zndraw-cli gif capture --curve my-curve --curve-step 0.02 -o out.gif
uv run zndraw-cli geometries toggle my-curve
- Use presets for visual styles — don't manually create PathTracing/lighting geometries. Use
presets apply pathtracing instead.
- Increase
--delay for path tracing — default 0.02s is too fast for path tracing to converge. Use --delay 0.5 or higher:
uv run zndraw-cli presets apply pathtracing
uv run zndraw-cli gif capture --orbit --delay 0.5 -o pathtraced.gif
--center and --target — --center x,y,z sets the orbit center, --target x,y,z sets where the camera looks. Default: target = center.
CLI Quick Reference
Use <command> --help for full options. Key patterns:
| Resource | Verbs |
|---|
auth | login, status, logout |
admin | users list, users login |
rooms | list, create, info, open, lock, unlock, set-default |
frames | count, get, list, extend FILE, export, delete |
step | get, set |
selection | get, set, clear |
selection-groups | list, get, set, delete |
bookmarks | list, set, delete |
figures | list, get, set (--file or --data), delete |
chat | list, send |
extensions | list, describe, run |
jobs | list, status |
geometries | list, get, set, delete, toggle, set-prop, types, describe |
preset | list, get, load, apply, save, export, delete, reset |
screenshots | list, request, get |
gif | capture |
sessions | list, get-camera, set-camera |
| Standalone | mount FILE |
Frames tips:
--format xyz for extxyz output, default is JSON
--keys arrays.positions,info.energy for server-side field filtering (works on both get and list). Keys use dotted prefixes: arrays.* for per-atom data (positions, numbers, colors, radii), info.* for per-frame metadata (energy, connectivity). Structural fields (cell, pbc) are always included.
--start N --stop M for ranges (don't fetch 10k frames at once)
export --indices "0:10" supports Python range syntax (start:stop or start:stop:step)
export --selection exports only currently selected atoms
extend goes through ASE file I/O which may reclassify info fields (e.g. energy → atoms.calc). Use Python client vis.append() for full fidelity.
Chat tips:
send has full markdown support
- Use single quotes for messages with special characters:
'Hello!'
- Escape sequences
\n and \t are processed — send 'line1\nline2' produces a multi-line message
- Send relevant updates to the chat in addition to printing them.
Geometry tips:
set KEY --data '{"resolution": 32}' without --type performs a partial update (PATCH, deep merge)
set KEY --type Sphere --data '{...}' with --type performs a full replace (PUT)
toggle KEY flips a geometry's active state (on/off)
set-prop KEY material.opacity 0.5 sets a single property using dot notation (builds nested PATCH)
Isosurface data contract
Isosurface renders a 3D surface from volumetric data stored in a frame's atoms.info dict.
cube_key: dotted path to the info key (e.g. info.orbital_homo)
- The referenced dict must contain:
grid: np.ndarray of shape (Nx, Ny, Nz) — scalar field values
origin: np.ndarray of shape (3,) — world-space origin (Angstrom)
cell: np.ndarray of shape (3, 3) — axis vectors spanning the grid (Angstrom)
- Parameters:
isovalue (float, default 0.02), resolution (0–1, default 1.0), opacity (0–1, default 0.6), color (hex, default #2244CC)
from zndraw import ZnDraw
from zndraw.geometries import Isosurface
vis = ZnDraw(room="ROOM")
vis.geometries["homo"] = Isosurface(cube_key="info.orbital_homo", isovalue=0.02)
Use positive and negative isovalues to show both lobes of an orbital.
Extensions: Discover → Describe → Run
uv run zndraw-cli extensions list
uv run zndraw-cli extensions describe "@internal:modifiers:AddFromSMILES"
uv run zndraw-cli extensions run "@internal:modifiers:AddFromSMILES" --smiles "CCO"
uv run zndraw-cli extensions run "@internal:selections:All" --wait --timeout 30
uv run zndraw-cli jobs status JOB_ID
Important: Extension parameters are passed as --flag value pairs (from describe output), NOT as --params '{"key": "value"}' JSON.
Three extension categories exist: modifiers (edit atoms), selections (pick atoms), analysis (create plots). Discover which are available via extensions list.
Note: Custom extensions registered via vis.register_job() are scoped to the room they were registered in. Use vis.register_job(cls, room="@global") for global registration (admin-only, visible in all rooms).
Visual Presets
Named visual styles (materials, lighting, fog) that can be applied to room geometries. Bundled presets (matt, flat, glossy, pathtracing) are always available in every room without needing to be created. Creating a room-level preset with the same name overrides the bundled one; deleting the override makes the bundled version reappear.
uv run zndraw-cli presets list
uv run zndraw-cli presets apply matt
uv run zndraw-cli presets reset
uv run zndraw-cli presets save my-style -p "particles*" -p "fog"
uv run zndraw-cli presets export my-style ./my-style.json
uv run zndraw-cli presets load ./my-style.json
Presets use fnmatch patterns (particles*, *light*, fog) and optional geometry_type filters to target specific geometries. Rules are deep-merged — only specified config keys are overridden.
Python Scripting with ZnDraw Client
Only use Python when no extension covers the task — typically for custom data reads, math, filtering, or Plotly figures. If you haven't run extensions list yet, do that first.
Use uv run python -c "..." with the ZnDraw Python client. It connects to the server and provides a MutableSequence[ase.Atoms] interface.
from zndraw import ZnDraw, Extension, Category
vis = ZnDraw(room="ROOM")
len(vis)
vis[0]
vis[vis.step]
vis.step = 42
vis.append(atoms)
del vis[0]
vis[:]
vis.selection = [0, 1, 2]
vis.bookmarks[42] = "label"
vis.figures["energy"] = fig
vis.chat.send("message")
vis.chat[0]
vis.locked = True
task = vis.run("@internal:modifiers:Delete")
task = vis.run("@internal:modifiers:AddFromSMILES", smiles="CCO")
task.wait(timeout=30)
task.status
task.id
list(vis.extensions)
vis.extensions["@internal:modifiers:Delete"]["schema"]
vis.tasks[task_id]
vis.tasks("running")
vis.sessions
list(vis.sessions)
sids = list(vis.sessions)
session = vis.sessions[sids[0]]
img = session.screenshot()
img.data
img.save("frame.png")
list(vis.presets)
vis.presets.apply("matt")
vis.presets.apply("@default")
vis.presets.load(Path("f.json"))
vis.presets.export("pub", Path("out.json"))
vis.presets["custom"] = Preset(name="custom", rules=[...])
del vis.presets["custom"]
vis.get(slice(None), keys=["info.energy"])
ZnDraw.list_rooms()
token = ZnDraw.login(username="...", password="...")
Bookmark frames matching a condition
uv run python -c "
from zndraw import ZnDraw
vis = ZnDraw(room='ROOM')
for i, atoms in enumerate(vis):
if atoms.info.get('energy', 0) > -40:
vis.bookmarks[i] = f'E={atoms.info[\"energy\"]:.2f}'
"
Plot energy over time
uv run python -c "
from zndraw import ZnDraw
import plotly.graph_objects as go
vis = ZnDraw(room='ROOM')
data = vis.get(slice(None), keys=['info.energy'])
energies = [f['info.energy'] for f in data]
fig = go.Figure(go.Scatter(x=list(range(len(energies))), y=energies, mode='lines+markers'))
fig.update_layout(title='Energy', xaxis_title='Frame', yaxis_title='Energy (eV)')
vis.figures['energy'] = fig
"
Select atoms by element (Python fallback)
Only use this if extensions list shows no suitable selection extension (e.g. no ByElement/BySymbol):
uv run python -c "
from zndraw import ZnDraw
vis = ZnDraw(room='ROOM')
atoms = vis[vis.step]
vis.selection = [i for i, s in enumerate(atoms.get_chemical_symbols()) if s == 'H']
"
Compute RDF (Python fallback)
Only use if extensions list shows no RDF analysis extension:
uv run python -c "
from zndraw import ZnDraw
import plotly.graph_objects as go
from ase.geometry.analysis import Analysis
vis = ZnDraw(room='ROOM')
ana = Analysis(vis[:])
rdf = ana.get_rdf(rmax=6.0, nbins=100, return_dists=True)
fig = go.Figure(go.Scatter(x=rdf[1][0].tolist(), y=rdf[0][0].tolist(), mode='lines'))
fig.update_layout(title='RDF', xaxis_title='r (A)', yaxis_title='g(r)')
vis.figures['rdf'] = fig
"
Mounting Large Files
For large files that support sliced access (.h5, .lmdb, .zarr, .db), use mount — frames are served lazily on demand.
uv run zndraw-cli mount trajectory.h5
uv run zndraw-cli mount trajectory.h5 --room my-room
This command keeps serving frames until Ctrl+C is pressed.
Camera and Session Control
Each browser session has an active camera. Use session commands to manage cameras:
uv run zndraw-cli sessions list
uv run zndraw-cli sessions get-camera SESSION_ID
uv run zndraw-cli sessions set-camera SESSION_ID my-cam
Camera geometries can be partially updated like any geometry:
uv run zndraw-cli geometries set my-camera --data '{"fov": 45}'
Task Mapping
| User request | Approach |
|---|
| "build a box / pack molecules" | extensions list → find PackBox or similar → describe → run |
| "delete selected atoms" | extensions list → find Delete modifier → describe → run |
| "add a molecule" | extensions list → find AddFromSMILES → describe → run with params |
| "show me aspirin / molecule" | extensions list → find AddFromSMILES → describe → run --smiles "..." |
| "select all hydrogens" | extensions list → check for element selection extension first; fallback: Python vis.selection = [...] |
| "duplicate / replicate atoms" | extensions list → find Replicate or similar → describe → run |
| "compute RDF / analysis" | extensions list → check for analysis extension first; fallback: Python ASE + Plotly |
| "jump to frame 42" | uv run zndraw-cli step set 42 |
| "how many frames?" | uv run zndraw-cli frames count |
| "bookmark high-energy frames" | Python: iterate vis, set vis.bookmarks[i] |
| "plot energy over time" | Python: vis.get(...) + Plotly → vis.figures[...] |
| "visualize this large H5 file" | uv run zndraw-cli mount trajectory.h5 (lazy) |
| "open / show the room" | uv run zndraw-cli rooms open |
| "lock the room" | uv run zndraw-cli rooms lock or Python: vis.locked = True |
| "send a chat message" | uv run zndraw-cli chat send "msg" or Python: vis.chat.send("msg") |
| "take a screenshot" | Check auth status → auth login if needed (ask user to approve in browser) → screenshots request |
| "create a GIF / orbit animation" | Check auth status → auth login if needed (ask user) → ensure room open in browser → gif capture --orbit -o out.gif |
| "create a path-traced GIF" | presets apply pathtracing → gif capture --orbit --delay 0.5 -o out.gif |
| "GIF along a camera path" | Curve must exist as a geometry (created via UI or geometries set) → geometries toggle CURVE to hide → gif capture --curve CURVE ... → toggle CURVE back |
| "list browser sessions" | uv run zndraw-cli sessions list |
| "who am I / admin check" | uv run zndraw-cli auth status |
| "apply matt style" | uv run zndraw-cli presets apply matt or Python: vis.presets.apply("matt") |
| "reset to defaults" | uv run zndraw-cli presets reset or Python: vis.presets.apply("@default") |
| "save current look as preset" | uv run zndraw-cli presets save my-style -p "particles*" -p "fog" |
| "share a preset" | uv run zndraw-cli presets export my-style ./my-style.json |
| "toggle fog off" | uv run zndraw-cli geometries toggle fog |
| "make particles transparent" | uv run zndraw-cli geometries set-prop particles material.opacity 0.5 |
| "run extension and wait" | extensions run EXT --wait or Python: vis.run(EXT).wait() |
| "visualize volumetric data / orbitals" | geometries set KEY --type Isosurface --data '{"cube_key": "info.KEY", "isovalue": 0.02}' |
| "what can I do?" | extensions list + --help on each resource group |
Common Mistakes
- Writing Python before checking extensions — the #1 mistake. An extension likely already does what you're about to script.
extensions list takes 1 second; writing+debugging a Python script takes minutes. Always check first.
- Trying to automate
auth login approval — auth login opens a browser for the human user to approve. You CANNOT use Playwright, Selenium, or any automation. Ask the user to approve in their browser and wait for confirmation.
- Screenshots/GIFs without login — guest tokens create a different user identity than the browser session. You must
auth login first, and the user must have the room open in their browser.
- Repeating
--room on every command — set export ZNDRAW_ROOM=... once at session start. Never pass --room manually after that.
- Running bare
zndraw-cli — always use uv run zndraw-cli; the binary is not on PATH
- Guessing extension names/params — extensions change per server and per room. Always
list then describe first; never assume you know what's available.
- Fetching everything — use
vis.get(..., keys=[...]) or CLI --keys to limit data
- Not waiting for tasks — use
--wait flag or Python task.wait() instead of manual polling
- Wrong room —
rooms list to discover, don't guess UUIDs
- Piping CLI to Python for mutations — use the
ZnDraw client instead; it connects directly and handles serialization
- Leaving helper geometries visible in GIFs — curves, helper lines show up in captures. Toggle them off with
geometries toggle KEY before gif capture.
- Manual path tracing setup — use
presets apply pathtracing, not manual geometries set --type PathTracing. Presets are single-command and correct.
- Default
--delay with path tracing — 0.02s produces noisy frames. Use --delay 0.5 or higher for path tracing to accumulate samples.
screenshots get instead of screenshots request — get ID downloads an existing screenshot by its database ID. request captures a new one.