Scripts

pyacad.exe and headless runs

The pyacad command line: check an install, attach the loader, register extension folders, and run a script over a list of drawings in AutoCAD Core Console.

pyacad.exe is installed with pyACAD, in %APPDATA%\pyACAD\cli. That folder is not on your PATH, so call the exe by its full path or add the folder yourself. Run it with no arguments, or an unknown verb, and it prints its usage and exits with code 2.

text
pyacad - pyACAD command line

  pyacad env                     show detected AutoCAD versions and state
  pyacad attach <year>           put the loader in the per-user bundle (2021-2026)
  pyacad detach <year>           remove the loader from the bundle
  pyacad extensions list         show configured extension roots
  pyacad extensions add <dir>    add an extension root to config.json
  pyacad run <script.py> [drawings]  run a script in AutoCAD Core Console (pyacad run --help)
  pyacad protect-extension ...   (the installer's) back up and move your own work
                                  out of pyACAD.extension before an update

pyacad env#

Prints where pyACAD keeps its settings, its builds and its Autodesk bundle, and the state of every supported AutoCAD year: not installed, installed, not attached or installed, pyACAD ATTACHED. Then every extension root listed in config.json. It is the first thing to run when the ribbon does not appear.

text
pyACAD environment
  config : C:\Users\you\AppData\Roaming\pyACAD\config.json
  builds : C:\Users\you\AppData\Roaming\pyACAD
  bundle : C:\Users\you\AppData\Roaming\Autodesk\ApplicationPlugins\pyACAD.bundle
  AutoCAD 2021 : not installed (no build)
  ...
  AutoCAD 2026 : installed, pyACAD ATTACHED (no build)
  extension root : D:\CadTools

A year counts as installed when AutoCAD's install folder for it holds acad.exe. The built or no build at the end of each line is about a development build folder and reads no build on an installed copy; that is expected.

pyacad attach and detach#

pyacad attach 2026 copies a loader built for that year into %APPDATA%\Autodesk\ApplicationPlugins\pyACAD.bundle\Contents\2026\ and registers it in the bundle's PackageContents.xml, with LoadOnAutoCADStartup="True" and the release series AutoCAD matches on (R25.1 for 2026). pyacad detach 2026 removes that year's folder and its entry, and deletes the bundle when no year is left.

The setup program does all of this for you, so on an installed copy you will not normally need either verb. attach copies from a build folder beside the command line and says No pyACAD build for 2026 when there is none; it is mainly a developer tool. It refuses a year whose AutoCAD is not installed.

pyacad extensions#

powershell
pyacad extensions add D:\CadTools
pyacad extensions list

add takes the folder that contains your *.extension folders, makes it absolute, and appends it to the "extensions" array in %APPDATA%\pyACAD\config.json. It refuses a folder that does not exist and says Already configured for one that is listed. Press Reload in AutoCAD afterwards. list prints the array; the two default roots (%APPDATA%\pyACAD\extensions and %PROGRAMDATA%\pyACAD\extensions) are scanned without being listed and do not appear.

pyacad run#

Some jobs are the same script over a folder of drawings: purge and audit each one, export its block attributes, check every layer name against a standard. pyacad run does that without the AutoCAD window. For each drawing it starts AutoCAD Core Console (accoreconsole.exe, the command-line AutoCAD that ships with every AutoCAD), opens the drawing, loads the pyACAD loader, runs your script once and exits. One process per drawing, one after another.

Available in this release

The project README still says pyacad run is not available yet. It is: the command, the Core Console runner and results.json are all in the code and were exercised against AutoCAD 2026.

powershell
pyacad run C:\Jobs\cleanup.py "C:\Jobs\Level 1.dwg" "C:\Jobs\Level 2.dwg" --save --timeout 300

pyacad run --help prints the usage:

text
pyacad run <script.py> [model ...] [options] [-- script args]

  --models <list.txt>    one model path per line, added to the positional ones
  --version <year>       AutoCAD year; default is the newest attached one
  --save                 save each drawing back to its own path after the script
  --show                 show the AutoCAD 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

Arguments#

  • The first plain word is the script, every later plain word a drawing. Relative paths resolve against the current folder.
  • --models list.txt adds one drawing per line, after the positional ones. Blank lines and lines starting with # are skipped. Relative paths in it also resolve against the current folder, not the list file's. Duplicates are kept: a drawing listed twice runs twice.
  • Everything after -- goes to the script untouched, as __args__.
  • --version 2025 picks the AutoCAD year. Without it, the newest year that is both installed and attached is used.
  • --save saves each drawing back to its own path, but only when the script succeeded on it. A script that raises leaves the file untouched.
  • --timeout 300 limits each drawing's whole Core Console process (open, run, save) to that many seconds. Without it the run waits as long as it takes.
  • --results path.json copies the run summary there as well.
  • --show is accepted but has no effect: Core Console has no window to show.

With no drawing at all, the script runs once in a Core Console that opens a blank drawing of its own. An unknown option, an option missing its value, a --version or --timeout that is not a whole number, or a script that does not exist stops the command before anything starts.

What happens per drawing#

pyacad.exe accoreconsole.exe, one per drawing Write request-N.json and run-N.scr script, drawing, args, run folder, save flag /i drawing /s run-N.scr /l en-US opens the drawing, then plays the script _.NETLOAD "...\PyAcad.dll" _.PYACADRUN "request-N.json" startup.py once, your script, the save Read response-N.json status, duration, error, __result__ Then the next drawing, in a fresh process. results.json is written once, at the end.
The command line and the add-in never talk directly: they trade a request file and a response file through the run folder.

Core Console boots the pyACAD loader the same way AutoCAD does, but builds no ribbon: there is nobody to click it. So no bundle commands, no hooks, no dock panes, no keyboard shortcuts, no notices and no update check. Every extension's startup.py still runs, once per process, before your script, with __runner__ set to True. The script runs on IronPython; engine: does not apply to a bare .py file.

Core Console is not AutoCAD with the window hidden

It loads the database and command line half of AutoCAD only. The pyacad.doc, ents, layers, blocks, xrefs, layouts, audit and units modules are written to work there; anything that needs the AutoCAD window (the ribbon, palettes, the application menu) does not exist. Test a headless script on one drawing before you point it at three hundred.

What the script sees#

The runner sets these names, on top of __file__, __commandpath__ (the script's folder), __title__ (the file name without .py), __pyacad__ and __selfinit__ (False):

NameTypeWhat it holds
__runner__bool True. False everywhere else.
__model__str or None Full path of the drawing open for this run, or None for a run with no drawing.
__models__sequence of str Every drawing in the run, in order. A .NET string array: iterate it, or wrap it in list().
__args__sequence of str The words after --, also a .NET string array. script.get_args() gives you a Python list.
__run_dir__str The run folder: the natural place for files the script exports.

Set __result__ to anything json can serialise (a dict, list, string, number or bool) and it lands in results.json beside that drawing's status. It is read only when the script succeeded or ended with a clean sys.exit(); a value json refuses is dropped, with a line in the log.

layer_report.py
"""Writes each drawing's layer names to the run folder; flags non-standard ones."""
import json
import os

from pyacad import layers, script

prefixes = script.get_args() or ['A-', 'M-', 'E-']
names = layers.names()
odd = [n for n in names if n != '0' and not n.startswith(tuple(prefixes))]

stem = os.path.splitext(os.path.basename(__model__))[0]
with open(os.path.join(__run_dir__, stem + '.json'), 'w') as fh:
    json.dump(names, fh)

__result__ = {'layers': len(names), 'nonStandard': odd}
powershell
pyacad run layer_report.py --models drawings.txt --timeout 120 -- A- M- E- S-

Library code that cannot see the script's globals asks script.is_runner(), which returns the same flag.

Output and dialogs#

Nobody can see a Core Console, so whatever a script would show goes to that drawing's transcript file instead, and whatever would wait for an answer raises:

  • print() and pyacad.output write to the transcript. HTML lands as its source, an element link as its label, errors start with ERROR: , and progress writes nothing.
  • pyacad.toast, pyacad.banner and pyacad.forms.alert write one line each, such as [success] Purged 14 items.
  • forms.confirm, ask_string, ask_number, ask_options, select_from_list, pick_folder, save_file, open_file and forms.WPFWindow.show_dialog() raise InvalidOperationException: Dialogs are not available under pyacad run. Check __runner__ and skip the prompt.
Command line prompts are not guarded

sel.pick_one(), sel.pick_many(), sel.pick_point() and any other Editor prompt are not stopped under the runner. In Core Console they wait for input that never comes, and the process sits there until --timeout ends it. Take the answer from __args__ when __runner__ is true, and always give a headless run a timeout.

python
from pyacad import forms, script

if script.is_runner():
    prefix = (script.get_args() or ['A-'])[0]
else:
    prefix = forms.ask_string('Layer prefix:', 'A-', 'Layer report')

The run folder#

Each run gets a folder named after the time it started:

folders
%APPDATA%\pyACAD\runs\<yyyyMMdd-HHmmss>\
  request-1.json      what the command line asked for, one per drawing
  run-1.scr           the two-line script Core Console played
  console-1.txt       everything Core Console printed (NETLOAD, PYACADRUN, errors)
  transcript-1.txt    everything the script wrote
  response-1.json     what the add-in answered
  results.json        the summary of the whole run

Two runs started in the same second share a folder and overwrite each other's numbered files; start them a second apart.

results.json holds the script, the AutoCAD year, the start and finish time, and one entry per drawing: its path, status, how long the script took, the last line of any error, and the script's __result__ as JSON (not as a string).

results.json
{
  "script": "C:\\Jobs\\layer_report.py",
  "year": "2026",
  "startedAt": "2026-10-15T09:30:00.0000000+01:00",
  "finishedAt": "2026-10-15T09:31:10.0000000+01:00",
  "models": [
    {
      "model": "C:\\Jobs\\Level 1.dwg",
      "status": "ok",
      "durationMs": 1840,
      "error": null,
      "result": { "layers": 42, "nonStandard": ["Defpoints", "temp"] }
    }
  ]
}
StatusMeans
okThe script succeeded, and so did the save when --save was given.
failedThe script raised (or exited with a message or a non-zero code). The drawing was not saved.
open-failedNo response came back: the drawing would not open, the loader did not load, or Core Console could not start. The error points at console-N.txt.
save-failedThe script succeeded but the save did not: the drawing is read-only, was never saved, or the save threw.
save-skippedThe script succeeded and --save was given, but the run had no drawing to save.
timed-outThe process took longer than --timeout and was ended. The next drawing still runs.
cancelledCtrl+C landed while this drawing's process was running.
not-runThe run was cancelled before it reached this drawing.

The console shows the same story as it happens: a header naming the script, the year and the run folder, then one line per drawing such as [2/5] Level 2.dwg ... ok or ... failed: NameError: name 'layres' is not defined, and a closing Done: line.

Saving#

--save writes the drawing back to the path it was opened from, in the current DWG format, after a script that succeeded. A drawing in an older DWG format is upgraded by the save. Read-only drawings and drawings with no file are reported as save-failed, not written somewhere else. To save under another name or format, do it in the script and leave --save off.

Exit codes#

CodeMeans
0Every drawing is ok.
1At least one drawing is not ok (including save-skipped), or the command itself hit an error.
2Usage: an unknown option, a missing value, a script that does not exist, no attached AutoCAD, an unsupported or uninstalled year, no accoreconsole.exe, or no pyACAD loader for that year.
3Cancelled with Ctrl+C.

A drawing that fails does not stop the run: the next one starts in a fresh process. Ctrl+C ends the current drawing's process, marks it cancelled and every later drawing not-run, and still writes results.json.

Limits#

  • One drawing per process, sequentially, so every drawing pays Core Console's start-up time.
  • The script always runs on IronPython.
  • No bundle commands exist in Core Console, so the script cannot run a ribbon tool by name. Put shared code in a *.lib folder and import it.
  • The same run gate as on the ribbon applies: one pyACAD script at a time per process.