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.
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.
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.
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.
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", ...)Declare explicitly if this is the first extension in the dependency chain to use omni.kit.commands:
[dependencies]
"omni.kit.commands" = {}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")