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.
—
—
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
High
Do not use without reviewing
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.
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.