PracticeVideosBlogFlashcards

9.2 Local Variables & Dynamic Refresh Behaviors

Key Takeaways

  • State management inside SAIL interfaces is orchestrated using a!localVariables(), creating encapsulated, interface-scoped variables (local!) that persist across user interactions without requiring process variables.

  • Local variable declarations evaluate sequentially from top to bottom, enabling downstream variables to reference and build upon values initialized in upstream variables within the same scope block.

  • The a!refreshVariable() function provides granular reactivity control, dictating whether a local variable recalculates on every user interaction (refreshAlways), when specific variables change (refreshOnVarChange), or on a recurring timer (refreshInterval).

  • refreshAlways can re-run heavy queries on unrelated server-side interface interactions; field refresh timing determines which events cause saves.

  • Cascading user interface patterns (such as Country -> State -> City dependent dropdowns) require resetting dependent child variables to null in saveInto to prevent stale, invalid data states.

Last updated: October 2026

Local Variables & Dynamic Refresh Behaviors

Quick Summary: In Appian SAIL, interface state is encapsulated using a!localVariables(), which defines temporary, UI-scoped variables prefixed with the local! domain. To prevent unnecessary server round-trips and optimize performance, developers control variable reactivity using a!refreshVariable(). By configuring targeted refresh triggers (refreshOnVarChange), recurring polling intervals (refreshInterval), and caching strategies, developers construct dynamic, high-performance interfaces such as cascading dropdowns and live operational queues.


Interface State Management with a!localVariables()

During a user's interaction with an interface, the UI must store transient state—such as active search queries, selected filter checkboxes, temporary calculations, or pagination offsets—that does not necessarily belong in the permanent business database or workflow process variables. In Appian, this ephemeral state is managed through a!localVariables().

a!localVariables(
  local!searchTerm: "",
  local!selectedDepartmentId: null,
  local!employeeList: rule!EMP_qrGetEmployees(
    departmentId: local!selectedDepartmentId,
    searchTerm: local!searchTerm
  ),
  
  /* Interface Layout and Components */
  a!formLayout(
    label: "Employee Directory",
    contents: { ... }
  )
)

Scoping and Encapsulation Rules

  1. Interface Lifespan: Local variables exist strictly within the runtime lifespan of the rendered interface. When the user navigates away, submits the form, or closes the browser window, local state is no longer available to later forms; explicitly pass or persist anything that must survive.
  2. Lexical Scoping: Variables declared within an a!localVariables() block are accessible only within that specific block and its nested child expressions. Outer interfaces cannot directly inspect the internal local variables of an embedded child interface rule.
  3. Sequential Evaluation Order: Local variables evaluate sequentially from top to bottom. A variable declared on line 4 can reference variables declared on lines 1, 2, and 3. However, referencing a downstream variable declared further down within the same block triggers an evaluation error:
/* VALID: local!taxAmount successfully references local!subTotal */
a!localVariables(
  local!subTotal: 100.00,
  local!taxAmount: local!subTotal * 0.08,
  local!grandTotal: local!subTotal + local!taxAmount,
  ...
)

/* INVALID: local!taxAmount fails because local!subTotal has not yet evaluated */
a!localVariables(
  local!taxAmount: local!subTotal * 0.08,  /* ERROR: Variable not found */
  local!subTotal: 100.00,
  ...
)

The Mechanics of a!refreshVariable()

By default, in modern Appian SAIL, when a local variable references other local variables or rule inputs in its initialization expression, it automatically re-evaluates whenever those referenced variables change (refreshOnReferencedVarChange: true). However, complex enterprise interfaces require fine-grained control over when, how, and why a variable recalculates. Developers achieve this using a!refreshVariable().

local!customerOrders: a!refreshVariable(
  value: rule!ORD_qrGetOrdersByCustomer(
    customerId: local!selectedCustomerId
  ),
  refreshAlways: false,
  refreshOnVarChange: {local!selectedCustomerId},
  refreshOnReferencedVarChange: true,
  refreshInterval: null,
  refreshAfter: { "RECORD_ACTION" }
)

Detailed Analysis of a!refreshVariable() Parameters

ParameterTypeDefaultBehavioral Impact & Technical Mechanics
valueAny(Required)The core expression or initial value to be computed, stored, and conditionally refreshed.
refreshAlwaysBooleanfalseWhen set to true, the variable recalculates on each interface save/refresh interaction across the entire interface that triggers interface evaluation, and on interval refreshes.
refreshOnVarChangeAny / List(None)Explicitly designates one or more variables to watch. Whenever any variable in this list changes value, the value expression recalculates.
refreshOnReferencedVarChangeBooleantrueWhen true, automatically re-evaluates value if any variable referenced within value changes. Setting this to false creates an initialized cache that holds its initial value regardless of upstream changes.
refreshIntervalNumbernullUses supported minute values: 0.5, 1, 2, 3, 4, 5, 10, 30, or 60. The variable cannot be a save target when interval refresh is configured.
refreshAfterList of TextnullConfigures the variable to refresh after specific external events occur, such as "RECORD_ACTION" completing inside a dialog.
                    a!refreshVariable() Decision Flowchart
                    =====================================

         User interaction or event occurs in interface
                             │
                             ▼
                 Is refreshAlways == true?
                   ├──► YES ──► Recalculate 'value' immediately
                   │
                   └──► NO
                         │
                         ▼
         Did any variable in refreshOnVarChange change?
                   ├──► YES ──► Recalculate 'value'
                   │
                   └──► NO
                         │
                         ▼
         Is refreshOnReferencedVarChange == true
         AND did a referenced variable change?
                   ├──► YES ──► Recalculate 'value'
                   │
                   └──► NO
                         │
                         ▼
         Has refreshInterval timer elapsed?
                   ├──► YES ──► Recalculate 'value'
                   │
                   └──► NO ──► Retain cached 'value' (NO SERVER CALL)

Memory & Query Performance Best Practices

In enterprise low-code systems, improper refresh configuration is one possible source of interface latency. Developers must adhere to four foundational performance guidelines:

1. Avoid refreshAlways: true on Database Queries

Wrapping a database query or an integration rule inside refreshAlways: true is one of the most severe anti-patterns in Appian development. A query configured with refreshAlways: true may re-run on unrelated save events. Text fields ordinarily save on unfocus unless configured for keypress refresh, so twenty typed characters do not universally mean twenty server queries. This spikes CPU utilization, exhausts relational database connection pools, and introduces noticeable UI lag.

2. Snapshot Caching for Static Reference Data

Reference data that remains static during a user session (such as country lists, order status types, or tax code tables) should be loaded once upon interface initialization and cached. For a load-once snapshot, disable referenced-variable refresh and leave other refresh triggers disabled:

local!countryReferenceList: a!refreshVariable(
  value: rule!REF_qrGetAllCountries(),
  refreshAlways: false,
  refreshOnReferencedVarChange: false
)

3. Guarding Queries with Conditional Execution

Never execute child queries when parent parameters are null. Always wrap dependent queries in if() or a!isNullOrEmpty() checks to prevent firing invalid queries against the database:

local!stateList: a!refreshVariable(
  value: if(
    a!isNullOrEmpty(local!selectedCountryId),
    {},
    rule!GEO_qrGetStatesByCountry(countryId: local!selectedCountryId)
  ),
  refreshOnVarChange: {local!selectedCountryId}
)

Real-World Scenario: Cascading Dropdowns (Country -> State -> City)

A common requirement on the ACD101 exam is designing a multi-tier cascading dropdown hierarchy where selecting a Country filters the available States, and selecting a State filters the available Cities.

The Complete Cascading Implementation Pattern

a!localVariables(
  local!selectedCountryId: null,
  local!selectedStateId: null,
  local!selectedCityId: null,
  
  /* State list refreshes ONLY when Country selection changes */
  local!states: a!refreshVariable(
    value: if(
      a!isNullOrEmpty(local!selectedCountryId),
      {},
      rule!GEO_qrGetStatesByCountry(countryId: local!selectedCountryId)
    ),
    refreshOnVarChange: {local!selectedCountryId}
  ),
  
  /* City list refreshes ONLY when State selection changes */
  local!cities: a!refreshVariable(
    value: if(
      a!isNullOrEmpty(local!selectedStateId),
      {},
      rule!GEO_qrGetCitiesByState(stateId: local!selectedStateId)
    ),
    refreshOnVarChange: {local!selectedStateId}
  ),
  
  a!formLayout(
    label: "Geographic Location Intake",
    contents: {
      a!dropdownField(
        label: "Country",
        choiceLabels: {"United States", "Canada", "United Kingdom"},
        choiceValues: {1, 2, 3},
        placeholderLabel: "--- Select Country ---",
        value: local!selectedCountryId,
        saveInto: {
          local!selectedCountryId,
          /* CRITICAL: Reset downstream child variables to prevent stale state */
          a!save(local!selectedStateId, null),
          a!save(local!selectedCityId, null)
        }
      ),
      a!dropdownField(
        label: "State / Province",
        choiceLabels: local!states.name,
        choiceValues: local!states.id,
        placeholderLabel: "--- Select State ---",
        value: local!selectedStateId,
        saveInto: {
          local!selectedStateId,
          /* Reset downstream city when state changes */
          a!save(local!selectedCityId, null)
        },
        disabled: a!isNullOrEmpty(local!selectedCountryId)
      ),
      a!dropdownField(
        label: "City",
        choiceLabels: local!cities.name,
        choiceValues: local!cities.id,
        placeholderLabel: "--- Select City ---",
        value: local!selectedCityId,
        saveInto: local!selectedCityId,
        disabled: a!isNullOrEmpty(local!selectedStateId)
      )
    }
  )
)

Why Resetting Downstream Selections in saveInto is Mandatory

Consider what occurs if the developer omits a!save(local!selectedStateId, null) from the Country dropdown's saveInto:

  1. The user selects United States (ID: 1).
  2. The user selects California (State ID: 105).
  3. The user realizes they made a mistake and changes the Country dropdown to Canada (ID: 2).
  4. The local!states variable re-evaluates, fetching Canadian provinces (such as Ontario, Quebec, and British Columbia).
  5. However, local!selectedStateId still retains its previous value of 105 (California)!
  6. Because the value 105 does not exist within the new choice values of Canadian provinces, Appian displays an error or displays an invalid blank selection state.
  7. Golden Rule: Whenever a parent selection changes, all dependent child variables must be explicitly set to null in the parent's saveInto array.

Exam Traps & Refresh Guidelines

Warning

Common Exam Trap 1: Believing local variables persist across process model task transitions. They do not. local! variables exist solely in client-server UI memory during the active session. If a user completes a task form and advances to a subsequent user input task, all local variables from the first form vanish. Any data that must carry forward into the process instance must be stored in a Rule Input (ri!) mapped to a Process Variable (pv!).

Warning

Common Exam Trap 2: Legacy load() and with() functions. In older versions of Appian, developers used load() (evaluate once on load) and with() (re-evaluate on every interaction). Prefer a!localVariables(). For new interface state management, a!localVariables() combined with a!refreshVariable() is the unified standard.

Tip

Polling Best Practice: When using refreshInterval to power live dashboards, choose one of the supported intervals. The smallest supported interval is 0.5 minutes; justify polling against freshness and load requirements rather than inventing a smaller value.

Source checked 2026-10-18: Appian refresh configuration.

Test Your Knowledge

A developer observes that an interface exhibits severe latency. Analysis reveals that an expensive database query assigned to a local variable executes every time a user types a single character in an unrelated text field. What configuration is the primary cause?

A

The local variable was declared using refreshOnReferencedVarChange: false.

B

The local variable is bound to an expression rule that uses activity chaining.

C

The interface is missing an enclosing a!formLayout() wrapper.

D

The local variable is configured with refreshAlways: true inside a!refreshVariable().

Test Your Knowledge

In a cascading dropdown implementation (Region -> Department), what critical action must be executed in the Region dropdown's saveInto parameter when a user selects a new Region?

A

Reset the dependent Department local variable to null to avoid holding an invalid or stale selection.

B

Execute an immediate write to the relational database to commit the new Region.

C

Set refreshAlways: true on the entire form layout container.

D

Re-evaluate all process model variables mapped to the interface.

Test Your Knowledge

Which parameter of a!refreshVariable() should be configured to periodically poll the server and refresh a live task queue counter every 30 seconds without requiring user interaction?

A

refreshAlways: 30

B

refreshOnVarChange: 0.5

C

refreshInterval: 0.5

D

refreshTimeout: 30

Sections you finish are checked off in the contents.