Configuration Rules NIDL API Reference

The Configuration Rules API lets you read and author the rules that drive and limit the configured aspects of a Configured Design in Fusion — the same rules you can build on the Rules Canvas, expressed as typed objects in code.

Preview: The Configuration Rules API is provided in preview. Its objects, methods, and properties may change in a future release.

In this Document

Overview — what a configuration rule is and the object model.
Getting Started — prerequisites, reaching the manager, and the script scaffold the examples use.
Reading Rules — getRules, itemById, itemByIds.
Modifying Rules — add, update, delete, reorder, and round-trip as JSON.
Building Rule Bodies — the typed statement and condition blocks.
Columns and Values — the columns a rule can target and the value to use for each.
Issues, Errors, and Best Practices — handling problems and writing predictable rules.
See also


Overview

What a Configuration Rule Is

A configuration rule is a logical rule that limits or drives the values of the configured aspects of a Configured Design. Each rule is evaluated against every configuration in the design — the configurations in order from left to right — and the rules themselves are evaluated from top to bottom. A rule produces one of two kinds of result for a configured aspect:

In the user interface, you author rules on the Rules Canvas by connecting blocks from the Toolbox into conditional statements. The API works with the same rules and the same building blocks, but you construct them as typed objects in code. A rule you create with the API appears on the Rules Canvas, and a rule authored in the user interface can be read and modified with the API.

image-20260715-020213.png

The Object Model

Access begins with the configurationRulesManager property of the Design object. It returns a ConfigurationRulesManager, or null if the design is not a Configured Design. Because rules are stored with the design rather than held in memory, each read is a discrete request, so read what you need into your own variables rather than calling the manager repeatedly. Through the manager you can:

Each rule is a ConfigurationRule with a name, an optional comment, an isActive flag, and a body. The body is an ordered list of typed blocks in two families — statement blocks (the actions a rule performs) and condition blocks (the tests an If block evaluates) — plus the typed values those blocks carry. See Building rule bodies for the full block reference.

In the user interface, these blocks sit inside a Rule block on the Rules Canvas that carries the rule's name and on/off state. In the API that container is the ConfigurationRule object itself — its name, isActive, and body — not a block within the body.

Reads and modifications both return a result that carries an issues array alongside the affected rules. An empty issues array means the operation was applied or read cleanly; entries describe non-fatal problems. Fatal problems are reported as an error instead (see Issues, errors, and best practices).

Configuration rules act on the columns of the configuration table's top table — not on theme tables directly, though a top-table theme column's value is a theme-table row key (see Columns and values).


Getting Started

Before using the API, create and edit some configuration rules through Fusion's user interface so you are familiar with how they behave; this document explains the equivalent functionality in the API. All examples are Python, intended to be run from the Fusion Script Editor (Utilities > Scripts and Add-Ins) with a Configured Design open and active.

Prerequisites

The Script Scaffold

Every example in this document runs inside the following scaffold, which connects to the manager and handles fatal errors. Later snippets show only the lines that matter and assume rulesManager, app, ui, and design are in scope from here.

import adsk.core, adsk.fusion, traceback

def
run(context):
    ui = None
    try:
        app = adsk.core.Application.get()
        ui = app.userInterface
        design: adsk.fusion.Design = app.activeProduct

        # The manager is null for a design that is not a Configured Design.
        rulesManager = design.configurationRulesManager

        if rulesManager is None:
            ui.messageBox('The active design is not a Configured Design.')
            return

        # ---- example code goes here, using `rulesManager` ----
except:
        # Fatal problems, such as a missing extension entitlement, arrive here.
        if ui:
            ui.messageBox('Failed:{}'.format(traceback.format_exc()))


Reading Rules

All three read methods return the affected rules together with an issues array. Inspect issues even on a read — a non-empty array describes a non-fatal problem, such as a CONVERSION_WARNING encountered while reading a rule back.

result = rulesManager.getRules()
app.log(f'The design has {len(result.rules)} configuration rule(s).')

for rule in result.rules:
    state = 'active' if rule.isActive else 'inactive'
    app.log(f'  {rule.name} (id={rule.id}, {state})')
for issue in result.issues:
    app.log(f'  issue: {issue.code} - {issue.message}')

Each ConfigurationRule exposes its id, name, comment, isActive flag, and body. To work with the blocks in a body, see Building rule bodies.



Modifying Rules

All changes go through modifyRules, which takes a ConfigurationRulesModifyInput built with ConfigurationRulesModifyInput.create(). The input has three optional collections:

The request is applied in a fixed order — deletes first, then reorder, then adds and updates — but it is not all-or-nothing: a per-rule or per-block problem does not roll back the rest of the batch. If an individual block fails validation, it is replaced with an empty placeholder and a VALIDATION_FAILED issue is recorded, while the rest of the rule and the batch still apply. Always inspect issues after a call. modifyRules returns a ConfigurationRulesResult whose rules collection holds the post-mutation state of the added or updated rules (in input order), which is how you recover the ids assigned to new rules.

Adding

A rule with an empty id is treated as new. Create it with ConfigurationRule.create(), populate it, and add it to the input's rules collection. (See Building rule bodies for populating body.)

newRule = adsk.fusion.ConfigurationRule.create()
newRule.name = 'Power requires table'
newRule.comment = 'Created from the API'

# newRule.body = [ ... ]  # see Building rule bodies

modifyInput = adsk.fusion.ConfigurationRulesModifyInput.create()
modifyInput.rules = [newRule]

result = rulesManager.modifyRules(modifyInput)

for rule in result.rules:
    app.log(f'Added "{rule.name}" with id {rule.id}.')

for issue in result.issues:
    app.log(f'  issue: {issue.code} - {issue.message}')

Updating

Submit a ConfigurationRule with a non-empty id. The name, comment, and isActive fields are merged individually — you can change one without affecting the others. The body, however, is replaced as a whole: setting body replaces the existing body completely. To change a single block, read the rule, modify its body, and submit the whole rule (see Building rule bodies).

# Deactivate a rule without touching anything else about it.
existing = rulesManager.itemById(ruleId).rule
existing.isActive = False

modifyInput = adsk.fusion.ConfigurationRulesModifyInput.create()
modifyInput.rules = [existing]
rulesManager.modifyRules(modifyInput)

Deleting

List the ids to remove in deleteRuleIds. Deletes run before reorder and add/update. An id that does not match an existing rule is reported as a RULE_NOT_FOUND issue; the remaining ids are still deleted.

modifyInput = adsk.fusion.ConfigurationRulesModifyInput.create()
modifyInput.deleteRuleIds = [ruleId]
rulesManager.modifyRules(modifyInput)

Reordering

Because rules are evaluated top to bottom, order matters when more than one rule affects the same column. Set orderedRuleIds to the desired final order. When non-empty, it must list exactly one entry for every rule that remains after the delete phase. An empty orderedRuleIds leaves the order unchanged.

currentIds = [rule.id for rule in rulesManager.getRules().rules]
modifyInput = adsk.fusion.ConfigurationRulesModifyInput.create()
modifyInput.orderedRuleIds = list(reversed(currentIds))  # reverse the current order
rulesManager.modifyRules(modifyInput)

Convenience Methods

For single-rule cases you can skip building a ConfigurationRulesModifyInput:

added = rulesManager.addRule('Width minimum').rule
app.log(f'Added rule with id {added.id}.')
wasDeleted = rulesManager.deleteRule(added.id)
app.log(f'Deleted: {wasDeleted}')

Use a ConfigurationRulesModifyInput when you need to populate a body before submitting, or to batch several changes together.

Duplicating a Rule and Round-Tripping as JSON

A rule's id identifies an existing rule, so to copy a rule you detach the copy from that id. ConfigurationRule.toJson() returns a JSON string capturing the rule's full state; the static ConfigurationRule.fromJson(ruleJson) parses it back into a detached rule. Call clearId() on the copy so the next submit treats it as new.

original = rulesManager.itemById(ruleId).rule
copy = adsk.fusion.ConfigurationRule.fromJson(original.toJson())
copy.clearId()
copy.name = original.name + ' (copy)'

modifyInput = adsk.fusion.ConfigurationRulesModifyInput.create()
modifyInput.rules = [copy]
rulesManager.modifyRules(modifyInput)

Rule names are disambiguated automatically on submit — a second "Width minimum" becomes "Width minimum 2". JSON is also useful for storing a rule outside the design or exchanging rules with external tooling. Round-tripping preserves blocks this version of the API does not recognize: an unknown block kind is kept as a read-only ConfigurationRuleRawStatement or ConfigurationRuleRawCondition and re-emitted unchanged.



Building Rule Bodies

A rule body is an ordered list of blocks that fall into two families: statement blocks and condition blocks. Each slot in a rule accepts only one family — a rule's body and an If block's branches take statement blocks, while a condition slot takes condition blocks.

Statement blocks are the actions a rule performs. They derive from ConfigurationRuleStatement and are legal in a rule's body and in an If block's thenBlocks and elseBlocks:

Condition blocks are the tests an If block evaluates. They derive from ConfigurationRuleCondition and are legal only as a condition or as the operand of another condition (an If block's condition, an If branch's condition, the left/right of an And/Or block, or the operand of a Not block):

Both Get and Set blocks carry a value — a subclass of ConfigurationRuleBlockValue — whose class depends on the column's type and the operator; see Columns and values.

Building a Body

Construct every block with the static create factory on its class, set its fields, and assemble the blocks. This example builds if the width is less than 400 mm, set the shelf count to 1. Column ids come from the configuration table — see Columns and values. Numbers are in internal units, so 400 mm = 40 cm.

# The condition: width < 400 mm.
condition = adsk.fusion.ConfigurationRuleGetBlock.create()
condition.columnId = widthColumnId
condition.operatorType = adsk.fusion.ConfigurationRuleGetOperators.LessThanConfigurationRuleGetOperator
condition.value = adsk.fusion.ConfigurationRuleNumberValue.create(40.0)

# The action: set the shelf count to 1.
action = adsk.fusion.ConfigurationRuleSetBlock.create()
action.columnId = shelfCountColumnId
action.operatorType = adsk.fusion.ConfigurationRuleSetOperators.EqualsConfigurationRuleSetOperator
action.value = adsk.fusion.ConfigurationRuleNumberValue.create(1.0)

# The If block ties them together.
ifBlock = adsk.fusion.ConfigurationRuleIfBlock.create()
ifBlock.condition = condition
ifBlock.thenBlocks = [action]

rule = adsk.fusion.ConfigurationRule.create()
rule.name = 'Narrow shelves have one shelf'
rule.body = [ifBlock]

modifyInput = adsk.fusion.ConfigurationRulesModifyInput.create()
modifyInput.rules = [rule]
rulesManager.modifyRules(modifyInput)

The If Block: Branches and Fallbacks

A ConfigurationRuleIfBlock evaluates its condition and runs thenBlocks when it is true. It also carries:

Evaluation follows a fixed order: the primary condition, then each branch in elseIfBranches in order; the first true branch runs its thenBlocks and no others; if none is true, elseBlocks runs.

getOps = adsk.fusion.ConfigurationRuleGetOperators
setOps = adsk.fusion.ConfigurationRuleSetOperators

def
widthLessThan(cm):
    block = adsk.fusion.ConfigurationRuleGetBlock.create()
    block.columnId = widthColumnId
    block.operatorType = getOps.LessThanConfigurationRuleGetOperator
    block.value = adsk.fusion.ConfigurationRuleNumberValue.create(cm)
    return block

def
setShelves(count):
    block = adsk.fusion.ConfigurationRuleSetBlock.create()
    block.columnId = shelfCountColumnId
    block.operatorType = setOps.EqualsConfigurationRuleSetOperator
    block.value = adsk.fusion.ConfigurationRuleNumberValue.create(count)
    return block

ifBlock = adsk.fusion.ConfigurationRuleIfBlock.create()
ifBlock.condition = widthLessThan(40.0)          # < 400 mm
ifBlock.thenBlocks = [setShelves(1.0)]

branch = adsk.fusion.ConfigurationRuleIfBranch.create()
branch.condition = widthLessThan(80.0)           # < 800 mm
branch.thenBlocks = [setShelves(2.0)]
ifBlock.elseIfBranches = [branch]

ifBlock.elseBlocks = [setShelves(3.0)]           # otherwise

Combining conditions with And, Or, and Not

ConfigurationRuleAndBlock is true when both its left and right operands are true; ConfigurationRuleOrBlock is true when either is; ConfigurationRuleNotBlock is true when its operand is false. Operands are themselves condition blocks, so you can nest them to any depth.

# width >= 400 mm AND height >= 600 mm
widthOk = adsk.fusion.ConfigurationRuleGetBlock.create()
widthOk.columnId = widthColumnId
widthOk.operatorType = getOps.GreaterThanOrEqualConfigurationRuleGetOperator
widthOk.value = adsk.fusion.ConfigurationRuleNumberValue.create(40.0)

heightOk = adsk.fusion.ConfigurationRuleGetBlock.create()
heightOk.columnId = heightColumnId
heightOk.operatorType = getOps.GreaterThanOrEqualConfigurationRuleGetOperator
heightOk.value = adsk.fusion.ConfigurationRuleNumberValue.create(60.0)

bothOk = adsk.fusion.ConfigurationRuleAndBlock.create()
bothOk.left = widthOk
bothOk.right = heightOk

Reading a Body, Placeholders, and Unrecognized Blocks

When you read a rule, its body contains the same block objects typed as their abstract base classes. Use the cast method on a concrete class to work with a block as its specific type; cast returns None if the block is not of that type, which is how you branch on block kind.

for block in rulesManager.itemById(ruleId).rule.body:
    ifBlock = adsk.fusion.ConfigurationRuleIfBlock.cast(block)

if ifBlock is
not
None:
        getBlock = adsk.fusion.ConfigurationRuleGetBlock.cast(ifBlock.condition)

if getBlock is
not
None:
            app.log(f'If block reads column {getBlock.columnId}.')

continue
    setBlock = adsk.fusion.ConfigurationRuleSetBlock.cast(block)

if setBlock is
not
None:
        app.log(f'Set block writes column {setBlock.columnId}.')

A Get or Set block with no operator, no value, or no columnId is an unconfigured placeholder: it is valid to store but does nothing until completed. A block whose columnId cannot be resolved, or whose operator or value is not valid for the column, is also reduced to a placeholder on submit and records a VALIDATION_FAILED issue. The read-only ConfigurationRuleRawStatement and ConfigurationRuleRawCondition wrappers hold block kinds a newer Fusion produced that this version does not recognize; they round-trip unchanged and are never constructed directly.



Columns and Values

A Get or Set block targets a column of the configuration table by its columnId, which is the id of a ConfigurationColumn. Never invent a columnId — obtain it from the top table (below). A columnId Fusion does not recognize at all, or an operator or value not valid for the column, is reduced to a placeholder with a VALIDATION_FAILED issue. A columnId Fusion recognizes but does not support for rules (see Known limitations) is reduced the same way but reported as COLUMN_NOT_SUPPORTED instead, so you can tell a typo apart from a column that will never work. Neither case raises an error, so always inspect issues after submitting.

Each column has a leaf type that determines the value class a block must carry:

Leaf typeValue class
numberConfigurationRuleNumberValue (or ConfigurationRuleGetRangeValue / ConfigurationRuleSetRangeValue for ranges, or ConfigurationRuleNumberSelectionValue for AnyOf/NoneOf)
textConfigurationRuleStringValue
booleanConfigurationRuleBooleanValue
dropdown / listConfigurationRuleStringListValue (a ConfigurationRuleStringValue is accepted on write and reads back as a list)

Finding Column IDs and Their Values

Column ids and the valid values for list columns come from Design.configurationTopTable, not from the rules API. Iterate its columns; every ConfigurationColumn has an id, a title, and a type you can test with isinstance.

top = design.configurationTopTable

for column in top.columns:
    columnId = column.id                       # use this as the block's columnId

if isinstance(column, adsk.fusion.ConfigurationParameterColumn):
        app.log(f'{columnId}: parameter "{column.parameter.name}"')

elif isinstance(column, adsk.fusion.ConfigurationSuppressColumn):
        app.log(f'{columnId}: suppression of "{column.feature.name}"')

elif isinstance(column, adsk.fusion.ConfigurationVisibilityColumn):
        app.log(f'{columnId}: visibility of "{column.entity.name}"')

elif isinstance(column, adsk.fusion.ConfigurationInsertColumn):
        app.log(f'{columnId}: insert of "{column.occurrence.name}"')

elif isinstance(column, adsk.fusion.ConfigurationThemeColumn):
        app.log(f'{columnId}: theme column')

For a dropdown column (a theme column or an insert column) the value you supply is an option key — the id of a ConfigurationRow in the referenced table, not its display name. Enumerate the referenced table's rows to get the keys:

for theme in top.customThemeTables:

for i in range(theme.rows.count):
        row = theme.rows.item(i)
        app.log(f'  option key {row.id} = "{row.name}"')

The Configuration Name aspect has no physical column; address it with the fixed id "CONFIGURATION_NAME", and its valid values are the top table's row names.

Supported Columns

Column typeControlsReadWriteValue class
ConfigurationParameterColumn (numeric)A numeric parameter✅✅ConfigurationRuleNumberValue; range values for ranges
ConfigurationParameterColumn (text)A text parameter✅✅ (Equals only)ConfigurationRuleStringValue
ConfigurationSuppressColumnFeature/occurrence suppression✅✅ConfigurationRuleBooleanValue
ConfigurationVisibilityColumnEntity visibility✅✅ConfigurationRuleBooleanValue
ConfigurationThemeColumnActive theme-table row✅✅ConfigurationRuleStringListValue (option keys)
ConfigurationInsertColumnInserted configuration row✅✅ConfigurationRuleStringListValue (option keys)
Configuration Name ("CONFIGURATION_NAME"The configuration's name✅❌ConfigurationRuleStringValue
Joint feature aspectsA joint's configurable aspects✅✅ConfigurationRuleBooleanValue / ConfigurationRuleStringListValue

Operators by Leaf Type

The operator must be valid for the column's leaf type. Set the Unknown operator to leave a block as an unconfigured placeholder. Each name is a value of ConfigurationRuleGetOperators or ConfigurationRuleSetOperators, suffixed with the operator kind — for example LessThanConfigurationRuleGetOperator and EqualsConfigurationRuleSetOperator.

Leaf typeGet operatorsSet operators
numberEquals, NotEquals, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Range, AnyOf, NoneOfEquals, Minimum, Maximum, Range, AnyOf
textEquals, NotEquals, Contains, NotContainsEquals
booleanEquals, NotEqualsEquals, NotEquals
dropdown / listEquals, NotEqualsEquals, NotEquals

Value Semantics

# Dropdown/theme values take a LIST of option keys, even for a single choice:
themeValue = adsk.fusion.ConfigurationRuleStringListValue.create([optionKey])

# Boolean values follow the aspect's state:
suppressValue = adsk.fusion.ConfigurationRuleBooleanValue.create(True)

Ranges

On a Get block, the Range operator tests whether the column's current value falls within [min, max]. Use a ConfigurationRuleGetRangeValue.create(hasMin, min, hasMax, max); a bound that is not set is open on that side.

# True when the width is between 400 mm and 900 mm, inclusive.
inRange = adsk.fusion.ConfigurationRuleGetBlock.create()
inRange.columnId = widthColumnId
inRange.operatorType = getOps.RangeConfigurationRuleGetOperator
inRange.value = adsk.fusion.ConfigurationRuleGetRangeValue.create(True, 40.0, True, 90.0)

On a Set block, the Range operator constrains the column with a lower bound, an upper bound, an increment, or any combination. Use ConfigurationRuleSetRangeValue.create(hasMin, min, hasMax, max, hasIncrement, increment). Each of the three parts — min, max, and increment — is independently optional, and at least one must be set. Only the parts you set take effect: min is the lowest value the user can choose, max the highest, and increment the step between the allowed values in between. When both min and max are set, min must not be greater than max.

# Constrain the width to 400-900 mm in 5 mm steps.
fullRange = adsk.fusion.ConfigurationRuleSetBlock.create()
fullRange.columnId = widthColumnId
fullRange.operatorType = setOps.RangeConfigurationRuleSetOperator
fullRange.value = adsk.fusion.ConfigurationRuleSetRangeValue.create(True, 40.0, True, 90.0, True, 0.5)

A Set block can also use Minimum and Maximum with a plain ConfigurationRuleNumberValueRange expresses the same intent in one block and can carry an increment, so prefer Range. Do not use a get-range value on a Set block or a set-range value on a Get block — the two classes differ (only the set range has an increment), and each belongs with its own operator.

AnyOf and NoneOf

Where Range tests or constrains a span of numbers, AnyOf and NoneOf test or constrain a number column against a discrete set of candidate values. On a Get block, AnyOf is true when the column's current value equals one of the selected entries; NoneOf is true when it equals none of them. On a Set block, AnyOf limits the column to the selected entries — there is no set-side NoneOf; use AnyOf whenever a Set block needs to offer a discrete list of numbers.

Both operators carry a ConfigurationRuleNumberSelectionValue, built from an array of ConfigurationRuleNumberSelectionEntry objects assigned to its entries property. Each entry pairs a numeric value (Fusion internal units) with its own isSelected flag, so an entry can be carried in the block without being part of the current selection. At least one entry must be selected; an all-false selection is rejected.

# True when the width is 400 mm, 600 mm, or 900 mm.
anyOf = adsk.fusion.ConfigurationRuleGetBlock.create()
anyOf.columnId = widthColumnId
anyOf.operatorType = getOps.AnyOfConfigurationRuleGetOperator
anyOf.value = adsk.fusion.ConfigurationRuleNumberSelectionValue.create()
anyOf.value.entries = [
    adsk.fusion.ConfigurationRuleNumberSelectionEntry.create(40.0, True),
    adsk.fusion.ConfigurationRuleNumberSelectionEntry.create(60.0, True),
    adsk.fusion.ConfigurationRuleNumberSelectionEntry.create(90.0, True),
]

NoneOf reads the same way with NoneOfConfigurationRuleGetOperator; to limit a Set block to the same kind of candidate list, use AnyOfConfigurationRuleSetOperator with a ConfigurationRuleNumberSelectionValue built the same way.

Known Limitations

The supported-columns table above lists what a rule can read and write today. A few limitations are worth calling out:


Issues, errors, and best practices

The API distinguishes two kinds of problem: non-fatal problems, reported as issues on the returned result, and fatal problems, reported as errors. An operation can succeed overall while a part of it silently degrades, so understanding the difference matters.

Non-fatal problems become issues. When a single block or rule in a request has a problem, the API records an issue and continues: the offending block is reduced to an empty placeholder and the rest of the rule and the batch still apply. The batch is not rolled back. Inspect the issues collection after every call, including reads and apparently successful writes.

Fatal problems are reported as an error. An invalid request shape, an uninitialised rules pipeline, or a missing extension entitlement on a write is reported as an error rather than returning a result. In Python the error is raised as an exception (a RuntimeError), so wrap calls in try/except; in C++ the call does not throw — retrieve the error with Application::getLastError.

The issue Object and Codes

Every issue is a ConfigurationRulesIssue with a code, a message (developer-facing, not localized), a ruleId, a columnId, a path (such as rules[0].body[2]), and a ruleIndex guarded by hasRuleIndex. To correlate an issue back to a rule in your request, use ruleIndex (when hasRuleIndex is True) rather than parsing path.

CodeMeaning
VALIDATION_FAILEDA Get or Set block did not pass validation and was replaced with a placeholder — e.g. a columnId Fusion does not recognize at all, or an operator/value not valid for the column.
COLUMN_NOT_SUPPORTEDA Get or Set block targeted a columnId that Fusion recognizes but does not support for rules (see <a href="#Known-limitations">Known limitations</a>) — distinct from VALIDATION_FAILED, which covers columns Fusion does not recognize at all. The block is replaced with a placeholder the same way.
RULE_NOT_FOUNDAn id in deleteRuleIds, or requested through itemByIds, did not match an existing rule.
DELETE_FAILEDA rule could not be deleted.
CREATE_FAILEDA new rule could not be created.
CONVERSION_WARNINGA non-fatal problem occurred while reading a rule back.
EXTENSION_REQUIREDA write was attempted without the Fusion Design Extension.

Treat any code you do not recognize as an opaque string — future versions may add more.

result = rulesManager.modifyRules(modifyInput)

if
not result.issues:
    app.log('Applied cleanly.')

for issue in result.issues:
    location = f'rule {issue.ruleIndex}'
if issue.hasRuleIndex else
'request'
    app.log(f'{issue.code} at {location} (column "{issue.columnId}"): {issue.message}')

Writing clear rules

Ordering and Evaluation

Rules are evaluated top to bottom, and the statements within a rule run in order. When more than one rule or block affects the same column, order determines the outcome, so use orderedRuleIds to control it deliberately. When multiple Set blocks limit the same column under the same condition, the result is the intersection of their value lists — you can narrow a list this way but not widen it. If two rules leave no value in common, the configuration has no valid value for that aspect, which is an error; lead with the least restrictive condition and review rules that constrain the same aspect.

Avoiding Cycles

A rule can both read and write the configuration, but avoid cycles that never settle. A cycle arises when rules read and write each other's columns in a loop — for example, Rule 1 sets Width from Length while Rule 2 sets Length from Width — so the values oscillate rather than settle. Avoid reading and writing the same column within one rule unless the write immediately satisfies the read, and structure rules so their conditions read from different columns than the ones they set.


See also