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.
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 updatepyacad 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.
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:\CadToolsA 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#
pyacad extensions add D:\CadTools
pyacad extensions listadd 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.
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.
pyacad run C:\Jobs\cleanup.py "C:\Jobs\Level 1.dwg" "C:\Jobs\Level 2.dwg" --save --timeout 300pyacad run --help prints the usage:
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 wellArguments#
- The first plain word is the script, every later plain word a drawing. Relative paths resolve against the current folder.
--models list.txtadds 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 2025picks the AutoCAD year. Without it, the newest year that is both installed and attached is used.--savesaves each drawing back to its own path, but only when the script succeeded on it. A script that raises leaves the file untouched.--timeout 300limits 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.jsoncopies the run summary there as well.--showis 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#
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.
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):
| Name | Type | What 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.
"""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}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()andpyacad.outputwrite to the transcript. HTML lands as its source, an element link as its label, errors start withERROR:, and progress writes nothing.pyacad.toast,pyacad.bannerandpyacad.forms.alertwrite 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_fileandforms.WPFWindow.show_dialog()raiseInvalidOperationException: Dialogs are not available under pyacad run. Check __runner__ and skip the prompt.
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.
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:
%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 runTwo 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).
{
"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"] }
}
]
}| Status | Means |
|---|---|
ok | The script succeeded, and so did the save when
--save was given. |
failed | The script raised (or exited with a message or a non-zero code). The drawing was not saved. |
open-failed | No 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-failed | The script succeeded but the save did not: the drawing is read-only, was never saved, or the save threw. |
save-skipped | The script succeeded and --save was
given, but the run had no drawing to save. |
timed-out | The process took longer than --timeout
and was ended. The next drawing still runs. |
cancelled | Ctrl+C landed while this drawing's process was running. |
not-run | The 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#
| Code | Means |
|---|---|
0 | Every drawing is ok. |
1 | At least one drawing is not ok (including
save-skipped), or the command itself hit an error. |
2 | Usage: 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. |
3 | Cancelled 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
*.libfolder and import it. - The same run gate as on the ribbon applies: one pyACAD script at a time per process.