Context for developing and debugging Hubitat Elevation apps, drivers, and hub environment — sandbox constraints, lifecycle idioms, capability contracts, plus grounded deploy/log-tail/lint mechanisms.
74
93%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
An app is not a long-running process. The hub wakes it on an event, a schedule, a UI render, install/update/uninstall, or an HTTP endpoint hit, runs one method, and sleeps.
definition() metadata — how the app is filed and instantiateddefinition() carries more than name/namespace/author. These metadata keys change where the app appears and how it is created, and the platform supplies a default for any you omit — so leaving one off is a silent decision, not a no-op:
menu — the left-nav menu the app sorts into. Set it explicitly on every app.
Apps, misfiling an event/presence-driven automation.Apps, Automations, Integrations.Integrations; device / time / presence automation → Automations; utility, manager, or dashboard → Apps.menu is not category — neither implies the other. Setting only category still leaves the app in Apps.category — separate metadata: Convenience, Safety & Security, Utility, My Apps, Hidden.singleInstance: true — the app installs once, not repeatedly (the usual parent-app case).installOnOpen: true — the instance is created when its page opens, not on the first Done.parent — declares this a child app under a named parent (namespace:name), set on the child.lint-review warns only on an absent menu, never on an unrecognized value.installed() — first install only.updated() — every time the user presses Done on an already-installed app.uninstalled() — on removal; subscriptions and schedules are auto-cleaned, so use it only for external cleanup.appButtonHandler(String name) — a button input was pressed.hubStartupHandler() — auto-called on hub startup (no subscription needed).installed() runs — not updated(). An app that creates its subscriptions solely in updated() silently does nothing until the second Done. This is the single most common app bug.installed() calls updated(); updated() calls unsubscribe() then re-subscribes.def installed() { updated() }
def updated() { unsubscribe(); initialize() }
def initialize(){ subscribe(motionSensor, "motion", "motionHandler") }unsubscribe() at the top of updated()/initialize() prevents duplicate subscriptions when the user changes a selected device. The same applies to schedules — unschedule() before re-scheduling, or runIn/schedule stack silently unless overwrite is left at its default.hubStartupHandler() runs on hub startup without routing through installed()/updated()/initialize(). Any state.* value that initialize() sets up may be null when it fires, and reading it NPEs on boot (cannot invoke method keySet() on null object).hubStartupHandler, a subscribed event, a scheduled job.state.* collections in initialize() alone — a Map and a List both start null (the example seeds each). Put the setup in a small helper, call it from every entry point that reads them, or null-guard at the read site (rules/groovy-gotchas.md).private ensureState() {
if (state.activeSince == null) state.activeSince = [:]
if (state.stuck == null) state.stuck = []
}
def initialize() { ensureState(); /* subscribe … */ }
private reconcile() { ensureState(); /* reached from hubStartupHandler(), which skips initialize() */ }
def hubStartupHandler() { reconcile() }skills/test).app.updateSetting(name, value) writes a setting; app.removeSetting(name) / clearSetting(name) drop one. All undocumented platform methods.removeSetting is deferred — the in-memory settings map still returns the old value for the rest of the current execution, so it is cleanup for the next wake, never a same-render crash-guard (rules/groovy-gotchas.md).subscribe(dev, "switch", "switchHandler"), runIn(300, "checkState"). A typo'd or missing handler name fails quietly — see rules/groovy-gotchas.md.evt param: evt.name, evt.value, evt.device.runIn/runInMillis/runOnce/schedule (7-field Quartz cron) over any busy-wait. Parent/child app communication goes through exposed methods, never shared state — see skills/_reference/endpoints.md only for hub-side APIs, not for cross-app calls.