Overview#
The Fusion API is event driven. Rather than polling, you register a handler for an event and Fusion calls it when something happens: a document is opened, the user switches workspaces, a command starts, a selection changes, or a timer you created fires. Nearly all interactive tools, and every add-in that reacts to the user, depend on events.
The mechanism is the same everywhere:
- Pick the event object (for example
app.documentOpened). - Write a class deriving from the matching handler base class and override
notify(self, args). The handler type must match the event type (thecommandStartingevent is anApplicationCommandEvent, so its handler isApplicationCommandEventHandler). - Create an instance and call
event.add(handler). - Keep a reference to the instance so Python does not garbage collect it.
- In
stop, callevent.remove(handler)and release references.
Every event derives from Event, which has add, remove, name and sender. Every EventArgs object has firingEvent, plus event-specific properties. The Python add-in template wraps all this in a small fusion360utils helper (futil.add_handler(event, function)), and the "Syntax" section of each event's reference page shows ready-made handler code in both plain Python and fusion360utils style.
This page catalogues the main events, shows patterns that work, and explains the dangerous parts: re-entrancy, long-running code, and threads.
Where to find it#
The events are properties on the classes that raise them. In the reference, search for Event classes: ApplicationEvent, DocumentEvent, WorkspaceEvent, CommandEvent, CommandCreatedEvent, InputChangedEvent, CustomEvent, HTMLEvent, MouseEvent, KeyboardEvent, ActiveSelectionEvent, UserInterfaceGeneralEvent, DataEvent (for data-panel operations), and WebRequestEvent. Each has a paired handler class named ...EventHandler and an ...EventArgs class passed to notify.
The main events#
Application-level (on Application)#
| Event | Fires when | Args class |
|---|---|---|
documentCreated | A new document is created | DocumentEventArgs |
documentOpening / documentOpened | A document begins/finishes opening | DocumentEventArgs |
documentSaving / documentSaved | Before/after save | DocumentEventArgs |
documentClosing / documentClosed | Before/after close | DocumentEventArgs |
documentActivating / documentActivated | Tab switch | DocumentEventArgs |
documentDeactivating / documentDeactivated | Tab switched away | DocumentEventArgs |
onlineStatusChanged | Network state changes | ApplicationEventArgs (has isOffLine) |
startupCompleted | Fusion finished starting, including startup add-ins | ApplicationEventArgs |
cameraChanged | The camera changed (rotate, zoom, pan, perspective switch, viewport extents) | CameraEventArgs |
mfgdmDataReady | Cloud (MFGDM) metadata such as mfgdmModelId is ready | see the MFGDM topic |
insertingFromURL / insertedFromURL, openingFromURL / openedFromURL, dataFileComplete, dataFileCopyComplete | URL and data-file operations | see the reference |
Note: not confirmed in Autodesk's documentation: an application "exit" event; add-ins learn about shutdown through
stop(context)withIsApplicationClosingtrue.
User interface level (on UserInterface)#
| Event | Fires when |
|---|---|
workspacePreActivate / workspaceActivated | A workspace is about to activate / has activated |
workspacePreDeactivate / workspaceDeactivated | A workspace is about to deactivate / has deactivated |
commandCreated | Any command is created (the per-definition event of the same name is on CommandDefinition) |
commandStarting / commandTerminated | Any built-in or custom command starts/ends; args.commandId identifies it |
activeSelectionChanged | The selection changes |
markingMenuDisplaying | The marking menu is about to show; lets you add entries |
(Custom events are not a UserInterface event: they are registered on the application with app.registerCustomEvent(id), shown below.)
commandStarting and commandTerminated are powerful because they apply to Fusion's own commands. ApplicationCommandEventArgs has commandId, commandDefinition and isCanceled, so you can block a command in commandStarting (use with care); commandTerminated adds terminationReason. The ids are internal strings; discover them by logging args.commandId while you click around. Ids that appear in Autodesk's samples include SelectCommand, SketchCommand, BrepCommand and ScriptsManagerCommand. > Note: not confirmed in Autodesk's documentation: ExtrudeCommand, used in the example below as a placeholder.
Command-level (on Command)#
Covered in the UI page: inputChanged, validateInputs, executePreview, execute, destroy, preSelect, select, mouseClick, keyDown, and others. Command events fire only while that command is active.
Step by step#
1. React when a document is saved#
import adsk.core, adsk.fusion, traceback
_handlers = []
app = adsk.core.Application.get()
ui = app.userInterface
class SavedHandler(adsk.core.DocumentEventHandler):
def __init__(self):
super().__init__()
def notify(self, args):
try:
doc = adsk.core.DocumentEventArgs.cast(args).document
app.log(f'Saved: {doc.name}')
except:
app.log('SavedHandler failed:\n' + traceback.format_exc())
def run(context):
try:
h = SavedHandler()
app.documentSaved.add(h)
_handlers.append(h)
except:
ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))
def stop(context):
try:
for h in _handlers:
app.documentSaved.remove(h)
_handlers.clear()
except:
ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))
Package this as an add-in with Run on Startup and you have a persistent hook that logs every save. Notice the pattern: handler list at module level, add in run, remove in stop, and try/except inside notify so one error does not break the next call.
2. Observe built-in commands#
class CmdTerminated(adsk.core.ApplicationCommandEventHandler):
def __init__(self):
super().__init__()
def notify(self, args):
a = adsk.core.ApplicationCommandEventArgs.cast(args)
if a.commandId == 'ExtrudeCommand':
app.log(f'Extrude ended: {a.terminationReason}')
ui.commandTerminated.add(CmdTerminated())
terminationReason takes values from adsk.core.CommandTerminationReason (CompletedTerminationReason, CancelledTerminationReason for Cancel, AbortedTerminationReason for OK followed by a failed execute, PreEmptedTerminationReason, SessionEndingTerminationReason, UnknownTerminationReason). The handler base class is ApplicationCommandEventHandler for these two events. Note that in this snippet the handler instance is not stored; in real code store it, or it may vanish.
3. Listen to selection changes#
class SelHandler(adsk.core.ActiveSelectionEventHandler):
def __init__(self):
super().__init__()
def notify(self, args):
sels = ui.activeSelections
if sels.count == 1:
e = sels.item(0).entity
app.log('Selected: ' + e.objectType)
ui.activeSelectionChanged.add(SelHandler())
objectType returns the fully qualified class name such as adsk::fusion::BRepFace; the same string appears in ui.selectEntity filters.
4. Custom events and background work#
Fusion's API runs on the main thread, and Autodesk's Threading topic says never to call Fusion API functions from a worker thread (even messageBox can crash Fusion). The API does not create or manage threads; you use your language's threading and a custom event to hand results back to the main thread. If you need to do slow work (a network request, a long computation) without freezing Fusion, run it on a Python thread and fire the custom event when data is ready.
import threading, json, adsk.core, traceback
CUSTOM_ID = 'myTool_dataReady'
_handlers = []
_thread = None
_stop_flag = threading.Event()
class DataReady(adsk.core.CustomEventHandler):
def __init__(self):
super().__init__()
def notify(self, args):
try:
payload = json.loads(adsk.core.CustomEventArgs.cast(args).additionalInfo)
adsk.core.Application.get().userInterface.messageBox('Got: ' + str(payload))
except:
adsk.core.Application.get().log(traceback.format_exc())
def worker(app):
n = 0
while not _stop_flag.wait(2.0): # sleep 2 s or stop
n += 1
app.fireCustomEvent(CUSTOM_ID, json.dumps({'tick': n})) # safe from threads
def run(context):
global _thread
app = adsk.core.Application.get()
ev = app.registerCustomEvent(CUSTOM_ID)
h = DataReady()
ev.add(h)
_handlers.append(h)
_thread = threading.Thread(target=worker, args=(app,), daemon=True)
_thread.start()
def stop(context):
app = adsk.core.Application.get()
_stop_flag.set()
if _thread:
_thread.join(timeout=3)
app.unregisterCustomEvent(CUSTOM_ID)
_handlers.clear()
Application.fireCustomEvent(eventId, additionalInfo) is the documented way for a worker thread (or another add-in, since events are identified by name) to reach the add-in. The event is queued and handled on the main thread "when Fusion is idle", not immediately. additionalInfo is a string. Do not touch Design or Sketch objects from the worker. Two further points from the Threading topic: if the handler will create or modify the document, first terminate any running command (if ui.activeCommand != 'SelectCommand': ui.commandDefinitions.itemById('SelectCommand').execute()), and in debugging, use something like ctypes.windll.user32.MessageBoxW (Windows) rather than ui.messageBox from the worker. Autodesk's two samples use this pattern: a timer thread that updates a user parameter, and a command dialog table filled from a worker. adsk.doEvents() is the other way to give Fusion time, but the topic says it is not appropriate in a long-running loop.
5. Avoid re-entrancy#
General guidance, not Autodesk data: an event handler that changes the document may trigger events itself, so guard against re-entry. Use a flag:
_busy = False
def notify(self, args):
global _busy
if _busy:
return
_busy = True
try:
... # work
finally:
_busy = False
Also avoid long modal dialogs inside high-frequency handlers such as activeSelectionChanged.
More patterns#
Reacting to workspace changes#
Add-ins often need to react to the workspace. Subscribe to ui.workspaceActivated, read the workspace from the event args (adsk.core.WorkspaceEventArgs.cast(args).workspace.id) and compare with the ids you care about. Palettes need special care: Fusion deletes all palettes when the workspace changes, so re-create yours here (or delete it in workspacePreActivate). The event is also useful for enabling or disabling commands, or warning the user that a tool only works in the Design workspace.
Validating inputs#
validateInputs is an event on Command. Fusion already does basic validation (a selection input needs a selection; a value input with an unparsable string such as 5 mmm turns red and disables OK). Set args.areInputsValid = False for rules unique to your command to disable the OK button. Use it to enforce relationships such as "inner diameter must be smaller than outer diameter":
class Validate(adsk.core.ValidateInputsEventHandler):
def __init__(self):
super().__init__()
def notify(self, args):
inputs = args.inputs
od = inputs.itemById('od').value
idia = inputs.itemById('id').value
args.areInputsValid = idia < od
Keep this function fast and free of side effects: it is called constantly, and you must not create or modify document geometry inside it.
Marking-menu and context additions#
The markingMenuDisplaying event on UserInterface supplies a MarkingMenuEventArgs with linearMarkingMenu, radialMarkingMenu and selectedEntities. You can add your own command definition at the moment the menu opens and filter by what is selected. General guidance: because the menu is rebuilt every time, add controls only inside the handler and do not cache them.
Order and cancellation#
Events named with a present participle (documentSaving, documentClosing, commandStarting) fire before the action; past-tense events fire after the work is done. Cancellation is narrow: ApplicationCommandEventArgs.isCanceled cancels a command in commandStarting, and DocumentEventArgs.isOperationCancelled (with cancelReason for the message) is documented as "only supported for the documentSaving event". Do not rely on cancelling any other document event. Note also that DocumentEventArgs.document "can be null" before a document has opened or after it has closed.
Logging pattern#
Because event code is hard to step through, a tiny logger helps (this is general guidance; app.log also works):
import os, datetime
LOG = os.path.join(os.path.expanduser('~'), 'fusion_addin.log')
def log(msg):
with open(LOG, 'a', encoding='utf-8') as fh:
fh.write(f'{datetime.datetime.now().isoformat()} {msg}\n')
Call it at the start of each notify. Writing to a file survives crashes that would lose the Text Commands window contents, so you can see the last event before a failure.
Options reference#
| Handler base class | Used for | Args cast |
|---|---|---|
adsk.core.CommandCreatedEventHandler | commandCreated | CommandCreatedEventArgs |
adsk.core.CommandEventHandler | execute, executePreview, destroy | CommandEventArgs |
adsk.core.InputChangedEventHandler | inputChanged | InputChangedEventArgs |
adsk.core.ValidateInputsEventHandler | validateInputs | ValidateInputsEventArgs |
adsk.core.DocumentEventHandler | document events | DocumentEventArgs |
adsk.core.WorkspaceEventHandler | workspaceActivated, workspaceDeactivated, workspacePreActivate, workspacePreDeactivate | WorkspaceEventArgs |
adsk.core.ApplicationEventHandler | onlineStatusChanged, startupCompleted | ApplicationEventArgs |
adsk.core.SelectionEventHandler | command preSelect/select | SelectionEventArgs (selection, isSelectable) |
adsk.core.UserInterfaceGeneralEventHandler | palette closed and similar | UserInterfaceGeneralEventArgs |
adsk.core.ApplicationCommandEventHandler | commandStarting/commandTerminated | ApplicationCommandEventArgs |
adsk.core.ActiveSelectionEventHandler | selection changes | ActiveSelectionEventArgs |
adsk.core.CustomEventHandler | custom events | CustomEventArgs |
adsk.core.HTMLEventHandler | palette messages | HTMLEventArgs |
adsk.core.MouseEventHandler, KeyboardEventHandler | canvas input | MouseEventArgs, KeyboardEventArgs |
event.add(handler) connects a handler and event.remove(handler) disconnects it. TypeScript uses plain objects with a notify method instead of handler classes, and Autodesk says handlers left registered are cleaned up automatically when a TypeScript add-in stops; for Python, remove them yourself in stop.
Tips and pitfalls#
Tip: Wrap every
notifybody intry/exceptand send failures toapp.logor a message box, as Autodesk's samples do, because exceptions in event handlers are easy to lose.
Tip: Write a small utility to register handlers and store them in one place, such as
def add_handler(event, handler_cls, *args)that creates the instance, callsevent.add, appends to a global list, and returns it. It cuts boilerplate and prevents the dropped-reference bug.
Warning: Python garbage collection will destroy a handler that has no reference, and the event then silently stops. Keep a module-level list.
Warning: Never call the Fusion API from a worker thread. Use
fireCustomEventto hop to the main thread. Autodesk says that evenmessageBoxfrom a worker can crash Fusion.
Warning: Do not leave handlers registered after
stop(Python). General guidance: reloading an add-in without removing the old handler can lead to duplicated callbacks.
Warning: Events such as
commandStartinginclude Fusion's internal commands, which fire often; keep the handler fast and filter early oncommandId.
Practice exercise#
Write an add-in that logs the name and time of every document opened, appending to a text file in the user's home folder. Register documentOpened, write a line in notify using datetime.now().isoformat(), and remove the handler in stop. Test it by opening and closing two documents. Expected result: two lines in the log file; after stopping the add-in and opening another document, no new line appears.
Stretch: add a custom event fired from a background timer thread every 10 seconds that updates a palette's text with the count of open documents.
Related#
- UI: commands, toolbars, palettes
- Debugging in VS Code
- Packaging and distributing add-ins
- Fusion API overview