Folder and Tag Methods¶
New in 2026.1 A plugin can ask to be told when folders are created, renamed or deleted, and when the tag library changes. Both are opt-in: a plugin that doesn't subscribe receives nothing.
The two subscriptions have deliberately different shapes. Folders are subscribed per object type, because each type has its own folder list. Tags are one global subscription, because there is a single tag library shared by every object type.
Plugins only
subscribeToChanges() on a folder list and indigo.server.subscribeToTagChanges() can only be
called from a plugin. From a script or the interactive shell they raise ValueError.
There is no ownership filtering on either
deviceCreated() and friends check dev.pluginId, so a plugin only reacts to its own devices.
Folders and tags have no owning plugin, so every subscribed plugin sees every change on the
lists it subscribed to. The default implementations in plugin_base.py do nothing.
Subscribing to folder changes¶
Each object type's folder list is subscribed separately — subscribing to device folders does not subscribe you to variable folders. Subscribe only to the lists you actually use, in startup():
def startup(self):
indigo.devices.folders.subscribeToChanges()
indigo.variables.folders.subscribeToChanges()
indigo.actionGroups.folders.subscribeToChanges()
indigo.schedules.folders.subscribeToChanges()
indigo.triggers.folders.subscribeToChanges()
indigo.controlPages.folders.subscribeToChanges()
Telling the lists apart¶
There are six folder lists but one set of callbacks, so a plugin subscribed to more than one list needs to know which list a change came from. Every indigo.Folder carries a read-only parentType:
def folder_updated(self, orig_folder, new_folder):
if new_folder.parentType != "device":
return
self.logger.info(f"device folder renamed: {orig_folder.name} -> {new_folder.name}")
Its value is one of "device", "variable", "actionGroup", "schedule", "trigger" or "controlPage" — the same names the command namespaces use.
What reaches these callbacks¶
| Change | Callback |
|---|---|
| Folder created | folder_created |
| Folder renamed | folder_updated |
| Folder's "display in remote UI" toggled | folder_updated |
| Folder deleted | folder_deleted |
| An object moved into or out of a folder | Not a folder change. The object's folderId changed, so it arrives as deviceUpdated() or the equivalent for its type |
| Objects in a deleted folder | Not deleted with it. They move to the top level, which again reaches you as ordinary object updates |
folder_created()¶
Called when a folder is created in one of the lists you subscribed to.
Method
| Method | Required |
|---|---|
folder_created(self, folder) |
No |
Parameters
| Parameter | Description |
|---|---|
folder |
an indigo.Folder object representing the folder that was created |
Return Value:
| Type | Description |
|---|---|
| None | This method does not return a value. |
Command Syntax Examples
def folder_created(self, folder):
self.logger.info(f"new {folder.parentType} folder: {folder.name}")
folder_updated()¶
Called when a folder is renamed, or its remote display setting is toggled. You get the folder both before and after the change, so you can see what actually changed rather than caching the previous state yourself.
Method
| Method | Required |
|---|---|
folder_updated(self, orig_folder, new_folder) |
No |
Parameters
| Parameter | Description |
|---|---|
orig_folder |
an indigo.Folder object representing the folder before the change |
new_folder |
an indigo.Folder object representing the folder after the change |
Return Value:
| Type | Description |
|---|---|
| None | This method does not return a value. |
Command Syntax Examples
def folder_updated(self, orig_folder, new_folder):
if orig_folder.name != new_folder.name:
self.logger.info(f"renamed {orig_folder.name} -> {new_folder.name}")
folder_deleted()¶
Called when a folder is deleted. The objects that were in it are not deleted with it; they move to the top level, which reaches you as ordinary object updates.
Method
| Method | Required |
|---|---|
folder_deleted(self, folder) |
No |
Parameters
| Parameter | Description |
|---|---|
folder |
an indigo.Folder object representing the folder that was deleted |
Return Value:
| Type | Description |
|---|---|
| None | This method does not return a value. |
Renaming a folder from a plugin¶
There is no setName on the folder command namespace. That's deliberate, so folder edits go through the same modify-then-push path as every other object:
indigo.devices.folder does offer create, duplicate, delete and displayInRemoteUI — see the Folders reference. Calling setName on it raises AttributeError.
Subscribing to tag changes¶
One subscription, not one per type, because there is a single tag library shared by every object type. Call it once in startup():
Each tag is a one-entry map¶
Every tag callback receives a {name: colors} dictionary with a single entry, not a loose name and color. That's the same shape indigo.server.tags returns for the whole library, which is the point of the symmetry: a callback argument can be handed straight to code written against the library, because it is the library's shape with one entry in it.
The value is a dictionary carrying three colors:
color is what the user chose and the only one stored. text_color and border_color are derived from it by the server, are read-only, and are what to draw a tag pill's text and border with. All three are bare "AABBCC" hex strings — the same form create_tag and update_tag accept for color. The dictionary could gain further keys without every plugin that unpacks one having to change.
def tag_created(self, tag):
for name, colors in tag.items():
self.logger.info(f"new tag {name} is #{colors['color']}, "
f"drawn with #{colors['text_color']} text")
What reaches these callbacks¶
| Change | Callback |
|---|---|
| Tag created in the library | tag_created |
| Tag renamed and/or recolored | tag_updated |
| Tag deleted from the library | tag_deleted |
| A tag put on or taken off an object | Not a library change. The object changed, so it arrives as deviceUpdated() or the equivalent for its type |
| Objects carrying a renamed or deleted tag | Their tag lists are rewritten, which reaches you as ordinary object updates, in addition to the library callback |
An update_tag that changes nothing |
Nothing at all. A no-op update isn't broadcast |
A recolor reaches a plugin only through these callbacks
Recoloring a tag rewrites no object, so nothing on any object subscription fires for it. Everything else in the table above is also visible as object updates, if you happen to be watching the right objects — a recolor is not.
tag_created()¶
Called when a tag is added to the library.
Method
| Method | Required |
|---|---|
tag_created(self, tag) |
No |
Parameters
| Parameter | Description |
|---|---|
tag |
a one-entry {name: colors} dictionary for the tag that was created |
Return Value:
| Type | Description |
|---|---|
| None | This method does not return a value. |
tag_updated()¶
Called when a tag is renamed, recolored, or both at once. You get the tag both before and after, so whichever field didn't change simply repeats and a plugin never has to have cached the previous library.
| Change | What arrives |
|---|---|
| Recolor only | same key in both, records differ |
| Rename only | identical records, keys differ |
| Both at once | one call, both differ |
Method
| Method | Required |
|---|---|
tag_updated(self, orig_tag, new_tag) |
No |
Parameters
| Parameter | Description |
|---|---|
orig_tag |
a one-entry {name: colors} dictionary for the tag before the change |
new_tag |
a one-entry {name: colors} dictionary for the tag after the change |
Return Value:
| Type | Description |
|---|---|
| None | This method does not return a value. |
Command Syntax Examples
def tag_updated(self, orig_tag, new_tag):
orig_name, orig_record = next(iter(orig_tag.items()))
new_name, new_record = next(iter(new_tag.items()))
if orig_name != new_name:
self.logger.info(f"renamed {orig_name} -> {new_name}")
if orig_record["color"] != new_record["color"]:
self.logger.info(f"recolored #{orig_record['color']} -> #{new_record['color']}")
tag_deleted()¶
Called when a tag is removed from the library, and therefore from every object carrying it. Its dictionary carries the colors the tag had at the moment it was removed.
Method
| Method | Required |
|---|---|
tag_deleted(self, tag) |
No |
Parameters
| Parameter | Description |
|---|---|
tag |
a one-entry {name: colors} dictionary for the tag that was deleted |
Return Value:
| Type | Description |
|---|---|
| None | This method does not return a value. |
These names are snake_case only¶
All six callbacks are snake_case, and there are no midCap aliases — no folderCreated, no tagUpdated. The older device and variable callbacks carry both spellings because they predate Indigo's move to snake_case. These are new, so there's no legacy name to stay compatible with.
A worked example¶
The SDK ships a plugin that subscribes to both and logs what arrives: Example Folder and Tag Subscriptions. See SDK Example Plugins.