Scripts
Running without the ribbon
pynavis run starts a hidden Navisworks, runs one script once per model, and writes down what happened.
Some jobs are the same script over a list of models: export the clash counts of every
model in a folder, check each one for a property, save each one after a fix. Clicking a
button in every model is the slow way to do that. pynavis run does it from a
command prompt instead.
The script is an ordinary pyNavis script. It imports pynavis, reads the open
document and calls the Navisworks API the way a button's script.py does. A few
things are different under the runner, and this page covers all of them.
The command#
pynavis run script.py model.nwdpynavis.exe is installed with pyNavis, in
%APPDATA%\pyNavis\cli. That folder is not on your PATH, so call the
exe by its full path or add the folder yourself. pynavis run --help prints the
usage:
pynavis run <script.py> [model ...] [options] [-- script args]
--models <list.txt> one model path per line, added to the positional ones
--version <year> Navisworks year; default is the newest attached one
--save save each .nwd or .nwf model back to its own path after the script
--show show the Navisworks window while each script runs
--timeout <seconds> give up on a model after this long; default none
--results <file.json> copy the run summary here as wellThe first plain word is the script and every later plain word is a model. Everything
after -- goes to the script untouched. A --models list file holds
one model path per line; relative paths in it resolve against the list file's own folder,
and blank lines and lines starting with # are skipped. With no model at all
the script runs once, with no document open.
pynavis run export_counts.py --models models.txt --timeout 600 -- Ducts PipesHere is what happens. The command starts a hidden Navisworks of the chosen year through
Autodesk's Automation API. It opens each model in turn in that one instance, runs the script
once per model, and closes Navisworks at the end. --save saves each model back
to its own path, only after the script succeeded on it, and only when it is a
.nwd or .nwf file. Any other model (an .nwc, an
.ifc, a native CAD file) is not saved: its status is save-skipped,
the console says save skipped: Only .nwd and .nwf files are saved back., and it
does not fail the run.
With --show the window appears once a model is open and stays up while the
script runs, then hides again before the next model opens. The order is deliberate: opening a
model into a Navisworks window that is already on screen ends the Navisworks process, measured
on Navisworks 2026, so every model opens hidden.
Navisworks shows first-run dialogs the first time it starts. A hidden Navisworks cannot show them to you, so start it once from the Start menu and close those dialogs before your first run.
What the script sees#
The runner sets these names in the script's scope, on top of the usual
__file__, __commandpath__ (the script's folder),
__title__ (the script's file name without its extension),
__pynavis__ and __selfinit__, which is False:
| Name | Type | What it holds |
|---|---|---|
__runner__ | bool | True under the runner and False on the ribbon, in hooks
and in panels. startup.py sees True only when the runner
starts it. |
__model__ | str or None | The full path of the model open for this run, or None when the run has
no model. |
__models__ | sequence of str | Every model path in the run, in order. It is a .NET string array: iterate it, or
wrap it in list(). |
__args__ | sequence of str | The words after -- on the command line, also a .NET string
array. |
__run_dir__ | str | The run folder. It is the natural place for files the script exports. |
A script may set __result__ to any JSON-friendly value: a dict, a list, a
string, a number or a bool. The runner reads it after the script finishes and writes it into
results.json beside that model's status. It is read only when the script
succeeded or ended with a clean sys.exit. A value that json cannot
serialise is dropped, with a line in the pyNavis log.
"""Writes the number of items in each clash test to the run folder."""
import json
import os
from pynavis import clash
counts = {}
for test in clash.walk_tests():
total, _grouped = clash.count_results(test)
counts[test.DisplayName] = total
name = os.path.splitext(os.path.basename(__model__))[0] + '.json'
with open(os.path.join(__run_dir__, name), 'w') as fh:
json.dump(counts, fh)
__result__ = countsLibrary code that has no access to the script's globals asks
pynavis.script.is_runner(), which returns the same flag.
Output and dialogs under the runner#
Nobody is watching a hidden Navisworks, so the runner writes what a script would show to a transcript file, and refuses what would wait for an answer:
pynavis.toastlines,pynavis.bannermessages andpynavis.forms.alertmessages are written to the transcript.pynavis.outputandprintwrite to the transcript instead of opening an output window. HTML lands as its source, an element link becomes its label, errors start withERROR:, and progress writes nothing.- The
pynavis.formsprompts and pickers raise instead of waiting for an answer:forms.confirm,ask_string,ask_length,select_from_list,ask_options,ask_number,pick_folder,save_fileandopen_file. So doforms.WPFWindow.show_dialog()and everypynavis.pickentry point (pick.point,point_then,measure_pointandmeasure_points): a pick would put the hidden Navisworks on a pick tool and wait for a click nobody can give.
The error those dialogs raise is an InvalidOperationException that reads
“Dialogs are not available under pynavis run. Check __runner__ and skip the
prompt.” That is the fix: a script that runs both from the ribbon and from the runner
takes its answers from __args__ under the runner and asks only on the
ribbon.
from pynavis import forms
if __runner__:
names = __args__
else:
names = [forms.ask_string('Which systems?', '', 'Systems')]The Clash Grouper, the viewpoint tools and Settings open windows of their own, and those
do not raise under the runner. A hidden run that reaches one blocks on it. These tools are
made for the ribbon: guard any call into them with __runner__, and use
--timeout so a run that blocks anyway ends.
Results#
Each run gets a folder of its own, named after the time it started:
%APPDATA%\pyNavis\runs\<yyyyMMdd-HHmmss>\
.owner # claims the folder, so two runs in the same second never share one
request-1.json # what the runner asked for, numbered per model
response-1.json # what the add-in answered
transcript-1.txt # everything the script wrote
results.json # the summary of the whole runA second run that starts in the same second gets the folder name with -2,
-3 and so on after it.
results.json holds the script, the Navisworks year, the start and end time,
and one entry per model: the model path, its status, how long the script took, the last line
of the error, and the script's __result__. The status is one of these:
| Status | Means |
|---|---|
ok | The script succeeded, and the save too when
--save was given. |
failed | The script raised, or the add-in gave no answer. |
open-failed | Navisworks could not open the model. |
save-failed | The script succeeded but the save did not, or Ctrl+C ended a save that had not finished. |
save-skipped | The script succeeded, and --save left the
model alone because it is not a .nwd or .nwf file. It does not
fail the run. |
timed-out | A step took longer than --timeout. |
cancelled | Ctrl+C landed before or while the model was opening or its script was running. Its error reads “Cancelled while the script was running.”, “Cancelled while the model was opening.”, “Cancelled before the script ran.” or “Cancelled before the model opened.” |
not-run | The run ended (a timeout or Ctrl+C) before it reached this model. Its error reads “Not run: the run ended after an earlier model.” |
--results copies the same file to a path you choose.
{
"script": "C:\\Jobs\\export_counts.py",
"year": "2026",
"startedAt": "2026-10-04T09:30:00+01:00",
"finishedAt": "2026-10-04T09:31:10+01:00",
"models": [
{
"model": "C:\\Jobs\\Level 1.nwd",
"status": "ok",
"durationMs": 41200,
"error": null,
"result": { "Ducts vs Pipes": 12 }
}
]
}The console shows the same story as it happens. Each model starts with an
Opening line; a run with no model has none. Then comes one line per model:
ok with the time in seconds, FAILED: or could not open:
with the last line of the error, or Timed out after N s. with a hint. That
model's transcript follows, indented two spaces. With --save a
could not save:, save skipped: or not saved: cancelled
line can come after it. The last
line is Summary: and the path of results.json. Just before it can
come Cancelled., and the lines about closing Navisworks described under
Failures and timeouts.
Failures and timeouts#
The exit code tells a batch file or a scheduled task how the run went:
| Code | Means |
|---|---|
0 | Every model was ok (or
save-skipped). |
1 | A script failed, a model failed to open, a save failed, a model timed out, or the run was cancelled with Ctrl+C. |
2 | A usage error: a missing script, an unknown option, an option without its value. |
3 | Navisworks of that year is not installed, pyNavis is not
attached to it (the message says to run pynavis attach <year>), or
Navisworks failed to start. |
A script that fails on one model does not stop the run: the next model opens and the
failure is recorded in results.json.
A timeout is different. --timeout applies on its own to each step of a
model: opening it, running the script and saving it. When one step takes longer, the runner
closes the hidden Navisworks and ends the run there. The message names the two usual causes:
a dialog the script did not guard with __runner__, or a first-run dialog on a
fresh install. Run again with --show to see which one it is. Without
--timeout the runner waits as long as the script takes. The models after the
one that timed out are listed in results.json as not-run.
Ctrl+C closes the hidden Navisworks, ends the run with exit code 1 and still writes
results.json, which still lists every model: the one whose script was running
is cancelled, and the models after it are not-run. A save is
different, because a save cut off
halfway can leave a broken file: the runner says it is waiting for the save, then waits up
to --timeout seconds for it, or 60 seconds without --timeout. A save
that finishes keeps its status. One that does not is save-failed, with the error
“Cancelled during the save; the file may be incomplete.”, and only then does
Navisworks close. A Ctrl+C that lands after the script succeeded but before its save began
never starts the save: the model keeps ok, its error reads “Cancelled
before the save; the model was not saved.”, and the console says not saved:
cancelled. A second Ctrl+C ends the command at once.
When the run closes Navisworks and that Navisworks is still running a few seconds later
(or closing it hangs or fails), the runner ends the Roamer.exe process it
started and says so: Closed Navisworks (process N) that did not exit on its
own. It only ever ends a process that started with the run, never a Navisworks that
was already open. Once the runner add-in has answered, the runner knows that process by its
id and ends no other. A close that fails outright first prints Closing Navisworks
failed: and the reason. When the runner cannot end the process, it prints
“Navisworks did not close. Close it from Task Manager (Roamer.exe).”
instead.
Limits#
- Each run uses a Navisworks Manage licence seat while it runs.
startup.pyof every extension runs once, inside the runner, before the first script, and sees__runner__asTrue. Its output and toasts go to the pyNavis log, and its dialogs raise.- By default, a Navisworks started by
pynavis runbuilds no pyNavis ribbon, with or without--show, so hooks and the before and after command events never run underpynavis run. Dock panels, keyboard shortcuts, upgrade notices and the update check are left out too. - Only one script runs at a time, as on the ribbon: the usual run gate applies.
- The script is a bare
.pyfile, sobundle.yamlkeys, includingengine:, do not apply. It runs on the default engine, IronPython.
To boot those stages anyway, set automatedBoot to "full" in
%APPDATA%\pyNavis\config.json. A Navisworks that pynavis run starts
then builds the ribbon, hooks, dock panels, shortcuts, upgrade notices and the update check
exactly as a normal session does. Any other value, or no key at all, keeps the default and
skips them. It is an escape hatch for a script that needs a side effect of the ribbon session,
or for finding out why a script behaves differently on the ribbon and under the runner. With it
on, hooks run during the automated session, and the update check may download an
installer.
{
"automatedBoot": "full"
}