Bundles
The bundle.yaml reference
Every key the runtime reads, 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 engine and binds its keys. Nothing in it is required:
a bundle with no bundle.yaml at all still produces a working button, because
every value has a fallback.
Count.pushbutton\
script.py # required: no script.py, no button
bundle.yaml # optional: metadata only
icon.png # optionalThe file is read once, at discovery time. Change it and click Reload on the pyNavis panel; there is no need to restart Navisworks.
It is not YAML#
This is the single most useful thing to know about the file. pyNavis does not use a YAML
library. It uses a forty-line 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
#. - 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.
That is all of it. There is no escape processing, no type coercion, no schema, and no error reporting. A file that cannot be read, or that does not exist, yields an empty dictionary and the bundle falls back on its defaults.
Inputs that surprise people#
Most of these are consequences of the rules above rather than special cases, but they are worth reading once so you recognise them when a value comes out wrong.
| Line | Key | Value | Why |
|---|---|---|---|
title: Export # for CSV | title |
Export # for CSV |
Trailing comments are not a thing. Only whole-line comments are. |
title: Export | title |
Export |
The line is trimmed first, so indentation carries no meaning. An indented line is a top-level key. |
TITLE: Export | title | Export |
Keys are lowercased. Title:, TITLE: and
tItLe: are the same key. |
tooltip: Ratio is 3:1 | tooltip |
Ratio is 3:1 |
The split is on the first colon, so colons in prose are safe. |
title: "Export" | title | Export |
One matching layer of quotes is removed. |
title: ""Export"" | title |
"Export" |
Exactly one layer, never two, 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. They do not have to be a pair. |
title: "Export' | title |
"Export' |
Mismatched quotes are left alone and become part of the value. |
title Export | none | none | No colon, so the line is discarded silently. |
: Export | none | none | The colon is at index 0, which the parser treats as no key. |
title: A then title: B | title |
B | Duplicate keys overwrite. The last one wins. |
title: | title | empty string | The key exists with an empty value, which is not the same as being absent. |
title: with nothing after it gives you a button labelled with an empty string,
not a button labelled from the folder name. Fallbacks only apply when the key is
missing, not when it is present and blank. If you want the default back, delete the
line.
What the file cannot express#
Because the parser only ever produces flat string pairs, none of the following work. They do not raise an error either; they parse into keys you will never read again.
- Nesting. An indented block is just more top-level keys, and the parent line becomes a key with an empty value.
- Lists. A line of
- itemhas no colon and is dropped. - Multi-line values. There is no
|or>handling. A tooltip is one line. - Anchors, aliases, tags, documents. None of the YAML machinery exists.
- Types. Every value is a string. There are no booleans, numbers or nulls.
# Export the current selection title: Export points tooltip: Writes every selected item to a CSV file. engine: ironpython shortcut: Ctrl+Shift+E keytip: EP
title: Export points # to CSV tooltip: > Writes every selected item to a CSV file. options: - csv - json
In the second file the title keeps the comment, tooltip is literally
>, the two prose lines vanish for having no colon, options is an
empty string, and the two list lines vanish as well. Nothing warns you.
The keys a pushbutton reads#
Exactly eight, and nothing else in the file is looked at. The same eight apply to the three
kinds that are parsed as pushbuttons: *.nobutton, *.toggle and
*.smartbutton, wherever they sit, including inside a *.slideout.
context: is the one key a nobutton reads to no effect, because greying needs a
rendered control.
| Key | Falls back to | What it does |
|---|---|---|
title |
__title__ in the script, then the folder name with any
NN_ prefix removed and underscores turned into spaces |
The caption on the button. A literal \n breaks it onto a second line,
see below. |
tooltip |
The script's module docstring, then nothing | The hover tooltip. The ribbon appends the resolved shortcut in brackets, so a bound
tool shows Writes a CSV. (Ctrl+Shift+E). |
engine |
The extension's engine, then ironpython |
ironpython or cpython, matched case-insensitively. |
shortcut | nothing | The author's default chord, for example Ctrl+Shift+E. 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 | nothing; absent means always enabled | Greys the button out unless a document condition holds. See below. |
min_host_version | nothing; absent means no floor | Oldest Navisworks release this button supports, as a year. See below. |
max_host_version | nothing; absent means no ceiling | Newest Navisworks release this button supports, as a year. See below. |
Discovery does not validate the engine name. engine: python3 builds a
perfectly normal-looking button; clicking it raises
Unknown engine 'python3'. into the output window and nothing runs. If a tool
does nothing but open a window with that message, check this line first.
context: enabling and disabling a button#
Without a context: key a button is always enabled, exactly as before. With
one, the runtime reads a small boolean expression and re-evaluates it against the live
document on every hub event, flipping the button's enabled state to match. It never blocks a
click by itself; it only decides whether the button can be clicked at all.
Seven condition names, answered from one snapshot of the active document:
| Condition | True when |
|---|---|
doc | A document is open with at least one model appended. |
selection | The current selection is not empty. |
clash-tests | Clash Detective has at least one test. |
clash-results | At least one clash test has at least one result. |
viewpoints | The document has at least one saved viewpoint or folder. |
selection-sets | The document has at least one saved selection set. |
multi-model | More than one model is appended to the document. |
Combine names with & (and), | (or) and ! (not).
! binds tightest, then &, then |, and both binary
operators are left-associative. There is no way to write parentheses: ( is not
part of the vocabulary and produces Unexpected character '(' in context rule.
Split the rule across two buttons, or simplify the condition, when a different grouping is
genuinely needed. The word forms and, or and not, and
the doubled && and ||, are not accepted either.
context: doc
context: selection & clash-tests
context: viewpoints | selection-sets
context: selection & !multi-modelA condition name that does not match one of the seven above is legal syntax and evaluates to false forever, but discovery says so rather than staying silent:
Bundle 'D:\NavisTools\...\Export.pushbutton': unknown context condition 'selction'.A rule that fails to parse outright, for instance a stray leading operator, is also logged, and the button falls back to always enabled rather than never enabled:
Bundle 'D:\NavisTools\...\Export.pushbutton': bad context rule - Unexpected '&' in context
rule '& selection'. Button stays always enabled.The seven conditions are read into a set once per hub event, and every gated button on the ribbon is checked against that same set. Gating a hundred buttons costs one pass over the document, not a hundred.
min_host_version and max_host_version: gating on the Navisworks release#
Two keys, each a bare year. Either can be used alone, and neither is required.
title: New clash API demo
min_host_version: 2024A ceiling on its own, for a tool that drives something the host later removed:
title: Legacy clash report
max_host_version: 2025Or both, for a tool that only works inside one window of releases:
title: Bridge export
min_host_version: 2024
max_host_version: 2026An out-of-range button still renders exactly like any other: it is not greyed out and it is not hidden. The check happens on click, before the script runs, and shows a warning toast in place of running it:
Requires Navisworks 2024 or newer (this is 2023).
Requires Navisworks 2026 or older (this is 2027).A version-gated button that can never run on the installed release still looks completely
normal on the ribbon; there is no visual difference until someone clicks it. There is no
context condition for "the running host year" either, so combine
min_host_version/max_host_version with context: when
you want both a greyed-out state and a version floor.
A value that is not a whole number is logged and ignored, leaving that side of the range
open rather than failing the bundle. The test is a plain integer parse, so 2024
works and 2024.1, v2024 and 20.0 do not:
Bundle 'D:\NavisTools\...\Export.pushbutton': min_host_version 'v2024' is not a year - ignored.The host's own year is derived from the Navisworks API assembly version. On the rare
install where that cannot be read, the gate is disabled entirely and every button runs,
because refusing to run on a host you cannot identify is the worse failure. The gate also
covers only the two runs that go through the script executor, a click or a chord: a
*.smartbutton's build-time run and a *.dockpane's
script.py are not version checked.
The keys a pulldown reads#
A *.pulldown folder may carry its own bundle.yaml describing the
menu button itself. It reads three keys: title, tooltip and
keytip. shortcut and engine are ignored there, because
a pulldown does not run anything; only the pushbuttons inside it do.
*.splitbutton and *.splitpushbutton are parsed exactly the same
way and read the identical three keys. Buttons, stacks and
pulldowns covers what makes their header click different from a plain pulldown's.
The keys a urlbutton reads#
A *.urlbutton has no script.py, so engine and
shortcut are not read: there is no code for either of them to apply to. It reads
four keys, one of them required.
| Key | Falls back to | What it does |
|---|---|---|
url | nothing; required | The address opened on click. Missing or blank and the bundle is dropped. |
title | the folder name, prefix stripped | The button caption. |
tooltip | the url value itself |
Hover tooltip. There is no docstring to fall back to. |
keytip | generated from the title | Alt-navigation letters. |
Urlbutton 'D:\NavisTools\...\Documentation.urlbutton' has no url: in bundle.yaml - skipped.The keys a linkbutton reads#
A *.linkbutton also has no script.py. Its required key is
plugin, the target add-in's id in Id.DeveloperId form, the same
identity Navisworks itself uses to find a registered plugin.
| Key | Falls back to | What it does |
|---|---|---|
plugin | nothing; required | The other add-in's plugin id. Missing or blank and the bundle is dropped. |
title | the folder name, prefix stripped | The button caption. |
tooltip | nothing | Hover tooltip. There is no docstring and no fallback. |
keytip | generated from the title | Alt-navigation letters. |
Linkbutton 'D:\NavisTools\...\Legacy_Tool.linkbutton' has no plugin: in bundle.yaml - skipped.The keys a dockpane reads#
A *.dockpane renders as a ribbon toggle rather than a button that runs and
returns, but its bundle.yaml is parsed by the same reader and reads five keys.
There is no width or height key: a pane's size belongs to the slot it
lands in, identical for all five compile-time slots, not to the bundle that borrows one.
| Key | Falls back to | What it does |
|---|---|---|
title | the folder name, prefix stripped | The ribbon toggle's caption. |
tooltip | nothing | Hover tooltip. There is no docstring to fall back to. |
engine | the extension's engine, then
ironpython |
The engine script.py runs under, when the bundle has one. |
keytip | generated from the title | Alt-navigation letters. Assigned to the ribbon toggle exactly like any other button's. |
shortcut | nothing | Parsed and stored, but not wired to anything: ShortcutManager dispatches
through ScriptExecutor.Run, which a dockpane click never goes through.
Treat this key as reserved, not as a working chord, in this release. |
pane.xaml in the bundle folder is required; a folder without one is skipped
(Dockpane '<dir>' has no pane.xaml - skipped.). script.py is
optional, unlike a pushbutton where its absence is what makes the folder invisible.
Buttons, stacks and pulldowns
covers what the toggle does and what __pane__ exposes.
The keys an extension reads#
An *.extension folder may carry extension.yaml, a different
filename read by the same parser. It has two keys.
| Key | Falls back to | What it does |
|---|---|---|
name | The folder name minus .extension |
Identifies the extension and feeds the generated tab ids. |
engine | ironpython |
The default engine for every pushbutton in the extension. A bundle's own
engine still overrides it. |
Folders that read nothing#
Four folder kinds have no configuration file at all, and putting one there is a common way to lose an afternoon.
| Folder | Config file | Title comes from |
|---|---|---|
*.tab | None | The folder name |
*.panel | None | The folder name |
*.stack | None | Nothing: a stack has no visible label |
*.slideout | None | Nothing: the flyout is labelled by the panel |
Stacks are parsed by walking straight to the *.pushbutton folders inside them,
and a slideout is parsed by walking straight to the panel items inside it. Neither folder's own
bundle.yaml is ever opened, and neither is looked at for an icon. If you want a
title on a stacked row, put it in the pushbutton's bundle.yaml, one level down.
There is also no panel.yaml or tab.yaml; rename the folder
instead.
How title and tooltip actually resolve#
Metadata resolves most specific first: bundle.yaml, then the script, then the folder name. The two chains are not the same length, which matters.
Breaking a long caption onto two lines#
A large button is as wide as its caption, so four long names can eat half the ribbon. A
literal \n in the title breaks it:
title: Export\nViewpoints
The break applies to the large vertical caption only: on the ribbon the
button reads Export over Viewpoints, while everywhere the name
appears as text, it stays one line with the break as a single space. That includes the log,
toasts, the output window title, the generated keytip letters, the Shortcuts editor and
__title__ inside the script. Spaces around the break are optional, so
Export \n Viewpoints gives the same two lines.
A stacked or menu row draws its caption beside the icon rather than under it, so those
never wrap: the same bundle used in a .stack shows the one-line form. Use one
break, not several; the ribbon gives a large button room for two lines.
How the script is scanned#
When bundle.yaml does not supply a title or tooltip, the runtime reads
script.py as text and looks for two things with a regular expression and a small
hand-written scan. It is not a Python parser, and the difference shows.
__title__#
The pattern is an assignment to __title__ that starts at column
zero, using a single or double quoted string. That is the entire grammar.
__title__ = 'Export points'
__title__="Export points"
__title__ = 'Export points' # the trailing comment is fine here __title__ = 'Export points' # indented: the pattern is anchored to column zero
__title__ = """Export points""" # triple quotes are not matched
__title__ = 'Export ' + 'points' # only the first literal is taken: "Export "
__title__ = f'Export {kind}' # the f is not a quote, so no match at all
__title__ = TITLES['export'] # no quote after the equals signNothing reports these. The button falls back to the folder name and looks like it ignored your script.
The docstring#
The scan skips blank lines and lines starting with #, then requires the next
content to open with """ or '''. It takes everything up
to the next occurrence of the same delimiter and trims it. If the first real statement is
anything else, there is no docstring, and the search stops there: a triple-quoted string
further down the file is never found.
# Licence header comments are skipped.
"""Exports the current selection to CSV."""
from pynavis import selectionfrom pynavis import selection
"""Exports the current selection to CSV."""A one-line docstring is the cheapest possible tooltip and keeps the description next to the
code it describes. Move to tooltip: when the tooltip and the docstring want to
say different things, or when the docstring is long enough to make an unwieldy tooltip: the
tooltip is a single line either way, so long text just gets awkward.
Keys that do nothing#
Every key in the file is parsed, but only the ones listed above are ever read. Anything else sits in a dictionary that nobody queries. This matters most if you are arriving from pyRevit, because several familiar keys have no equivalent here.
| Key | Status in pyNavis |
|---|---|
author | Not read anywhere. There is no author display. |
min_navis, max_navis, version |
Not read. The real keys are min_host_version and
max_host_version, both a bare year; see above. |
highlight, beta | Not read. There are no badges. |
layout, collapsed, panel background |
Not read. Panel layout comes from folder names and ordering only. |
modules |
Not read. The search path is the bundle folder plus the extension's
lib\. |
help_url, hyperlink |
Not read. Link to your docs from a toast or the output window instead. |
None of these produce a warning, in the log or anywhere else. If a key is not in the tables on this page, assume it does nothing.
A complete example#
# Points exporter. Comments have to live on their own line.
title: Export points
tooltip: Writes every selected item to a CSV file, one row per item.
engine: ironpython
shortcut: Ctrl+Shift+E
keytip: EP