Skip to main content
Question

What are the best practices you follow while creating a query rule in Appian?

A
Anonymous
January 8, 2026

Answer

When creating query rules in Appian, I follow several best practices so they are efficient, reusable, and easy to maintain as the application grows.

My primary goal is to retrieve only the required data, avoid unnecessary database calls, and design query rules that can be reused across interfaces, expression rules, and process models.


In modern Appian applications, prefer a!queryRecordType() for querying Record Types and use a!queryEntity() when direct Data Store Entity access is required.


1.Apply filters at the query level

I apply filters within the query instead of retrieving all records and filtering them afterward with Appian expressions. That keeps unnecessary data retrieval and application-side processing to a minimum.

For example, if an Employee Record Type contains 100,000 records but the interface needs only active employees, I apply the status filter directly.


Code example:

a!queryFilter(
field: recordType!Employee.fields.status,
operator: "=",
value: "Active"
)

Best practices:

  1. Filter records within a!queryRecordType() or a!queryEntity().
  2. Use a!queryLogicalExpression() for multiple conditions.
  3. Use appropriate operators based on the field’s data type.
  4. Validate mandatory filter values before executing the query.


2. Retrieve only the required fields

I don’t retrieve every column when an interface needs only a few fields. If an Employee Record Type contains 20 fields but the interface displays Employee ID, Name, and Department, I select only those three. That cuts processing and improves query performance.


Code example:

fields: {
recordType!Employee.fields.id,
recordType!Employee.fields.name,
recordType!Employee.fields.department
}

Best practices:

  1. Explicitly select required fields.
  2. Avoid a!selectionFields() when all fields are unnecessary.
  3. Avoid unnecessarily fetching large text fields and expensive real-time custom record fields.
  4. Pass selection fields as a rule input when different callers require different columns.


3. Use proper pagination and batch size

Pagination is essential when retrieving large datasets, and how it is handled depends on how the data is used.


Scenario A: Record-backed read-only grid

When a read-only grid directly uses a Record Type or a!recordData(), Appian manages pagination and sorting automatically. We only configure the grid’s pageSize.

Code example:

a!gridField(
label: "Employees",
data: recordType!Employee,
columns: {
a!gridColumn(
label: "Employee Name",
value: fv!row[recordType!Employee.fields.name]
)
},
pageSize: 20,
initialSorts: a!sortInfo(
field: recordType!Employee.fields.id,
ascending: true
)
)

Here:

  1. The grid displays 20 records per page.
  2. Appian manages paging when users navigate between pages.
  3. The grid manages sorting.
  4. No manual startIndex or batchSize is required.


Scenario B: Query rule used outside a grid

If the query is used in an expression rule, process model, dropdown preparation, or other custom logic, I control pagination with a!pagingInfo().

a!pagingInfo(
startIndex: 1,
batchSize: 20
)

Best practices:

  1. Let records-powered grids manage their own pagination.
  2. Use pageSize in a!gridField() to control visible rows.
  3. Use a!pagingInfo() for independent query operations.
  4. Parameterize batch size when different callers need different numbers of records.
  5. Avoid retrieving unnecessarily large datasets.

Important: a!queryRecordType() supports a maximum batch size of 5,000 and does not support batchSize: -1.


4. Avoid unnecessary total count queries

Calculating the total number of matching records adds processing overhead. If a query is used outside a grid and the caller only needs the returned data, I disable total count retrieval.

Code example:

a!queryRecordType(
recordType: recordType!Employee,
fields: {
recordType!Employee.fields.id,
recordType!Employee.fields.name
},
pagingInfo: a!pagingInfo(
startIndex: 1,
batchSize: 20
),
fetchTotalCount: false
)

Best practices:

  1. Use fetchTotalCount: false when the count is unnecessary.
  2. Enable it when a caller requires the total number of matching records.
  3. Pass fetchTotalCount as a Boolean rule input for reusable queries.


Important: For a records-powered grid using a!recordData(), the grid manages its paging and count requirements. We do not manually configure fetchTotalCount inside a!recordData().


For a grid powered by a DataSubset returned from a query, the total count is generally needed for paging controls.


5. Make query rules reusable using parameters

I create reusable query rules rather than separate ones for each requirement. Callers control selection fields, batch size, pagination, filters, and total count.

Example parameters:

fields: ri!selection,

pagingInfo: a!pagingInfo(
startIndex: ri!startIndex,
batchSize: ri!batchSize
),

fetchTotalCount: ri!fetchTotalCount

For example:

  1. A dropdown may request only ID and Name with 100 records.
  2. A dashboard may request Status and Department with 20 records.
  3. A process model may retrieve a single record without calculating the total count.

All three callers can use the same query rule with different inputs.

Best practices:

  1. Parameterize frequently changing query requirements.
  2. Use meaningful rule input names.
  3. Avoid hardcoding selection fields and batch sizes when reusability is needed.
  4. Keep the rule focused and avoid unnecessary parameters.

The final practical example has a complete reusable query rule.


6. Minimize unnecessary database calls

I avoid repeatedly executing queries when the required information can be retrieved together. For example, if I need five employees by ID, I fetch them in one query instead of calling a query rule five times inside a!forEach().

Code example:

Assume ri!employeeIds contains {101, 102, 103, 104, 105}.

a!queryRecordType(
recordType: recordType!Employee,
fields: {
recordType!Employee.fields.id,
recordType!Employee.fields.name
},
filters: a!queryFilter(
field: recordType!Employee.fields.id,
operator: "in",
value: ri!employeeIds
),
pagingInfo: a!pagingInfo(
startIndex: 1,
batchSize: 5
)
)

This retrieves the matching employees in a single query.

Best practices:

  1. Avoid unnecessary queries inside a!forEach().
  2. Retrieve multiple records together when appropriate.
  3. Reuse already-retrieved data.
  4. Avoid unnecessary interface refreshes.
  5. Use relationships when accessing related information.


7. Use Record Type Relationships and Data Sync appropriately

In modern Appian development, I evaluate Record Type Relationships before creating database views solely to join related data.

For example, suppose a Customer Record Type has a one-to-many relationship with Support Ticket records. Rather than retrieving every support ticket for each customer, I can retrieve only the latest three.

Code example:

a!relatedRecordData(
relationship: recordType!Customer.relationships.tickets,
sort: a!sortInfo(
field: recordType!SupportTicket.fields.createdOn,
ascending: false
),
limit: 3
)

This can be used in the relatedRecordData parameter of an appropriate record query or records-powered grid. The example assumes the Customer-to-SupportTicket relationship has already been configured.

Best practices:

  1. Use Record Type Relationships when appropriate.
  2. Retrieve only necessary related fields.
  3. Limit one-to-many related data.
  4. Use Data Sync when suitable for the application’s requirements.
  5. Apply sync filters to high-volume Record Types when appropriate.
  6. Avoid expensive real-time custom fields unless they are needed.

Record Type Relationships are not always faster than database views. The best choice depends on the data source and query requirements.


8. Optimize database indexes

When working with a!queryEntity() or unsynced database-backed Record Types, I check whether appropriate database indexes exist. For example, if users frequently search employees by username, an index may improve query performance.

SQL example:

CREATE INDEX idx_employee_username
ON employee(username);


Best practices:

  1. Review indexes on frequently filtered columns.
  2. Consider indexes for joins and sorting.
  3. Analyze database execution plans before adding indexes.
  4. Avoid creating unnecessary indexes because they increase storage and database write overhead.

For synced Record Types, Appian uses its optimized data service for reads, so source database indexes do not directly improve queries against already-synced data.


9. Monitor and test query performance

I test query rules with realistic data volumes and different input values. Appian provides Query Editor, Performance Details, and Query Performance monitoring to help identify slow queries.

Example: inspect a query result

a!localVariables(
local!result: a!queryRecordType(
recordType: recordType!Employee,
fields: {
recordType!Employee.fields.id,
recordType!Employee.fields.name
},
pagingInfo: a!pagingInfo(
startIndex: 1,
batchSize: 20
)
),

a!map(
success: local!result.success,
rowsReturned: length(local!result.data),
errorCode: local!result.errorCode
)
)

This shows whether the query succeeded and how many records came back. It does not measure execution time, so for timing and bottlenecks I use Appian’s built-in performance tools. I compare query execution times before and after optimization.


10. Follow naming conventions and validate inputs

I use meaningful rule names and validate mandatory inputs to prevent unexpected query behavior. For example, if employee status is required, I avoid executing a broad query when the status is missing.

Code example:

if(
a!isNullOrEmpty(ri!status),
{},
rule!APP_QR_GetEmployees(
selection: {
recordType!Employee.fields.id,
recordType!Employee.fields.name
},
batchSize: 20,
startIndex: 1,
fetchTotalCount: false,
status: ri!status,
department: null
).data
)

If the status is missing, the caller returns an empty list instead of executing the query.

Best practices:

  1. Follow naming conventions such as APP_QR_GetEmployees.
  2. Define appropriate rule input types.
  3. Validate mandatory inputs.
  4. Return consistent result structures.
  5. Keep query logic simple and maintainable.


Final practical example: an optimized, reusable query rule

Consider an Employee Management application where different components need employee information. We will implement two approaches:

  1. A reusable query rule for expression rules, process models, dropdown preparation, and other custom queries.
  2. A records-powered grid that manages its own pagination automatically.


Part A: Create a reusable query rule

Expression rule: APP_QR_GetEmployees

Configure these rule inputs:

Rule input

Data type

Purpose

ri!selection

Any Type

Fields to retrieve

ri!batchSize

Integer

Number of records

ri!startIndex

Integer

Starting position

ri!fetchTotalCount

Boolean

Whether total count is needed

ri!status

Text

Mandatory status filter

ri!department

Text

Optional department filter

Final query rule code:

a!localVariables(
/* Default to 20 and cap requests at 100 rows */
local!batchSize: if(
a!isNullOrEmpty(ri!batchSize),
20,
if(
ri!batchSize < 1,
20,
if(ri!batchSize > 100, 100, ri!batchSize)
)
),

/* Ensure valid start index */
local!startIndex: if(
a!isNullOrEmpty(ri!startIndex),
1,
if(ri!startIndex < 1, 1, ri!startIndex)
),

/* Default total count to false */
local!fetchTotalCount: if(
a!isNullOrEmpty(ri!fetchTotalCount),
false,
ri!fetchTotalCount
),

/* Prevent query if mandatory filter is missing */
if(
a!isNullOrEmpty(ri!status),

a!map(
success: false,
data: {},
totalCount: -1,
startIndex: local!startIndex,
batchSize: local!batchSize,
errorCode: "STATUS_REQUIRED"
),

a!queryRecordType(
recordType: recordType!Employee,

/* Retrieve only requested fields */
fields: if(
a!isNullOrEmpty(ri!selection),
{
recordType!Employee.fields.id,
recordType!Employee.fields.name
},
ri!selection
),

/* Filter at the query level */
filters: a!queryLogicalExpression(
operator: "AND",
filters: {
a!queryFilter(
field: recordType!Employee.fields.status,
operator: "=",
value: ri!status
),
a!queryFilter(
field: recordType!Employee.fields.department,
operator: "=",
value: ri!department,
applyWhen: a!isNotNullOrEmpty(
ri!department
)
)
}
),

/* Dynamic pagination and sorting */
pagingInfo: a!pagingInfo(
startIndex: local!startIndex,
batchSize: local!batchSize,
sort: a!sortInfo(
field: recordType!Employee.fields.id,
ascending: true
)
),

/* Optional total count */
fetchTotalCount: local!fetchTotalCount
)
)
)

The maximum batch size of 100 is an application-specific safeguard. Appian’s own limit is 5,000. The validation branch returns a custom failure map with STATUS_REQUIRED, so callers should check success before using the data.


Part B: Calling the query rule from an expression

Suppose a dropdown requires 50 active employees from the IT department. It only needs ID and Name, and the total count is unnecessary.

rule!APP_QR_GetEmployees(
selection: {
recordType!Employee.fields.id,
recordType!Employee.fields.name
},
batchSize: 50,
startIndex: 1,
fetchTotalCount: false,
status: "Active",
department: "IT"
)

Illustrative result, assuming 80 employees match the filters:

Property

Result

data

First 50 matching employees

startIndex

1

batchSize

50

totalCount

Not calculated

success

true

If the dropdown needs to offer all 80 employees, the caller must retrieve the additional results or use a searchable selection component with an appropriate data source.


Part C: Creating a records-powered grid

An Employee Grid doesn’t need the query rule above for pagination. It can use the Employee Record Type directly through a!recordData().

Final grid code:

a!gridField(
label: "Active Employees",

/* Records-powered data source */
data: a!recordData(
recordType: recordType!Employee,

/* Filter at the data source */
filters: a!queryFilter(
field: recordType!Employee.fields.status,
operator: "=",
value: "Active"
),

/* Retrieve only the required fields */
fields: {
recordType!Employee.fields.id,
recordType!Employee.fields.name,
recordType!Employee.fields.department,
recordType!Employee.fields.status
}
),

columns: {
a!gridColumn(
label: "Employee ID",
value: fv!row[recordType!Employee.fields.id],
sortField: recordType!Employee.fields.id
),
a!gridColumn(
label: "Employee Name",
value: fv!row[recordType!Employee.fields.name],
sortField: recordType!Employee.fields.name
),
a!gridColumn(
label: "Department",
value: fv!row[recordType!Employee.fields.department],
sortField: recordType!Employee.fields.department
),
a!gridColumn(
label: "Status",
value: fv!row[recordType!Employee.fields.status]
)
},

/* Grid automatically handles paging */
pageSize: 20,

/* Initial sorting */
initialSorts: a!sortInfo(
field: recordType!Employee.fields.id,
ascending: true
)
)

How this grid works:

  1. Appian retrieves the records required for the displayed page.
  2. The grid displays 20 employees per page.
  3. When the user navigates between pages, Appian handles paging automatically.
  4. Sorting is managed through the grid.
  5. Filters are applied through a!recordData().
  6. The grid retrieves only the fields configured in the fields parameter.
  7. No manual startIndex, batchSize, or fetchTotalCount is needed.


Important: If a grid instead uses a reusable a!queryEntity() rule, Appian supports passing fv!pagingInfo to the rule’s PagingInfo input. This allows the grid to supply its page size and sorting information. For an a!queryRecordType() result used as grid data, follow Appian’s documented DataSubset conversion approach using a!dataSubset().

Summary: Choosing the appropriate approach

Requirement

Recommended approach

Read-only grid using a Record Type

a!gridField() with Record Type or a!recordData()

Grid pagination

Let a!gridField() manage it

Query inside an expression rule

a!queryRecordType()

Query inside a process model

Reusable query rule

Dynamic field selection

ri!selection

Dynamic batch size for custom queries

ri!batchSize

Optional total count for custom queries

ri!fetchTotalCount

Data Store Entity query used in a grid

Pass fv!pagingInfo through the query

Large related datasets

Record Type Relationships with appropriate limits

Interview takeaway

When creating query rules in Appian, I filter at the query level, retrieve only the necessary fields, paginate, skip unnecessary total count queries, and minimize repeated database calls.

I make reusable query rules configurable by passing selection fields, batch size, start index, and fetchTotalCount as rule inputs. Different callers can then customize their queries without changing the underlying expression rule.

For a read-only grid on a Record Type, I let Appian handle pagination through a!gridField() and use its built-in capabilities wherever possible, instead of managing paging variables manually.

I also consider Record Type Relationships, Data Sync, appropriate database indexes, and performance monitoring.


Official Appian documentation

  1. Record Type Query Performance Best Practices
  2. Configuring the Read-Only Grid
  3. Read-Only Grid Component
  4. a!recordData() Function
  5. a!queryRecordType() Function

Check yourself

Question 1 of 5, on what the answer above covers.

  1. 1.
    What batch size limits apply when you use a!queryRecordType()?
SeniorJuniorRecordsQuery Expressions