Implementing Commands#
All user-facing mutations must go through omni.kit.commands.Command. This is what makes actions undoable. Any action
that modifies data and is visible to the user requires this pattern — direct mutations will be rejected in review.
When to Use This Pattern#
Whenever a user action changes data: setting a property, moving a prim, replacing an asset, deleting an element, etc. If the action would have an “Undo” entry in a standard application, it needs a Command here.
Class Template#
Place the command in commands.py in the relevant .core extension and export it from __init__.py.
__all__ = ["MyActionCommand"]
import omni.kit.commands
import omni.kit.undo
class MyActionCommand(omni.kit.commands.Command):
"""One-line description of what this command does."""
def __init__(self, param: str):
self._param = param
self._previous_value: str | None = None # captured in do(), used in undo()
def do(self):
self._previous_value = get_current_value() # capture state BEFORE modifying
set_value(self._param)
def undo(self):
set_value(self._previous_value)
Critical rules:
do()must capture all state needed forundo()before making any changes.Never call
omni.kit.commands.execute()from insidedo()orundo()— this corrupts the undo stack.Never instantiate a command and call
do()directly — that bypasses the undo stack entirely.
Executing a Command#
omni.kit.commands.execute("MyActionCommand", param="value")
The string name must exactly match the class name. Commands are registered globally by name — using the wrong string fails silently.
Grouping Multiple Sub-Commands#
When a user action logically requires several sub-commands that should undo as one unit:
with omni.kit.undo.group():
omni.kit.commands.execute("SubCommandA", ...)
omni.kit.commands.execute("SubCommandB", ...)
Extension Dependency#
Declare explicitly if this is the first extension in the dependency chain to use omni.kit.commands:
[dependencies]
"omni.kit.commands" = {}
Testing#
Write two separate tests — one Act each. The undo test’s execute() call belongs in Arrange (it is setup, not the
thing under test).
async def test_my_action_command_applies_new_value(self):
# Arrange
set_value("old_value")
# Act
omni.kit.commands.execute("MyActionCommand", param="new_value")
# Assert
self.assertEqual(get_current_value(), "new_value")
async def test_my_action_command_undo_restores_previous_value(self):
# Arrange
set_value("original_value")
omni.kit.commands.execute("MyActionCommand", param="new_value")
# Act
omni.kit.undo.undo()
# Assert
self.assertEqual(get_current_value(), "original_value")