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#

powershell
pynavis run script.py model.nwd

pynavis.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:

text
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 well

The 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.

powershell
pynavis run export_counts.py --models models.txt --timeout 600 -- Ducts Pipes

Here 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.

A fresh Navisworks install needs one normal start first

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:

NameTypeWhat 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.

export_counts.py
"""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__ = counts

Library 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.toast lines, pynavis.banner messages and pynavis.forms.alert messages are written to the transcript.
  • pynavis.output and print write to the transcript instead of opening an output window. HTML lands as its source, an element link becomes its label, errors start with ERROR: , and progress writes nothing.
  • The pynavis.forms prompts 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_file and open_file. So do forms.WPFWindow.show_dialog() and every pynavis.pick entry point (pick.point, point_then, measure_point and measure_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.

python
from pynavis import forms
if __runner__:
    names = __args__
else:
    names = [forms.ask_string('Which systems?', '', 'Systems')]
The shipped tools' own windows are not guarded

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:

folders
%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 run

A 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:

StatusMeans
okThe script succeeded, and the save too when --save was given.
failedThe script raised, or the add-in gave no answer.
open-failedNavisworks could not open the model.
save-failedThe script succeeded but the save did not, or Ctrl+C ended a save that had not finished.
save-skippedThe script succeeded, and --save left the model alone because it is not a .nwd or .nwf file. It does not fail the run.
timed-outA step took longer than --timeout.
cancelledCtrl+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-runThe 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.

config.json
{
  "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:

CodeMeans
0Every model was ok (or save-skipped).
1A script failed, a model failed to open, a save failed, a model timed out, or the run was cancelled with Ctrl+C.
2A usage error: a missing script, an unknown option, an option without its value.
3Navisworks 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.py of every extension runs once, inside the runner, before the first script, and sees __runner__ as True. Its output and toasts go to the pyNavis log, and its dialogs raise.
  • By default, a Navisworks started by pynavis run builds no pyNavis ribbon, with or without --show, so hooks and the before and after command events never run under pynavis 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 .py file, so bundle.yaml keys, including engine:, 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.

%APPDATA%\pyNavis\config.json
{
  "automatedBoot": "full"
}