PropertyPersistenceRule
Every property configured in propConfig carries a persistent flag, and Ignition defaults it to true. For a bound property that saved value is often stale or incorrect: the designer saves the last binding result into view.json on every save, so a dynamic binding (a timestamp, a query result, a tag value), or a transformation reliant on other bindings rewrites the file constantly. This results in pure git-diff churn with no runtime benefit, since at runtime the binding recomputes the value anyway.
This rule lets a project enforce a persistence convention for bound properties. It is opt-in and inert by default: without expected_persistent in your config it checks nothing.
Severity: configurable, error by default. Start with "warning" while adopting.
Auto-fix: Yes, for expected_persistent: false — --fix sets persistent: false on the propConfig entry and deletes the stale stored value from custom.<name>. No fix for expected_persistent: true (the linter can't invent a default value).
The trade-off you're opting into
Making bound properties non-persistent is best practice, but it demands discipline:
- The key doesn't exist until the binding first evaluates. Anything reading the property must chain off the binding and handle nulls, rather than assume a value is present at view startup. Done right, downstream bindings naturally wait for the first evaluation; done carelessly, you get null-reference errors or brief render glitches.
- Tag bindings may never create the key. A tag binding — especially an indirect one — whose tag isn't found will never resolve the initial evaluation. In a designer, the property key never appears at all and the user assumes the binding does not exist. For this reason, tag bindings are exempt by default.
Basic config
{
"PropertyPersistenceRule": {
"enabled": true,
"kwargs": {
"expected_persistent": false,
"severity": "warning"
}
}
}
This flags every bound custom.* property (view-level and component-level) that is persistent — explicitly or by Ignition's absent-means-true default — except tag-bound properties.
Less common configurations
Check tag bindings too
The default exempt_binding_types: ["tag"] skips all tag bindings. If your project guarantees its tags exist, override the exemption list (it replaces the default entirely):
{
"PropertyPersistenceRule": {
"enabled": true,
"kwargs": {
"expected_persistent": false,
"exempt_binding_types": []
}
}
}
You can also exempt by tag mode — "tag.indirect" exempts only indirect tag bindings while still checking direct ones — or exempt other binding types entirely ("query", "expr", "expr-struct", "property").
Enforce persistence instead
Some projects prefer stored defaults so every key exists before the first binding evaluation:
{
"PropertyPersistenceRule": {
"enabled": true,
"kwargs": {
"expected_persistent": true
}
}
}
This flags bound properties with persistent: false, and also flags persistent bound properties that have no stored default value — persistence without a value defeats its own purpose.
Example
Problematic view.json
{
"custom": {
"lineData": "2026-08-31 06:24:59"
},
"propConfig": {
"custom.lineData": {
"binding": {
"config": { "expression": "now()" },
"type": "expr"
},
"persistent": true
}
}
}
Every designer save rewrites custom.lineData with whatever the binding last produced. The linter emits:
PropertyPersistenceRule (warning):
• custom.lineData: bound property is persistent; set persistent=false to stop storing designer results in view.json
After --fix
{
"custom": {},
"propConfig": {
"custom.lineData": {
"binding": {
"config": { "expression": "now()" },
"type": "expr"
},
"persistent": false
}
}
}
See also
- Full PropertyPersistenceRule reference — every option and violation format
- PropertyAccessRule — the companion opt-in rule for
accessmodes - UnusedCustomPropertiesRule — detects and removes ghost/stale property definitions (propConfig entries with no binding, no value, and no references)