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, the value to use for each, and expressions.
Issues and Errors — handling problems reported by a call you made.
Rule Statuses — getStatuses, getStatusesByRuleId, getStatusesByConfigurationId.
Best Practices — 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 and errors).

Issues and statuses are different things. An issues entry describes what went wrong with the call you just made — a malformed block, an id that matched nothing. A status describes what the rules engine found when the rules last ran, for one rule on one configuration. A call can return no issues at all while a rule is switched off on half the design's configurations, so check both (see Rule statuses).

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.

clearId() clears the rule id only. Each block in the copied body still carries the blockId of the original rule's block, so submitting the copy as-is records a BLOCK_ID_CONFLICT issue per block while Fusion mints fresh ids. The copy is still created correctly, but clear blockId on the copied blocks to duplicate without those issues (see Block IDs).

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

# Detach the copied blocks from the original's block ids as well.
for block in copy.body:
    block.blockId = ''

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

This example clears the ids on the body's top-level blocks. A body with If blocks nests further blocks inside condition, thenBlocks, elseIfBranches, and elseBlocks; clear those too, or accept the BLOCK_ID_CONFLICT issues and let Fusion mint the ids.


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. Both values are expressions, which carry their own units.

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

# 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.ConfigurationRuleExpressionValue.create('1')

# 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(width):
    block = adsk.fusion.ConfigurationRuleGetBlock.create()
    block.columnId = widthColumnId
    block.operatorType = getOps.LessThanConfigurationRuleGetOperator
    block.value = adsk.fusion.ConfigurationRuleExpressionValue.create(width)
    return block

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

ifBlock = adsk.fusion.ConfigurationRuleIfBlock.create()
ifBlock.condition = widthLessThan('400 mm')
ifBlock.thenBlocks = [setShelves('1')]

branch = adsk.fusion.ConfigurationRuleIfBranch.create()
branch.condition = widthLessThan('800 mm')
branch.thenBlocks = [setShelves('2')]
ifBlock.elseIfBranches = [branch]

ifBlock.elseBlocks = [setShelves('3')]           # 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.ConfigurationRuleExpressionValue.create('400 mm')

heightOk = adsk.fusion.ConfigurationRuleGetBlock.create()
heightOk.columnId = heightColumnId
heightOk.operatorType = getOps.GreaterThanOrEqualConfigurationRuleGetOperator
heightOk.value = adsk.fusion.ConfigurationRuleExpressionValue.create('600 mm')

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.

Block IDs

Every block carries a blockId — a stable id that identifies it within its rule. It is defined on both family base classes, ConfigurationRuleStatement and ConfigurationRuleCondition, so every statement and every condition has one.

A block you have just created with create() has an empty blockId. Fusion mints one when the rule is submitted and reports it back on the next read, so a body read with getRules or itemById has ids on all of its blocks.

You can also set blockId yourself, and Fusion honours it wherever it does not collide with a block that already exists. On a collision Fusion mints a fresh id instead; the rule still applies, and a BLOCK_ID_CONFLICT issue records that the id you asked for was not the id used. Always read the id back rather than assuming the one you supplied was kept.

Block ids matter because of the round trip. Setting body replaces the body as a whole, so a read-edit-submit cycle rebuilds every block. Carrying each block's existing blockId back through that cycle is what keeps the ids on the blocks you did not change — which in turn is what lets a rule status name the block that raised it (see Rule statuses). Blocks read back from Fusion already carry their ids, so a cycle that edits a body in place preserves them for free.

# Read, edit one block, and submit. The other blocks keep their ids because the
# objects read back from Fusion still carry them.
rule = rulesManager.itemById(ruleId).rule

for block in rule.body:
    setBlock = adsk.fusion.ConfigurationRuleSetBlock.cast(block)
    if (setBlock is not None and setBlock.columnId == shelfCountColumnId
            and setBlock.operatorType == setOps.EqualsConfigurationRuleSetOperator):
        setBlock.value = adsk.fusion.ConfigurationRuleExpressionValue.create('2')

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

# Read the ids back; a BLOCK_ID_CONFLICT means an id you supplied was replaced.
for block in result.rules[0].body:
    app.log(f'block {block.blockId}')


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
numberConfigurationRuleExpressionValue wherever the operator accepts an expression; otherwise ConfigurationRuleNumberValue (or ConfigurationRuleGetRangeValue / ConfigurationRuleSetRangeValue for ranges, or ConfigurationRuleNumberSelectionValue for AnyOf/NoneOf)
textConfigurationRuleExpressionValue wherever the operator accepts an expression; otherwise ConfigurationRuleStringValue
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✅✅ConfigurationRuleExpressionValue where the operator accepts one; otherwise ConfigurationRuleNumberValue, or range values for ranges
ConfigurationParameterColumn (text)A text parameter✅✅ (Equals only)ConfigurationRuleExpressionValue; ConfigurationRuleStringValue for Contains and NotContains
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)

Expressions

Instead of a fixed number or piece of text, a Get or Set block on a number or text column can carry an expression — a formula such as Length / 2 that Fusion evaluates when the rules run. This lets a rule compare or drive one parameter relative to another, the same way you would build a parameter's value from other parameters in the Parameters dialog. Expressions are carried by a ConfigurationRuleExpressionValue, created with ConfigurationRuleExpressionValue.create(expression); its value property gets and sets the formula string.

Prefer expressions wherever they are accepted, for fixed values as well as formulas: write 400 mm rather than the number 40.0, and 'Oak' rather than the string Oak. An expression states its units, so it needs no conversion to internal units. Use ConfigurationRuleNumberValue and ConfigurationRuleStringValue only with the operators that do not accept an expression.

An expression uses the same syntax as Parameter.expression:

The result must suit the column: a number column needs an expression that evaluates to a compatible number and unit, while a text column accepts either text or a number. Expressions are accepted only by these operators:

BlockOperators that accept an expression
GetEquals, NotEquals, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual
SetEquals

Every other operator takes its literal value class. The one exception is a Get-side Range, whose bounds can be expressions through minExpression and maxExpression on the range value itself (see Ranges).

On the Rules Canvas, an expression appears in a block's value field in place of a number. The rule below drives a minimum shelf count from the cabinet's proportions: when the width is greater than five times the depth plus 100 mm, it sets the minimum number of shelves to 2.

A rule whose If block compares Width3 against the expression (Depth * 5) + 100 mm

This builds the same rule in code. The Get block compares Width3 against an expression, so the threshold follows Depth in every configuration. The Set block uses Minimum, which takes a literal number rather than an expression.

# The condition: Width3 > (Depth * 5) + 100 mm, evaluated per configuration.
isWide = adsk.fusion.ConfigurationRuleGetBlock.create()
isWide.columnId = width3ColumnId
isWide.operatorType = getOps.GreaterThanConfigurationRuleGetOperator
isWide.value = adsk.fusion.ConfigurationRuleExpressionValue.create('(Depth * 5) + 100 mm')

# The action: at least 2 shelves. Minimum takes a number, not an expression.
minShelves = adsk.fusion.ConfigurationRuleSetBlock.create()
minShelves.columnId = shelfCountColumnId
minShelves.operatorType = setOps.MinimumConfigurationRuleSetOperator
minShelves.value = adsk.fusion.ConfigurationRuleNumberValue.create(2.0)

ifBlock = adsk.fusion.ConfigurationRuleIfBlock.create()
ifBlock.condition = isWide
ifBlock.thenBlocks = [minShelves]

rule = adsk.fusion.ConfigurationRule.create()
rule.name = 'Minimum shelves'
rule.comment = 'Set minimum shelves based on width'
rule.body = [ifBlock]

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

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

A Set block with Equals can drive a column from an expression too. On a text column, that lets the value be built from other parameters:

# Drive a text parameter from the length.
setLabel = adsk.fusion.ConfigurationRuleSetBlock.create()
setLabel.columnId = labelColumnId
setLabel.operatorType = setOps.EqualsConfigurationRuleSetOperator
setLabel.value = adsk.fusion.ConfigurationRuleExpressionValue.create(
    "if (Length < 500 mm; 'Short'; 'Long')")

Expressions are checked when you submit. Fusion validates each expression against the design's current parameters when modifyRules runs. The Configuration Rules API has no call to check a rule expression in advance. UnitsManager.isValidExpression can catch syntax and unit errors beforehand, but only submitting the rule gives the definitive result, so always inspect issues. A block whose expression fails is reduced to a placeholder with a VALIDATION_FAILED issue, and the rest of the rule still applies. The issue's message names the reason, for example:

Reason in the messageCause
emptyExpressionThe expression string is empty or contains only spaces. create('') is accepted, but the block is rejected on submit.
parseErrorThe expression is not valid syntax.
variableNotFoundThe expression names a parameter the design does not have.
unitMismatchThe result does not suit the column — for example, a length expression on an angle column, or text on a number column.
sameVariableUsedThe expression refers to the column's own parameter, such as Width * 2 on the Width column.

An expression on an operator that does not accept one, or on a boolean or dropdown column, is reported as VALIDATION_FAILED in the same way. Treat these reasons as diagnostic text rather than a fixed list to branch on.

Expressions are evaluated when the rules run. A valid expression is evaluated separately for each configuration, using that configuration's parameter values. If it cannot be evaluated for a configuration — for example, it divides by a parameter that is zero there — the rule reports an ERR_EXPRESSION_INVALID status for that configuration rather than an issue on your call (see Rule statuses).

Read the expression back rather than assuming it is the text you wrote. Fusion stores each expression in a standard form, so the value you read back can differ from what you submitted in spacing, number formatting, or units while meaning the same thing. Compare expressions by what they mean — for example, by checking for the parameter names they reference — rather than by exact string.

A literal value can read back as an expression. On a number or text column with an operator that accepts expressions, Fusion stores a plain ConfigurationRuleNumberValue or ConfigurationRuleStringValue as an expression too, so the block reads back with a ConfigurationRuleExpressionValue — a number comes back as an equivalent expression with units, and the text Oak as the text expression 'Oak'. Code that reads values should check for an expression as well as for the literal class:

getBlock = adsk.fusion.ConfigurationRuleGetBlock.cast(ifBlock.condition)

if getBlock is not None:
    expression = adsk.fusion.ConfigurationRuleExpressionValue.cast(getBlock.value)
    if expression is not None:
        app.log(f'Compares against the expression {expression.value}')
    else:
        number = adsk.fusion.ConfigurationRuleNumberValue.cast(getBlock.value)
        if number is not None:
            app.log(f'Compares against {number.value} (internal units)')

Ranges

On a Get block, the Range operator tests whether the column's current value falls within [min, max]. Use a ConfigurationRuleGetRangeValue, and prefer expressions for its bounds: create the range with both bounds unset, then set minExpression and maxExpression. 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
bounds = adsk.fusion.ConfigurationRuleGetRangeValue.create(False, 0.0, False, 0.0)
bounds.minExpression = '400 mm'
bounds.maxExpression = '900 mm'
inRange.value = bounds

create(hasMin, min, hasMax, max) takes only numbers, which is why the expressions are assigned afterwards through the range value's minExpression and maxExpression properties. Each bound is either a number or an expression, never both; setting one replaces the other:

maxExpression, max, and hasMax behave the same way.

# True when the width is between half the length and 900 mm, inclusive.
bounds = adsk.fusion.ConfigurationRuleGetRangeValue.create(False, 0.0, False, 0.0)
bounds.minExpression = 'Length / 2'    # hasMin becomes True; min stays 0.0
bounds.maxExpression = '900 mm'

inRange = adsk.fusion.ConfigurationRuleGetBlock.create()
inRange.columnId = widthColumnId
inRange.operatorType = getOps.RangeConfigurationRuleGetOperator
inRange.value = bounds

When you read a range back, a bound you submitted as a number may be reported as an equivalent expression, with minExpression populated and min reading 0.0. Test which form a bound is in rather than assuming the form you wrote:

bounds = adsk.fusion.ConfigurationRuleGetRangeValue.cast(getBlock.value)

if bounds is not None and bounds.hasMin:
    if bounds.minExpression:
        app.log(f'Lower bound is the expression {bounds.minExpression}')
    else:
        app.log(f'Lower bound is {bounds.min} (internal units)')

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 ConfigurationRuleNumberValue, but a Range 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 and Errors

This section covers problems with the call you just made. For what the rules engine found when the rules last ran, see Rule statuses.

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, an operator/value not valid for the column, or an expression that is empty or invalid (see Expressions).
COLUMN_NOT_SUPPORTEDA Get or Set block targeted a columnId that Fusion recognizes but does not support for rules (see Known limitations) — 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.
BLOCK_ID_CONFLICTA blockId you supplied was already in use by a block outside the rule being written, so Fusion minted a fresh id instead. The rule still applies; read the ids back (see Block IDs).
BLOCK_ID_NOT_PRESERVEDA blockId you supplied did not end up on the rebuilt block. The rule still applies; read the ids back.

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}')


Rule Statuses

A status is what the configuration rules engine found the last time the rules ran, for one rule on one configuration. A rule that fails on four configurations produces four statuses. These are the same errors, warnings, and information the Configuration Rules panel shows against the blocks on the Rules Canvas.

Only problems are reported. A rule that applied cleanly to a configuration reports nothing for it, so the absence of statuses is the signal that all is well.

Statuses answer a different question from issues, and you generally need both:

 IssuesStatuses
AnswersWhat went wrong with the call I just made?What did the engine find when the rules last ran?
WhereThe issues array on the result of the callThe three getStatuses methods
ScopeThe request, or a rule or block in itOne rule on one configuration
MessageDeveloper-facing, not localizedLocalized, ready to display to a user

A modifyRules call can return no issues at all while a rule is switched off on half the design's configurations. Statuses exist only once the rules have run, so a design whose rules have never run returns an empty result — that is not an error.

The Three Queries

All three are read-only, need no Fusion Design Extension, and never trigger a rules run. Each returns a ConfigurationRuleStatusesResult.

MethodAnswersReturns null when
getStatuses()Everything the engine holds, design-wideNever — it always returns a result
getStatusesByRuleId(ruleId)Is this rule healthy?ruleId names no rule the engine is running
getStatusesByConfigurationId(configurationId)Is this configuration healthy?configurationId does not match a row on the design's top configuration table

On the two filtered queries, null and an empty result mean opposite things. An empty statuses array means the subject was found and nothing is wrong — that is the success case. null means there is no such subject. Check for null first: a script that goes straight to result.errorCount == 0 reads a typo'd id as a clean bill of health. Both filtered methods report an error if the id is empty (in Python, a raised exception).

getStatusesByRuleId returns null for an inactive rule as well as an unknown or deleted one, because an inactive rule is excluded from the engine's rule set. Use itemById to tell them apart: it returns the rule when it exists, and ConfigurationRule.isActive then says whether the rule is merely switched off.

The Result Object

A ConfigurationRuleStatusesResult holds the matching statuses, in no guaranteed order, plus errorCount, warningCount, and infoCount. The counts are scoped to the query, so getStatusesByRuleId(ruleId).errorCount is the error count for that one rule. They always sum to the length of statuses, so a health check needs no iteration at all.

A result is a point-in-time snapshot. It does not update when the rules run again or when the design changes — call the query again for current statuses.

The Status Object

PropertyDescription
severityA ConfigurationRuleStatusSeverities value. Use this for triage rather than inspecting code.
codeThe kind of status, such as ERR_CONSTRAIN. Use this to branch on a specific condition.
messageThe localized, human-readable description, ready to display. This is the same text the Configuration Rules panel shows.
ruleIdThe rule this status belongs to. Matches ConfigurationRule.id, so the rule can be fetched with itemById.
configurationIdThe configuration this status applies to. Matches ConfigurationRow.id on Design.configurationTopTable. Empty when the status is not scoped to a single configuration.
blockIdThe block within the rule that raised the status. Matches blockId on a block in the rule's body (see Block IDs). Empty when the status describes the rule as a whole, which is normal, or when the rule's blocks have no ids yet.

Severity

Severity tells you what happened to the rule on the affected configuration. The Error row is the one that matters most: an error does not mean the rule applied and something looked odd, it means the rule is not running there at all.

Value of ConfigurationRuleStatusSeveritiesWas the rule applied?What it means
InformationConfigurationRuleStatusSeverityYesThe configuration is correct as generated. The message is best-practice advice about how the rule is authored or how it applied.
WarningConfigurationRuleStatusSeverityYes, but it may produce unpredictable resultsReview the message and confirm the configuration is what you intended.
ErrorConfigurationRuleStatusSeverityNoThe rule has been turned off for that configuration until the error is resolved. The configuration does not reflect the rule at all, and will not until the reported problem is fixed.

A configuration with no error statuses for a rule had that rule applied to it.

Status Codes

Branch on severity for general triage and on code only when you need a specific condition. Treat any code you do not recognize as an opaque string — future versions may add more.

CodeSeverityMeaning
ERR_CONSTRAINErrorThe aspect is already constrained by another rule or block with a conflicting value, so this rule could not constrain it.
ERR_EXPRESSION_INVALIDErrorAn expression — in a parameter, or in one of the rule's blocks — could not be evaluated for the affected configuration (see Expressions).
ERR_INVALID_INPUTErrorThe block is missing data it requires. Rebuild the block, and contact Autodesk Support if it recurs.
ERR_UNHANDLEDErrorThe block hit an error Fusion could not classify. Contact Autodesk Support.
WARN_CUSTOM_CONSTRAINWarningA non-custom aspect is driven by a custom aspect, so only parent-row values change.
WARN_SNAP_CONSTRAINWarningBoth joint snaps point to the same component, which a joint does not allow.
WARN_INCOMPLETEWarningThe block is an unconfigured placeholder, so it is ignored when the rule runs.
INFO_CONSTRAINInformationAnother block also constrains the aspect, so only the values common to both are valid.
INFO_SNAP_CONSTRAINInformationA joint snap list contains an invalid option; the last valid snap is selected.
INFO_CYCLEInformationThe same aspect is referenced in a Get block and a Set block, which may create a cycle and produce unstable results (see Avoiding cycles).

Reading Every Status

severities = adsk.fusion.ConfigurationRuleStatusSeverities
labels = {
    severities.ErrorConfigurationRuleStatusSeverity: 'ERROR',
    severities.WarningConfigurationRuleStatusSeverity: 'WARNING',
    severities.InformationConfigurationRuleStatusSeverity: 'INFO',
}

result = rulesManager.getStatuses()
app.log(f'{result.errorCount} error(s), {result.warningCount} warning(s), '
        f'{result.infoCount} information.')

for status in result.statuses:
    # blockId is empty when the status describes the rule as a whole.
    block = status.blockId or '-'
    app.log(f'[{labels[status.severity]}] {status.code} rule={status.ruleId} '
            f'configuration={status.configurationId} block={block}: {status.message}')

Checking One Rule

A non-null result with a zero errorCount means the rule is active and was applied to every configuration.

health = rulesManager.getStatusesByRuleId(ruleId)

if health is None:
    # Not a rule the engine is running. itemById separates a deleted rule, where it
    # reports an error, from an inactive one, where it returns the rule.
    try:
        rule = rulesManager.itemById(ruleId).rule
        app.log(f'"{rule.name}" is inactive, so it is not evaluated.')
    except RuntimeError:
        app.log('No rule with that id exists.')
elif health.errorCount == 0:
    app.log('Applied to every configuration.')
else:
    app.log(f'Turned off on {health.errorCount} configuration(s).')

Checking One Configuration

Configuration ids are ConfigurationRow.id values from Design.configurationTopTable. The result also carries every status that names no configuration at all — a rule with an unfilled socket is a problem for this configuration too — so it may contain entries whose configurationId is empty rather than the id you asked for.

rows = design.configurationTopTable.rows

for i in range(rows.count):
    row = rows.item(i)
    health = rulesManager.getStatusesByConfigurationId(row.id)

    if health is None:
        app.log(f'{row.name}: not a row on the top configuration table.')
    elif health.errorCount == 0:
        app.log(f'{row.name}: reflects every rule.')
    else:
        app.log(f'{row.name}: {health.errorCount} rule(s) turned off.')
        for status in health.statuses:
            app.log(f'    {status.code}: {status.message}')

Finding the Block That Raised a Status

When blockId is not empty, it names a block in the rule's body. Read the rule with itemById and walk the body to find it — remembering that an If block nests further blocks in its condition, thenBlocks, elseIfBranches, and elseBlocks. The example below descends through If blocks; extend it through the left, right, and operand of And, Or, and Not blocks if your rules nest conditions.

def findBlock(blocks, blockId):
    """Depth-first search of a statement list for the block with the given id."""
    for block in blocks:
        if block.blockId == blockId:
            return block

        ifBlock = adsk.fusion.ConfigurationRuleIfBlock.cast(block)
        if ifBlock is None:
            continue

        # Everything an If block can nest: its own condition and then-branch, each
        # else-if branch's condition and then-branch, and the else branch.
        nested = list(ifBlock.thenBlocks) + list(ifBlock.elseBlocks)
        if ifBlock.condition is not None:
            nested.append(ifBlock.condition)
        for branch in ifBlock.elseIfBranches:
            nested += list(branch.thenBlocks)
            if branch.condition is not None:
                nested.append(branch.condition)

        found = findBlock(nested, blockId)
        if found is not None:
            return found

    return None

for status in rulesManager.getStatuses().statuses:
    if not status.blockId:
        continue    # Describes the rule as a whole rather than one block.

    rule = rulesManager.itemById(status.ruleId).rule
    block = findBlock(rule.body, status.blockId)
    if block is not None:
        app.log(f'{status.code} raised by {block.objectType} in "{rule.name}".')


Best Practices

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.

Expressions can create the same kind of loop less visibly, because the parameters an expression references are reads too. Setting Width to Length / 2 in one rule while another rule sets Length from Width is a cycle, even though neither Get block mentions the other column. Fusion accepts such rules when you submit them — only an expression that refers to its own column is rejected — and the INFO_CYCLE status does not flag them either, because it only compares the columns of Get and Set blocks. Review the parameters your expressions reference as carefully as the columns your conditions read.


See also