A script that adds a boolean property to every taught PTP statement is a few lines of code. The same few lines behave completely differently depending on where they live. Attached to a robot as a Python Behavior, the module is imported when the component loads and the handler binds. Dropped as a loose .py file into the My Commands folder, nothing happens at all: no error, no output, no checkbox on the statement. The number that matters here is not a line of code, it is a moment in time — when the module is imported, and what the application state looks like at that instant.
Fixes That Do Not Load the Command
Four attempts show up repeatedly before the real cause surfaces, and each fails for a different reason.
Copying the .py file into My Commands and restarting. The add-on loader walks the command directories looking for packages, not scripts. A bare module sitting in the folder is never imported, so the interpreter never touches it. This is the single most common cause of the symptom.
Re-testing the code as a Python Behavior to prove it works. It does work, and that result is misleading. A behavior runs inside the component's own load sequence with a different lifetime and a different scope. Passing as a behavior tells you the API calls are correct; it tells you nothing about whether the command was registered.
Adding print statements or wrapping the body in try/except. Nothing appears in the output panel because the exception path is never reached — the module is not executing. Silence in the console is the diagnostic: an add-on that fails during import throws a visible traceback, an add-on that was never discovered throws nothing.
Guarding the binding with a TeachContext null check at module scope. This one is worse than useless. Module-level code runs exactly once, at load. At that moment app.TeachContext is typically None and ActiveRobot is None, because no robot has been selected and teach mode has not been entered. The guard evaluates false, the entire binding block is skipped, and OnStatementAdded is never assigned. The add-on loads cleanly and does nothing forever.
Load Order and Event Lifetime
Two independent conditions must both be true for the checkbox to appear. First, the application has to know the add-on exists. Second, the statement handler has to be bound to the routine that is actually open in the program editor at the time the operator teaches a point.
Discovery is a filesystem question. The loader treats a subfolder of the commands directory as a Python package and imports it, which requires an __init__.py file in that folder. Without it, the folder is invisible.
Binding is a lifetime question. executor.Program.MainRoutine resolves to a specific routine object belonging to a specific robot's program. Select a different robot, load a different program, or open a subroutine, and the object your handler was attached to is no longer the one receiving statements. Because the module body only ever ran once, there is no code path that re-resolves it. The handler stays bound to whatever existed at load time — usually nothing.
| Item | Requirement / behavior | Where to read it |
|---|---|---|
| Add-on discovery | Folder containing __init__.py, not a loose script |
Help documentation, Creating a Python Add-on |
| Module body execution | Once, at add-on load | Python API reference, add-on lifecycle |
app.TeachContext at load |
None until teach mode is active |
Application object, Python API reference |
| Active robot |
app.TeachContext.ActiveRobot, changes with selection |
Application object |
| Handler scope |
MainRoutine only; subroutines are separate objects |
executor.Program members |
| Statement filter | VC_STATEMENT_PTPMOTION |
Statement type constants |
| Property type | VC_BOOLEAN |
Property type constants |
Registering the Add-On on Disk
- Create a subfolder inside the My Commands directory, named without spaces — for example
CheckboxOnPTP. - Place the command module inside that folder.
- Add an
__init__.pyfile in the same folder. This file is what makes the loader import the package; follow the registration pattern given in the Creating a Python Add-on help topic for the command entry point and metadata. - Restart the application, or reload add-ons if your version exposes that action.
- Confirm the command name appears in the command list. If it does not, the folder was not discovered — check the path and the presence of
__init__.pybefore touching the Python.
An add-on that appears in the list but does nothing has cleared the discovery hurdle and failed the binding hurdle. Those are separate problems and separate fixes.
Rebinding the Handler on Selection Change
Move the binding out of module scope and into a callback that fires whenever the selected robot changes. The module body then does one job only: subscribe.
app = getApplication()
_bound_routine = None
def on_statement_added(statement):
if statement.Type != VC_STATEMENT_PTPMOTION:
return
if statement.getProperty("checkbox") is None:
statement.createProperty(VC_BOOLEAN, "checkbox")
def bind_active_program():
global _bound_routine
if app.TeachContext is None or app.TeachContext.ActiveRobot is None:
return
robotdata = getSelectedRobotsData()
if not robotdata:
return
executor = robotdata[0]
routine = executor.Program.MainRoutine
if routine is _bound_routine:
return
if _bound_routine is not None:
_bound_routine.OnStatementAdded = None
routine.OnStatementAdded = on_statement_added
_bound_routine = routine
def on_selection_changed(*args):
bind_active_program()
# Subscribe to the application selection-changed signal. The exact signal
# name is listed in the Python API reference under Application events.
app.<SelectionChangedSignal> += on_selection_changed
bind_active_program()
Three details in that listing are not cosmetic. The _bound_routine module-level reference keeps the handler and the routine alive; a handler assigned from inside a function with no surviving reference is a candidate for collection and will stop firing at an unpredictable point. The unbind of the previous routine prevents handler stacking, which otherwise produces a duplicate createProperty call and an exception on the second attempt. The getProperty check makes the handler idempotent, so re-teaching or re-entering the editor does not fault.
Verification in the Program Editor
- Load a layout with at least one robot and open the program editor.
- Select the robot. The selection-changed callback fires here; if you are stepping through with logging, this is where the bind should register.
- Teach a PTP point. The new statement must show a
checkboxboolean in its property panel. - Teach a second PTP point, then a linear motion statement. The second PTP gets the property; the linear statement does not. That asymmetry confirms the
VC_STATEMENT_PTPMOTIONfilter is being evaluated rather than the handler firing blindly. - Select a second robot and teach a PTP point on it. A checkbox on that statement proves the rebind worked; no checkbox means the selection signal is not wired to
on_selection_changed. - Restart the application and repeat step 3 without reloading anything manually. Passing here confirms the add-on is registered rather than surviving on a stale import.
Recurring Failure Modes
Statements added to a subroutine never trigger the handler, because MainRoutine is a different object. If operators teach into subroutines, enumerate the routines in executor.Program and bind each one, then rebind when routines are created or deleted.
Editing the module while the application is running leaves the old code resident. Every behavioral test after a source edit needs a full reload, otherwise you are debugging the previous version.
A traceback in the output panel at startup means the package was found and the import failed — usually a missing constant because the module was written against the behavior scope, where some names are already present. Import or qualify the constants explicitly in a command module.
Handler stacking is the quiet one. Without the unbind, each selection change adds another subscriber; the first PTP statement then gets one property, the fifth gets five attempts and an exception on the second. Symptom order matters: intermittent failures that get worse the longer the session runs are almost always duplicate subscriptions.
If the command folder is discovered, the module imports without a traceback, and the statement handler still never fires after a clean restart, the problem is on the API side rather than in the add-on structure — an event signal that changed name or signature between releases, or a statement type constant that no longer matches. At that point capture the version, the folder layout, and the module, and open a case with Visual Components support rather than continuing to permute the code. Reference the Creating a Python Add-on help topic in the ticket so the reviewer starts from the same registration pattern you used.
FAQ
What happens if I leave out the __init__.py file?
The command folder is never imported, so the script does not execute and no error appears anywhere. The add-on simply does not exist as far as the application is concerned — silence in the output panel is the signature of this failure, not a syntax problem in the Python.
What happens if I bind OnStatementAdded at module scope instead of in a selection handler?
At load time app.TeachContext and ActiveRobot are normally None, so the guard skips the whole block and the handler is never assigned. Even if a robot happened to be selected, the binding would point at one routine and stop working the moment the operator switched robots or programs.
What happens if the same statement gets createProperty called twice?
The duplicate property name raises an exception and the teach operation reports an error. Guard with statement.getProperty("checkbox") before creating, and unbind the previous routine on every rebind so handlers do not stack across selection changes.