Bundles
The bundle.yaml reference
Every key the runtime reads from bundle.yaml and extension.yaml, which kinds read it, what it falls back to, and the exact rules of the very small parser that reads the file.
A bundle is a folder. bundle.yaml is the optional file inside it that names the
button, describes it, picks its command name and engine, binds its keys and gates it. Nothing
in it is required for a script bundle: one with no bundle.yaml still produces a
working button and command, because every value has a fallback. Two kinds do need it: a
*.urlbutton needs url: and a *.linkbutton needs
plugin:.
Count.pushbutton\
script.py # required: no script.py, no button
bundle.yaml # optional: metadata only
icon.png # optionalThe file is read at discovery: at AutoCAD startup and on every Reload. Change it and click
Reload on the pyACAD panel (or type PYACADRELOAD); there is no
need to restart AutoCAD. The same reader handles extension.yaml and the
bundle.yaml inside tabs, panels and containers.
It is not YAML#
This is the single most useful thing to know about the file. pyACAD does not use a YAML
library. It uses a small reader that walks the file one line at a time and produces a flat
dictionary of strings. The name and the .yaml extension are a convention
inherited from pyRevit, not a promise of YAML semantics.
The whole algorithm, in order:
- Split the text on
\n. Trim a trailing\r, then trim whitespace from both ends of the line. - Skip the line if it is now empty, or if it starts with
#. - If the last key line had nothing after its colon and this line starts with
-(dash, space), the rest of the line is one item of that key's block list. One matching layer of quotes is removed from the item and it is appended. Nothing else on the line is looked at. - Otherwise find the first
:. If there is none, or it is the first character, skip the line. - The key is everything before that colon, trimmed and lowercased.
- The value is everything after that colon, trimmed.
- If the value is two characters or longer and starts and ends with the same quote
character,
"or', remove exactly one character from each end. - Store it. A key seen twice keeps the last value. An empty value opens a block list for
the
-lines that follow; the next key line closes it.
That is all of it. There is no escape processing, no type coercion and no schema. The block
list is the one shape of structure the reader knows, and layout: is the only key
that reads one. A file that does not exist yields no keys; a file that exists but cannot be
read is logged (Could not read '<path>' - its title, tooltip, context and shortcut
are ignored.) and also yields none, so the bundle falls back on its defaults.
Inputs that surprise people#
Most of these follow from the rules above rather than being special cases, but they are worth reading once so you recognise them when a value comes out wrong.
| Line | Key | Value | Why |
|---|---|---|---|
context: doc # gate | context |
doc # gate |
Trailing comments are not a thing. Here it breaks the rule (# is not
allowed in one), so the button is never greyed; on min_host_version it
makes the value "not a year"; on title it shows on the ribbon. |
title: Count | title |
Count |
The line is trimmed first, so indentation means nothing. |
TITLE: Count | title | Count |
Keys are lowercased. Values keep their case. |
tooltip: Scale is 1:50 | tooltip |
Scale is 1:50 |
The split is on the first colon, so colons in prose are safe. |
title: "Count" | title | Count |
One matching layer of quotes is removed. |
title: ""Count"" | title |
"Count" |
Exactly one layer, and there is no escape syntax. |
tooltip: "A" or "B" | tooltip |
A" or "B |
The test is only that the first and last characters match. |
title: "Count' | title |
"Count' |
Mismatched quotes are left alone. |
title Count | none | none | No colon, so the line is dropped silently. |
: Count | none | none | A colon at the very start means no key. |
title: A then title: B | title |
B | The last one wins. |
title: | title | empty | The key exists with an empty value, which is not the same as being absent; it also opens a block list. |
title: with nothing after it gives you a button with an empty caption, not one
titled from the folder name, and an empty title derives the command name
PYTOOL. Fallbacks apply only when a key is missing. To get the default
back, delete the line.
What the file cannot express#
Because the reader only produces flat string pairs, none of the following work, and none of them raise an error: they parse into keys nothing reads.
- Nesting. An indented block is just more top-level keys.
- Lists, except one shape. A key with nothing after its colon followed by
- itemlines is a block list, andlayout:is the only key that reads one. A- itemline anywhere else has no colon and is dropped. - Multi-line values. There is no
|or>. A tooltip is one line. (A docstring tooltip can be several; see below.) - Types. Every value is a string. Numbers are parsed by the keys that want them; there are no booleans or nulls.
# Count objects by layer title: Count\nObjects tooltip: Counts the selected objects, or all of model space. command: PYCOUNTOBJ shortcut: Ctrl+Shift+C context: doc
title: Count Objects # by layer tooltip: > Counts the selected objects, or all of model space. context: doc # needs a drawing
In the second file the title keeps its comment, tooltip is literally
>, the two prose lines vanish for having no colon, and the context rule fails to
parse, so the button is never greyed. Nothing warns you except the context rule, in the
log.
Which file reads which keys#
Every folder in the tree may carry a bundle.yaml, but each kind reads a
different subset. Any other key is ignored without a word, with one exception noted below.
| Where | Keys read |
|---|---|
*.extension\extension.yaml |
name, engine, layout |
*.tab, *.panel, *.stack, *.slideout |
layout only |
*.pulldown, *.splitbutton, *.splitpushbutton |
title, tooltip, keytip, layout |
*.pushbutton, *.nobutton, *.smartbutton |
title, tooltip, command, engine,
shortcut, keytip, context,
min_host_version, max_host_version |
*.toggle |
All of the pushbutton keys, plus title_on and tooltip_on |
*.urlbutton |
url (required), title, tooltip, keytip |
*.linkbutton |
plugin (required), title, tooltip, keytip |
*.dockpane |
title, tooltip, engine, keytip,
shortcut (read but not bound in the current build) |
The exception: title_on or tooltip_on on anything but a toggle is
logged (title_on and tooltip_on are for *.toggle bundles only - ignored.). Keys
some other tools use, such as author, help_url,
highlight, icon or min_revit_version, are not read by
pyACAD at all.
The keys a pushbutton reads#
These apply to *.pushbutton, *.nobutton, *.toggle and
*.smartbutton, wherever they sit: on a panel, in a stack, in a menu, or in a
slideout.
| Key | Falls back to | What it does |
|---|---|---|
title |
__title__ in the script, then the folder name with any legacy
NN_ prefix removed and underscores turned into spaces |
The caption. A literal \n breaks it onto a second line on a large button.
The command name is derived from it unless command is set. |
tooltip |
The script's module docstring, then nothing | The hover tooltip. The ribbon appends the resolved shortcut in brackets, then a
line naming what to type: Counts objects. (Ctrl+Shift+C) and
Command: PYCOUNT (alias CNT). |
command |
PY plus the title's letters and digits, uppercased |
The AutoCAD command name. See below. |
alias | none | Short extra names to type, like BX. Users can replace or drop them. See
below. |
engine |
The extension's engine, then ironpython |
ironpython or cpython, matched ignoring case. |
shortcut | none | The author's default keyboard chord, for example Ctrl+Shift+C. Needs Ctrl
or Alt unless the user allows bare keys. Users can rebind or disable it. See
Shortcuts and keytips. |
keytip | generated from the title | The letters shown for Alt navigation of the ribbon, uppercased. |
context | none: always enabled | Greys the button unless a drawing condition holds. See below. |
min_host_version | none: no floor | Oldest AutoCAD release the tool supports, as a year. See below. |
max_host_version | none: no ceiling | Newest AutoCAD release the tool supports, as a year. See below. |
title_on, tooltip_on |
none: one caption and tooltip for both states | *.toggle only: the caption and tooltip while the toggle is on. See
Buttons. |
Discovery does not check the engine name. engine: python3 builds a
normal-looking button; running it writes Unknown engine 'python3'. to the output
window and nothing runs. If a tool does nothing but open a window with that message, check
this line first.
command: naming the AutoCAD command#
Every bundle that reads the pushbutton keys is registered as an AutoCAD command at boot and
on every Reload. Without command:, the name is PY followed by every
letter and digit of the resolved title, uppercased, capped at 64 characters:
| Resolved title | Command |
|---|---|
Blocks to Excel | PYBLOCKSTOEXCEL |
Find and Replace Attributes | PYFINDANDREPLACEATTRIBUTES |
Export\nLayers (a two-line caption) | PYEXPORTLAYERS |
2D Takeoff (beta) | PY2DTAKEOFFBETA |
| empty, or no letters or digits | PYTOOL |
With command:, the value is uppercased, anything but letters, digits,
_ and - is dropped, and it is used as is. No PY
is added, so command: blkx registers BLKX. A bundle with a
config.py also gets the same name with CFG appended, which runs the
config script.
title: Blocks to Excel
command: PYB2XNames are handed out in load order and the first claim wins. A bundle loses its command
when the name is already taken by an earlier bundle, by one of the reserved loader names
(PYACAD, PYACADRELOAD, PYACADCONSOLE,
PYACADRUN, PYACADSETTINGS), or by anything AutoCAD already answers to
(a native command, a LISP-defined one, another add-in's). Each loss is logged and toasted
(One command was not registered, with the reason), and the losing bundle's button
still works by running its script directly. Pick a command: that is distinctive
enough not to collide with the tools of the people you share with.
alias: short names to type#
People who draw all day type two or three letters, not PYBLOCKSTOEXCEL.
alias: gives a tool short extra names. Each one is registered as a second command
that runs the same tool, so it works typed, in a macro, in a script file and from
(pyacad "BX"). Several aliases go on one line, split by commas or spaces; each is
uppercased and cleaned the same way as command:.
title: Blocks to Excel
alias: BX, B2XAn alias never changes what a keystroke already does:
- One that the user's
acad.pgpdefines, or that AutoCAD already answers to (a native command, a LISPC:function, another add-in), is left out. The existing meaning keeps working and the full command name still runs the tool. - One that a drawing's LISP defines later (an
acaddoc.lspthat loads after pyACAD) hands the run to that LISP command. - Aliases are named after every full command name, so an alias never takes another tool's command.
A bundle's alias that is left out for one of these reasons only goes to the log, because
nothing is lost. Users choose their own aliases in config.json under
aliases, which replaces
the bundle's list; an alias of their own that cannot be registered is toasted.
context: enabling and disabling a button#
Without a context: key a button is always enabled. With one, the runtime parses
a small boolean expression once at discovery and re-evaluates it whenever it sees an AutoCAD
event (a drawing opening or switching, a command ending, the selection changing, a layout
switch, and so on), greying or enabling the button to match.
Eight condition names, answered from one snapshot of the active drawing:
| Condition | True when |
|---|---|
doc | A drawing is open and active. |
selection | The pickfirst selection is not empty. |
modelspace | The Model tab is current (TILEMODE is 1). |
paperspace | A layout tab is current, including while working inside a viewport on it. |
xrefs | The drawing has at least one attached or overlaid xref. |
blocks | The drawing has at least one ordinary block definition (not a layout, anonymous, xref or xref-dependent block). A definition is enough; it need not be inserted. |
layouts | The drawing has at least one paper space layout. |
multi-doc | More than one drawing is open. |
Combine names with & (and), | (or) and ! (not).
! binds tightest, then &, then |. There are no
parentheses: ( is not part of the vocabulary. The word forms and,
or and not, and the doubled && and
||, are not accepted either. Names ignore case.
context: doc
context: selection & modelspace
context: xrefs | blocks
context: doc & !multi-docA name that is not one of the eight is legal syntax but always false, and discovery logs it:
Bundle 'D:\CadTools\...\Count.pushbutton': unknown context condition 'selction'.A rule that does not parse at all, such as a stray operator or a trailing comment, is logged too, and the button falls back to always enabled rather than never enabled:
Bundle 'D:\CadTools\...\Count.pushbutton': bad context rule - Unexpected '&' in context
rule '& selection'. Button stays always enabled.context: greys a drawn button and nothing else. The command can still be typed,
run from a macro or from (pyacad ...), and a keyboard chord still fires it. A
script that needs a selection must still check for one. On a *.nobutton, which is
never drawn, the key has no effect at all.
min_host_version and max_host_version: gating on the AutoCAD release#
Two keys, each a bare year such as 2023. Either can be used alone.
title: Batch Publish
min_host_version: 2023
max_host_version: 2026An out-of-range bundle still renders exactly like any other: it is not greyed and not
hidden. The check happens when it runs, before the script starts, whichever way it was started
(click, chord, typed command, config.py, PYACAD or
(pyacad ...)), and shows a warning toast instead of running it:
Requires AutoCAD 2023 or newer (this is 2022).
Requires AutoCAD 2024 or older (this is 2026).A value that is not a whole number is logged and ignored, leaving that side of the range
open: 2024 works; 2024.1, R24.3 and
2024 # note do not.
Bundle 'D:\CadTools\...\Batch_Publish.pushbutton': min_host_version 'R24.3' is not a year - ignored.The host year comes from AutoCAD's own version number. If it cannot be determined, the gate
is off and every bundle runs, because refusing on a host you cannot identify is the worse
failure. A smartbutton's build-time run and a dock pane's script.py are not
version checked.
The keys a pulldown reads#
A *.pulldown, *.splitbutton or *.splitpushbutton may
carry its own bundle.yaml describing the menu button itself. It reads four keys.
| Key | Falls back to | What it does |
|---|---|---|
title | The folder name | The caption; \n breaks it. |
tooltip | none | The hover tooltip. |
keytip | generated from the title | The Alt-navigation letters. |
layout | name order | The order of the menu rows. |
command, shortcut, engine and context
are not read there, because the menu button runs nothing itself; put them on the pushbuttons
inside it.
The keys a urlbutton reads#
| Key | Falls back to | What it does |
|---|---|---|
url | required | The address opened in the default browser, trimmed. Missing or blank: the bundle is
skipped with Urlbutton '<dir>' has no url: in bundle.yaml - skipped. |
title | The folder name | The caption. |
tooltip | The url itself | The hover tooltip. |
keytip | generated from the title | The Alt-navigation letters. |
The keys a linkbutton reads#
| Key | Falls back to | What it does |
|---|---|---|
plugin | required | The AutoCAD command the button sends, for example LAYWALK or
-LAYER. Uppercased and cut down to letters, digits, _ and
-; sent as _.<NAME>. Missing or blank: skipped with
Linkbutton '<dir>' has no plugin: in bundle.yaml - skipped. |
title | The folder name | The caption. |
tooltip | none | The hover tooltip. |
keytip | generated from the title | The Alt-navigation letters. |
The keys a dockpane reads#
| Key | Falls back to | What it does |
|---|---|---|
title | The folder name | The ribbon caption and the palette's title. |
tooltip | none | The ribbon toggle's tooltip. |
engine | The extension's engine, then
ironpython | The engine that runs the pane's script.py. |
keytip | generated from the title | The Alt-navigation letters. |
shortcut | none | Read, but not bound to anything in the current build. |
A dock pane does not read tooltip from a docstring, has no
command, and takes no context or host version keys.
Buttons covers the palette itself.
The keys an extension reads#
extension.yaml, directly inside the .extension folder, reads three
keys.
name: CadTools
engine: ironpython
layout:
- CadTools
- Plotting| Key | Falls back to | What it does |
|---|---|---|
name | The folder name without .extension |
The extension's identity: part of its tab ids, the label on its hooks and
startup.py in the log, and the key that spots a second copy loaded from
another root (the second is skipped). Not shown on the ribbon. |
engine | ironpython |
The default engine for every bundle, hook and startup.py in the
extension. |
layout | name order | The order of the extension's tabs. |
layout: ordering the folders inside a container#
layout: is a block list: the key with nothing after its colon, then one
- name line per child folder, in the order you want.
layout:
- Count
- Rename
- Layers.stack
- More- Listed children come first, in list order; unlisted ones follow in case-insensitive name order, so nothing is ever hidden by being left off.
- An entry matches a folder with or without its suffix (
LayersorLayers.stack), ignoring case and any legacyNN_prefix on either side. - An entry with no matching folder is reported in the after-scan toast and the log, and skipped. A repeated entry counts once.
- Comment lines (
#) inside the list are skipped and do not end it; the nextkey:line does.
It is read from extension.yaml (tabs) and from the bundle.yaml of a
tab (panels), panel (items), stack (rows), pulldown or split button (menu rows) and slideout
(flyout items). Anatomy covers ordering
in context.
Folders that read only a layout list#
The bundle.yaml inside a *.tab, *.panel,
*.stack or *.slideout is opened for layout: and nothing
else. In particular there is no title: for a tab or a panel: their captions are
always the folder name, with underscores turned into spaces. To rename a panel on the ribbon,
rename its folder (and its entry in the tab's layout:).
How title and tooltip actually resolve#
| Kind | Title, first that exists | Tooltip, first that exists |
|---|---|---|
| Pushbutton, nobutton, toggle, smartbutton | title:, __title__ in script.py, folder name |
tooltip:, module docstring of script.py, none |
| Pulldown, splitbutton, splitpushbutton | title:, folder name | tooltip:, none |
| Urlbutton | title:, folder name | tooltip:, the url |
| Linkbutton, dockpane | title:, folder name | tooltip:, none |
| Tab, panel | folder name | none |
"Folder name" always means: suffix removed, a leading run of two or more digits and an
underscore removed, remaining underscores turned into spaces. A toggle that is on shows
title_on and tooltip_on instead, when set.
Breaking a long caption onto two lines#
A long caption stretches its panel wide. Write a literal backslash-n in the title to break
it: title: Export\nLayers. Spaces around the \n are absorbed, so
Export \n Layers is the same. The break applies only where there is a second line
to use, the caption of a large button (pushbutton, toggle, smartbutton, pulldown, split
button, url and link buttons, dock pane toggle). Everywhere else (stack and menu rows, logs,
toasts, the output window title, keytips, __title__ and the derived command name)
sees the flat form Export Layers.
How the script is scanned#
For the pushbutton family, pyACAD reads script.py as text at discovery, without
running it, to find two things. Both are found with simple pattern matching, so they have to
be written plainly.
__title__#
A line that starts, in the first column, with __title__ = followed by a
single- or double-quoted string on that same line. The string between the quotes is the title.
Indented assignments, triple-quoted strings, string concatenation and anything computed are
not seen.
__title__ = 'Count Objects' # seen: the title is "Count Objects"The docstring#
The first statement of the file, after any blank and comment lines, must open with
""" or '''. The text up to the matching closing quotes, trimmed, is
the tooltip, and it may run over several lines. A docstring with a prefix such as
r""", or one that comes after an import, is not seen.
# -*- coding: utf-8 -*-
"""Counts the selected objects, or every object in model space.
Pass a layer pattern from AutoLISP to count only those layers."""
from pyacad import selThe scan never fails a bundle: a script that cannot be read or scanned simply contributes no title and no tooltip.
A complete example#
# Freeze everything but the current layer; click again to restore.
title: Isolate\nLayer
title_on: Restore\nLayers
tooltip: Freezes every layer except the current one, remembering what was on.
tooltip_on: Puts back the layer states saved when you isolated.
command: PYISOLATECURRENT
engine: ironpython
shortcut: Ctrl+Alt+I
keytip: IL
context: doc & modelspace
min_host_version: 2022That toggle registers PYISOLATECURRENT, greys itself unless a drawing is open
with the Model tab current, refuses to run on AutoCAD 2021, binds Ctrl+Alt+I
(which users can rebind), answers to Alt, PY,
IL on the ribbon, and swaps caption and tooltip while on.