Service-Backed Record Tutorial

The walk-through on this page will help you create your first service-backed record.

Use the provided API to understand how the configurations work. Then, try it with your own data or API. Keep in mind, the final configurations will need to change to fit your data type's field names and types.

For this tutorial, you will be required to create custom data types and a new record type. If you do not have permissions to do so, speak with your system administrator.

The content below assumes a basic familiarity with interfaces and focuses more on the specifics of configuring the data source and list view. Consider going through the Interface Tutorial and Process-Backed and Entity-Backed Record Tutorial and taking a look at the Record Design page before proceeding.

The content below also assumes that you have a basic familiarity with using APIs. Note that you will need to register for a freely available, open source API to complete this tutorial.

Setup

Service-backed records differ from other types of records in that service backed records accept expressions as both their sources of data and their user filters. These two distinctions make service-backed records a powerful means of bringing external data into Appian as records. The distinctions also mean additional steps and nuances, unique to service-backed records. In this tutorial, we will show you the best ways to handle these additional steps and provide you with enough information to get your service-backed record ready for production.

Throughout the tutorial, you will be asked to create new rules and interfaces. After creating a new interface or expression, always test it to make sure that you did not miss any inputs or portions of the interface.

Create the Appian Tutorial Application

The Appian Tutorial application is used to contain the design objects created while working through this tutorial.

The tutorial application only needs to be created once. If you have already created the tutorial application, skip the steps below.

To create the Appian Tutorial application

  1. Log in to Appian Designer (for example, myappiansite.com/suite/design).
  2. Click New Application.
  3. In the Name field, type Appian Tutorial.
  4. Optionally, in the Description field, add a short description.
  5. Click Create.

The application contents view displays. Right now the application is empty. Each design object that you create during the course of this tutorial will appear in this list and be associated with the tutorial application.

Register for API Access

The API used in this tutorial is currently being updated and does not require an API. This will change in the near future, but for the time being, you can ignore any mention of an API key.

If you already have access to the Sunlight Foundation API, you can skip to the next section. Otherwise, register an account here. It is free of charge and takes less than a minute. Once you have registered, you will receive an API key by e-mail (it may go to your spam folder). The key will be a string of 32 numbers and letters. It will look something like this: xxxxxxxxxxxxxxxxxxxxxxxxxxxx64dd.

Build the Record List

In the next four subsections, you will create the different components required for a service-backed record list view. The list view you will create will not be complete, however, because the user will be unable to navigate to a specific record. That aspect of the list view will be completed in Enable Navigation to Records.

Create the Source Data Type

To start, you will need a custom data type that the source expression can map to. Although the data can come from anywhere, it needs to fit into an entity type that can be referenced by the record definition.

The source data type acts as a container for the record data. Often, when dealing with web services, you will have one call that can search for records and return list view data and one or more calls to get data for the record views. Other times, you may have just one call. In either case, the source data type should be tailored towards the list view.

For this tutorial, you will build a record of US legislators. To get the data, you will query the Open States API provided by the Sunlight Foundation. If you look through the API documentation, you can find the fields that the query will return. When you create a service-backed record with your own data, you will want to analyze the returned fields and create a data type that maps to them appropriately. For this example, we've done that for you. So all you need to do is download the XSD and import it into your application from the application view. After importing the XSD, you should have a data type called Legislator that has the following fields:

  • bioguide_id (text, primary key)
  • first_name (text)
  • middle_name (text)
  • last_name (text)
  • gender (text)
  • chamber (text)
  • state_name (text)
  • district (text)
  • party (text)
  • website (text)
  • term_start (text)
  • term_end (text)

Note: Best practice suggests camel casing for field names, but because this needs to map directly to the data returned by a web service, we will use the exact field names of the web service result.

Create the Source Expression

Now that you have an appropriate source data type, you can construct a rule to get the data. For this tutorial, you will be using an Integration to retrieve the data, a!fromJSON() to convert the response to an Appian type, and then cast() to convert that to the source data type.

Note: When using your own data, if an Integration does not meet your specific needs, you may want to check out our connector functions.

For this example, you need to first create three new constants to hold text that you will be using in multiple places. To create new constants, open the application in Appian Designer.

The first constant needs to hold your API key. Create a new constant called SUNLIGHT_LABS_API_KEY. Set Type to text and copy and paste your API key into the value field. If you have not yet gotten your API key, go back to the Register for API Access section. For your own services, this value might be environment specific because you may have a development API key and a production API key. For this tutorial, we won't worry about that, but it's something to keep in mind.

The second constant will hold the API URL. You will be creating two HTTP integrations in this tutorial, and both will use the same URL. Create a new constant called LEGISLATOR_API_URL. Set it to type text and copy and paste the following text into the value field: https://congress.api.sunlightfoundation.com/legislators. Like the last constant, this would likely be environment specific in a real-world scenario.

The third and final constant will detail which fields to retrieve from the service. This tutorial only uses a subset of the fields available in the service, and it is good practice to only get the fields that you are using. Getting fewer is generally faster and easier to work with. Additionally, the API documentation requests you do this because it reduces the load on the service. Create a new constant called LEGISLATOR_SERVICE_FIELDS_TO_RETRIEVE. Set its Type to text (not an array), and copy and paste the following text into the value field: bioguide_id,first_name,middle_name,last_name,gender,chamber,state_name,district,party,website,term_start,term_end.

Now that you have the required constants, you need to create a rule to retrieve the record list data. Create a new Integration called getLegislatorRecordListData. To configure the integration, follow the steps below:

  1. For the URL, click Edit as Expression, and provide your URL constant as the value in the expression box: cons!LEGISLATOR_API_URL.
  2. Add the following five parameters to the integration. To add a parameter, click Add Parameter. Make sure you test the integration after adding each new parameter; it should always succeed.
    1. Add the API key
      • Set the Name to apikey
      • For the Value, click Edit as Expression, and provide your API key constant as the value in the expression box: cons!LEGISLATOR_API_URL
    2. Add the fields we want to retrieve
      • Set the Name to fields
      • For the Value, click Edit as Expression, and provide your fields constant as the value in the expression box: cons!LEGISLATOR_SERVICE_FIELDS_TO_RETRIEVE
    3. Add the start index
      • Set the Name to page
      • Set the Value to 1. We will come back and make this dynamic when we paging and sorting.
    4. Add the batch size
      • Set the Name to per_page
      • Set the Value to 50. We will come back and make this dynamic when we paging and sorting.
    5. Add sorting
      • Set the Name to order
      • Set the Value to bioguide_id__asc. We will come back and make this dynamic when we paging and sorting.
  3. Click Save to save your changes

After you create an Integration to get the data, you will want a rule that handles user interaction with the record list. This rule will wrap the above Integration to handle paging and sorting, search, and user filters and will put the data into a form the record can understand. From now on, we will reference this wrapper rule as the source expression.

For service-backed records, the source expression needs to return a DataSubset, created using a!dataSubset(). When using a!dataSubset(), the data field needs to hold the data that populates the list view, and the identifiers field needs to hold the unique identifiers of the data (bioguide_id in our case). The remaining fields in a!dataSubset() pertain to paging information, which we will address later. For now, you can rely on the services default pagination.

To get started on your source expression, create a new rule called legislatorsSourceExpression, and copy and paste the following into the new rule's definition:

with(
  
  /* Calls our Integration and converts the response to a dictionary */
  local!response: a!fromJson(rule!getLegislatorRecordListData().result.body),
  
  /* Get the actual data */
  local!data: local!response.results,
  
  /*
    Generate the data subset. bioguide_id is our unique identifier.
    Use placeholder configurations for paging.
  */
  a!dataSubset(
    data: local!data,
    identifiers: index(local!data, "bioguide_id", {}),
    totalCount: local!response.count,
    /* These values are placeholders and hard-coded into our Integration */
    startIndex: 1,
    batchSize: 50,
    sort: a!sortInfo(field: "bioguide_id", ascending: true)
  )
)

Make sure to save the expression rule.

Create the Record Type

Now that you can get the record list data, you can create the record type. Create a new record type called Legislator with the plural name Legislators, then do the following from the Record Designer:

  1. Under Data, change Source to Expression.
  2. In the Data Type field, provide the source data type (Legislator). If you start typing, the form will suggest data types.
  3. In the Expression field, call your rule containing the source expression. Your Expression should be rule!legislatorsSourceExpression().
  4. For Record List, select "Grid" as the List Style
  5. Click Edit Record List
  6. Make the following edits to the grid view. Making a grid view for a service-backed record is just like configuring the grid for an entity-backed record.
    1. Replace the middle_name column with a column for last_name
    2. Add columns for chamber, state, and party
    3. Update the column labels to be proper headings by removing the underscores and updating the words to title casing.
    In the end, your grid should look something like this:
  7. From the application, create a new expression rule called legislatorsRecordListTitle.
  8. Add the following inputs to the rule:
    • first_name (text)
    • middle_name (text)
    • last_name (text)
    • party (text)
  9. Copy & paste the expression below as the rule definition
  10. ri!last_name & ", " & ri!first_name 
    & if(
        isnull(ri!middle_name),
        "",
        " " & ri!middle_name
      )
    & " (" & ri!party & ")"
  11. Call rule!legislatorsRecordListTitle in the Record Title field, passing in the necessary record fields
  12. rule!legislatorsRecordListTitle(
      first_name: rf!first_name,
      middle_name: rf!middle_name,
      last_name: rf!last_name,
      party: rf!party
    )
  13. Click Summary under Views
  14. Type ={} as the Interface definition in the newly-opened dialog
  15. Note: The Summary definition is a placeholder
  16. Click Save to save your changes

If you have configured everything correctly, you should be able to find the new record type in the Records tab on Tempo. And, if you navigate to the record type, it should look something like this:

But, in its current state, users cannot interact with the record. You still must configure navigation to specific records, search, and user filters.

Enable Navigation to Records

For service-backed records, you need to configure in detail how Appian handles user-interaction with the record list and links to specific records. This allows you to create records with data sourced from anywhere. It also means a little more complexity in configuration. In this section, you will enable users to navigate to specific records. In the following section, you will enable users to search and filter the records in the record list.

Configure the Source Expression for Navigation

No matter how you get the data, you need to configure the source expression to enable navigation to specific records. To do so, you will need to analyze rsp!query.

rsp!query is a data type of type Query, available only in the source expression of the record type definition. It contains information about the user's current interaction with the record type. By analyzing rsp!query, you can determine whether the source expression should return a list of data or a single, specific data point.

Correctly parsing rsp!query can be difficult because there are many ways a user can interact with a record list. So, we've provided an expression for you that can take in the data type and return a Dictionary with all of the information you will need for most use cases. To use the expression, create a new rule called parseRspQuery. Give it one input, rspQuery of type Query, and define it with the following:

with(

  /* 
   * Get the first data type containing one or more of the following:
   * Logical Expression, Query Filter, Search Query. It is called
   * a logicalExpression|filter|search -- abbreviated as LFS
  */
  local!parentLFS: index(ri!rspQuery, "logicalExpression|filter|search", null),

  /* 
   * Check to see if the above data type has a nested LFS in it. This can happen
   * if the user has clicked multiple filters. If all filters were in a single
   * set, for example a group of states on a multi select filter, we want to use the
   * parent as the main LFS so that the expression correctly consolidates the filters.
   */
  local!childLFS: if(
    index(local!parentLFS, "operator", "") = "OR",
    null,
    index(local!parentLFS, "logicalExpression|filter|search", null)
  ),

  /*
    Get the LFS containing the filter(s) and/or search.
    Basically, if the child exists, it will contain all the information.
    If it doesn't then we want to parent.
  */
  local!mainLFS: if(isnull(local!childLFS), local!parentLFS, local!childLFS),

  /* Get runtime types of LFS (type!QueryFilter or type!Search) */
  local!types: cast(
    'type!{http://www.appian.com/ae/types/2009}Type?list',
    a!forEach({local!mainLFS}, runtimetypeof(fv!item))
  ),

  /*
    Get the search data type, if there is one. It has its own type that we can look for.
  */
  local!searchData: index({local!mainLFS}, where(local!types='type!{http://www.appian.com/ae/types/2009}Search'), null),

  /* Gets filter data types if there are any. We can find them by their type. */
  local!singleSelectFilters: index({local!mainLFS}, where(local!types='type!{http://www.appian.com/ae/types/2009}QueryFilter'), {}),
  
  /* Gets logical expression that, for our case, could hold multiple state filters */
  local!multiSelectFilterLogicalExpressions: index(
    {local!mainLFS},
    where(local!types='type!{http://www.appian.com/ae/types/2009}LogicalExpression'),
    {}
  ),
  
  /*
   * Converts multiple filters to single filter for each multiple filter.
   * This assumes that all options in a single filter operate on the same field
   */
  local!multiSelectSingleFilter: if(
    length({local!multiSelectFilterLogicalExpressions}) = 0,
    {},
    a!forEach(
      local!multiSelectFilterLogicalExpressions,
      a!queryFilter(
        field: fv!item.'logicalExpression|filter|search'[1].field,
        operator: "in",
        value: fv!item.'logicalExpression|filter|search'.value
      )
    )
  ),

  /* Gets paging info */
  local!paging: index(ri!rspQuery, "pagingInfo", topaginginfo(1,100)),

  /* 
    Creates and returns a Dictionary containing the pagingInfo, search text, and filters 
	currently applied to the record list.
  */
  {
    pagingInfo: local!paging,
    searchText: tostring(
      index(
        cast('type!{http://www.appian.com/ae/types/2009}Search',local!searchData), 
        "searchQuery", 
        ""
      )
    ),
    filters: cast(
      'type!{http://www.appian.com/ae/types/2009}QueryFilter?list',
      {
        local!singleSelectFilters,
        local!multiSelectSingleFilter
      }
    )
  }
)

Note: This rule will be adequate for most use cases but may need to be modified for use outside of the tutorial. See also: a!forEach(), cast(), where()

You can now modify the source expression, legislatorsSourceExpression, to account for user-interaction. Start by adding a new rule input rspQuery of type Query and changing the Expression field in your record type to pass rsp!query to your rule.

Next, add a local variable to hold the value returned by parseRspQuery.

with(
+ local!queryInformation: rule!parseRspQuery(ri!rspQuery),
  
  /* Calls our rule that hits the service. */
  local!response: rule!getLegislatorRecordListData(),
  
  /* Get the actual data */
  local!data: local!response.results,
  
  /*
    Generate the data subset. bioguide_id is our unique identifier.
    User placeholder configurations for paging and sorting.
  */
  a!dataSubset(
    data: local!data,
    identifiers: index(local!data, "bioguide_id", {}),
    totalCount: local!response.count,
    startIndex: 1,
    batchSize: 50,
    sortInfo: a!sortInfo("bioguide_id", true)
  )
)

local!queryInformation holds three fields: pagingInfo, searchText, and filters. You can use this information to customize your web service call based on what the user requests. The first customization should be to make a different call when a user attempts to access a specific record.

First, you'll need an Integration that can retrieve a record based on its unique identifier. In this case, that means a call to get a Legislator given its bioguide_id. You could create a single Integration to handle all calls, but this could create issues in more complex record definitions or when using different services. A new Integration is a better choice. So, to save a few steps, create a copy of our previous integration, getLegislatorRecordListData, by choosing Duplicate existing integration from the Create Integration menu, and call the new Integration getLegislatorById. Now, follow the steps below to finish configuring the Integration:

  1. Add one input, bioguide_id, of type Text.
  2. Remove the query parameters with the following names: page, per_page, and order. They are for sorting and pagination, but since we will only ever get one Legislator from this call, they are unnecessary.
  3. Click Add Parameter
    • Set the Name to bioguide_id
    • For the Value, click Edit as Expression, and in the expression box, paste the expression below. The service we use will ignore a parameter with no value, which we do not want.
    if(
      isnull(ri!bioguide_id),
      "no_value",
      ri!bioguide_id
    )
  4. Set the test input value for bioguide_id to A000055 and click TEST REQUEST. The body of the response should have a single legislator's information.
  5. Click SAVE to save changes.

Next, you'll need to need to configure the source expression to call this Integration when someone tries to go to a specific record. To do this, you need to investigate the filters field of local!queryInformation. If a user is trying to access a record, filters will contain a QueryFilter with the following definition:

{field: "rp!id", operator: "=", value: <identifier>}

The unique field, rp!id is what will let you know when to get a specific legislator. You can then use the unique identifier in the filter's value field, bioguide_id in our case, to retrieve the legislator. Replace the definition of legislatorSourceExpression with the following expression to enable navigation to specific records.

with(
  local!queryInformation: rule!parseRspQuery(ri!rspQuery),
  
  /* Get unique idenifier if present in filters */
  local!uniqueIdentifier: displayvalue(
    "rp!id",
    index(local!queryInformation.filters, "field", {}),
    index(local!queryInformation.filters, "value", {}),
    null
  ),
  
  
  /* 
    If there is no unique identifier, get the record list data;
    otherwise, get the specific record using the id.  
  */
  local!response: a!fromJson(
    if(
      isnull(local!uniqueIdentifier),
      rule!getLegislatorRecordListData().result.body,
      rule!getLegislatorById(bioguide_id: local!uniqueIdentifier).result.body
    )
  ),
  
  /* Get the actual data */
  local!data: local!response.results,
  
  /*
    Generate the data subset. bioguide_id is our unique identifier.
    User placeholder configurations for paging and sorting.
  */
  a!dataSubset(
    data: local!data,
    identifiers: index(local!data, "bioguide_id", {}),
    totalCount: local!response.count,
    startIndex: 1,
    batchSize: 50,
   	sortInfo: a!sortInfo("bioguide_id", true)
  )
)

The source expression should now be able to handle navigation to specific records. You can test this by going to the record list and clicking a link. If you followed the tutorial correctly, the record you are taken to has the correct title at the top of the page. Notice the page is blank. The next step is to create a summary view.

Create the Summary View

A summary view for a service-backed record is no different, structurally, from a summary view for any other type of record. What may be different, however, is how you acquire the data. It may be the case that for your record, data will be coming from different service calls depending on the context. In this example, though, all of the data will be available in the rf! domain to be passed into the summary view rule.

So, create a new interface rule called legislatorSummaryView. Add the following inputs:

Input Name Input Type
bioguide_id Text
first_name Text
middle_name Text
last_name Text
district Text
chamber Text
party Text
website Text

And define the rule using the following expression:

={
    a!sectionLayout(
      label: "Basic Information",
      contents: {
        a!columnsLayout(
          columns: {
            a!columnLayout(
              contents: {
                a!textField(
                  label: "Legislator Identifier",
                  labelPosition: "ADJACENT",
                  value: ri!bioguide_id,
                  readOnly: true
                ),
                a!textField(
                  label: "First Name",
                  labelPosition: "ADJACENT",
                  value: ri!first_name,
                  readOnly: true
                ),
                a!textField(
                  label: "Middle Name",
                  labelPosition: "ADJACENT",
                  value: if(
                    isnull(ri!middle_name),
                    "N/a",
                    ri!middle_name
                  ),
                  readOnly: true
                ),
                a!textField(
                  label: "Last Name",
                  labelPosition: "ADJACENT",
                  value: ri!last_name,
                  readOnly: true
                ),
                a!textField(
                  label: "Chamber",
                  labelPosition: "ADJACENT",
                  value: if(isnull(ri!chamber),
                    "",
                    proper(ri!chamber)
                  ),
                  readOnly: true
                ),
                a!textField(
                  label: "District",
                  labelPosition: "ADJACENT",
                  value: ri!district,
                  readOnly: true
                ),
                a!textField(
                  label: "Party",
                  labelPosition: "ADJACENT",
                  value: ri!party,
                  readOnly: true
                ),
                a!linkField(
                  label: "Website",
                  labelPosition: "ADJACENT",
                  links: a!safeLink(
                    label: ri!website,
                    uri: ri!website
                  )
                )
              }
            ),
            a!columnLayout(
              contents: {
                a!imageField(
                  labelPosition: "COLLAPSED",
                  images: a!webImage(
                    source: "https://www.congress.gov/img/member/" & lower(ri!bioguide_id) & ".jpg",
                    links: a!safeLink(
                      uri: "https://www.congress.gov/img/member/" & lower(ri!bioguide_id) & ".jpg"
                    ),
                    altText: "Photo"
                  ),
                  size: "MEDIUM",
                  style: "AVATAR",
                  showWhen: not(ri!chamber = "house")
                )
              }
            )
          }
        )
      }
    )
  }

Note: This interface is fairly bare-bones. There are more fields available in the API. Feel free to explore them and add your own sections to the Summary View. In addition, unless you provide a test bioguide_id for the image, your summary view will show an error.

The last step in enabling navigation is to replace the placeholder interface definition with the Summary View rule in the record type definition. So, navigate to the Legislator record. Once there, call the Summary View rule, legislatorSummaryView, and pass the appropriate rf! fields as inputs. The expression should look like this:

rule!legislatorSummaryView(
  bioguide_id: rf!bioguide_id,
  first_name: rf!first_name,
  middle_name: rf!middle_name,
  last_name: rf!last_name,
  district: rf!district,
  chamber: rf!chamber,
  party: rf!party,
  website: rf!website
)

Once you are done copying and pasting the expression, click "OK". Now, the Views section should look like this:

At this point, navigation to specific records is fully functional with a working summary view. You can link to a record given its unique identifier. The record list is lacking, however, proper search and filtering. We'll address those in the next two sections.

Add Additional Interactive Features

In process-backed and entity-backed records, given search text or activated filters, Appian will automatically handle the data retrieval appropriately. In service-backed records, this task is left to you. In the next two subsections, you will learn how to configure search and user filters, both of which are necessary for a good record list.

With parseRspQuery, you can easily access any entered search-text. Once you have the search-text, all that is left is making use of it.

The Legislators API has a text search parameter, but your service may not have a generic search term. If this is the case, a good way to leverage the search term is to search on the primary key, allowing users who know the identifier of the record they're looking for to access the record quickly.

The Legislator query parameter will search on first_name, middle_name, last_name, and any known nicknames. Implementing search is fairly simple, but it does require editing two rules. Follow the instructions below to make the edits:

  1. Open the Integration, getLegislatorRecordListData, and add a new input, searchText, of type Text.
  2. Click Add Parameter
    • Set the Name to query
    • For the Value, click Edit as Expression, and set the value to ri!searchText
  3. Type in a test value for searchText, such as sanders, and click TEST REQUEST ensure that you get a response that matches your search.
  4. Click SAVE to save your Integration
  5. Open legislatorsSourceExpression, and add the new searchText parameter to your call of getLegislatorRecordListData.
   with(
     local!queryInformation: rule!parseRspQuery(ri!rspQuery),

     /* Get unique idenifier if present in filters */
     local!uniqueIdentifier: displayvalue(
       "rp!id",
       index(local!queryInformation.filters, "field", {}),
       index(local!queryInformation.filters, "value", {}),
       null
     ),


     /*
       If there is no unique identifier, get the record list data;
       otherwise, get the specific record using the id.
     */
     local!response: a!fromJson(
       if(
         isnull(local!uniqueIdentifier),
   +     rule!getLegislatorRecordListData(
   +       searchText: local!queryInformation.searchText
   +     ).result.body,
         rule!getLegislatorById(bioguide_id: local!uniqueIdentifier).result.body
       )
     ),

     /* Get the actual data */
     local!data: local!response.results,

     /*
       Generate the data subset. bioguide_id is our unique identifier.
       User placeholder configurations for paging and sorting.
     */
     a!dataSubset(
       data: local!data,
       identifiers: index(local!data, "bioguide_id", {}),
       totalCount: local!response.count,
       startIndex: 1,
       batchSize: 50,
       sortInfo: a!sortInfo("bioguide_id", true)
     )
   )
   

Now, if you type a search term from your record list in Tempo, the list data should filter.

Add Record List User Filters

The final component of the record list is user filters. In this section, you will use a!facet() and a!facetOption() to construct a user filter expression. It may be good to familiarize yourself with these functions before getting started.

To construct a user filter, you should create a separate rule to call in the record type definition. So, create a new rule called legislatorUserFilters, and define it as the following:

/*
  Each call of a!facet() will represent a single, labeled set of options for the user to pick from.
  Only one option may be chosen at a time from a 'facet'. 
  
  The 'id' field in a!facetOption must be unique across ALL facetOptions.
*/
{
  /* First filter will be on party. There are two options: 'Republican', and 'Democratic' */
  a!facet(
    name: "Party",
    options: {
      a!facetOption(
        id: 1,
        name: "Republican",
        filter: a!queryFilter(field: "party", operator: "=", value: "R")
      ),
      a!facetOption(
        id: 2,
        name: "Democratic",
        filter: a!queryFilter(field: "party", operator: "=", value: "D")
      )
    },
    allowMultipleSelections: false
  ),
  
  /* Second filter will be on chamber. There are two options: 'upper' and 'lower'. */
  a!facet(
    name: "Chamber",
    options: {
      a!facetOption(
        id: 3,
        name: "House",
        filter: a!queryFilter(field: "chamber", operator: "=", value: "house")
      ),
      a!facetOption(
        id: 4,
        name: "Senate",
        filter: a!queryFilter(field: "chamber", operator: "=", value: "senate")
      )
    },
    allowMultipleSelections: false
  ),
  
  /* Third filter will be on state. We will use a!foreach() to loop through all the states */
  a!facet(
    name: "State",
    options: a!foreach(
      {"Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado", "Connecticut", "Delaware", "District of Columbia", "Florida", "Georgia", "Hawaii", "Idaho", "Illinois", "Indiana", "Iowa", "Kansas", "Kentucky", "Louisiana", "Maine", "Maryland", "Massachusetts", "Michigan", "Minnesota", "Mississippi", "Missouri", "Montana", "Nebraska", "Nevada", "New Hampshire", "New Jersey", "New Mexico", "New York", "North Carolina", "North Dakota", "Ohio", "Oklahoma", "Oregon", "Pennsylvania", "Rhode Island", "South Carolina", "South Dakota", "Tennessee", "Texas", "Utah", "Vermont", "Virginia", "Washington", "West Virginia", "Wisconsin", "Wyoming"},
      a!facetOption(
        id: fv!index + 4,
        name: fv!item,
        filter: a!queryFilter(field: "state_name", operator: "=", value: fv!item)
      )
    )
  )
}

If you want to add different filters to the list view, you must make sure that the field value of the filter has a corresponding field in the source data type and that the ID for each facet option is unique across facet options within the same filter.

Next, go into the record type definition, and call the new rule in the User Filters field:

Now, if you navigate to the list view, you will see the filters.

You may notice, however, that pressing on a filter does nothing. This is because the source expression does not do anything with the filters. To address this issue, you will first need to alter the Integration that retrieves the data. So, open getLegislatorRecordListData, and add two new parameters: party of type Text and chamber of type Text. Then, take the following steps to utilize the new parameters:

  1. Add the following three inputs
    • ri!party of type Text
    • ri!chamber of type Text
    • ri!states of type List of Text
  2. Add the following two parameters to the integration. To add a parameter, click Add Parameter. Make sure you test the integration after adding each new parameter; it should always succeed.
    1. Add the parameter for party
      • Set the Name to party
      • For the Value, click Edit as Expression, and set the value to ri!party
    2. Add the parameter for chamber
      • Set the Name to chamber
      • For the Value, click Edit as Expression, and set the value to ri!chamber
    3. Add the parameter for states. Since this is a multi-select filter, it will look a little different.
      • Set the Name to state_name__in. The field is called state_name, and the __in is equivalent to the in operator for Appian query filters.
      • For the Value, click Edit as Expression, and paste the expression below into the expression box. Notice that we are joining the values in ri!states together with a pipe (|). This is in accordance with the API for passing multiple values to an in filter.
      if(
        length({ri!states}) > 0,
        joinarray(ri!states, "|"),
        null
      )
      
  1. Click SAVE to save your changes.

Next, you will need to alter the source expression to pass the appropriate party and chamber values to the query. Change the rule definition of legislatorSourceExpression to the following:

with(
  local!queryInformation: rule!parseRspQuery(ri!rspQuery),
  
  /* Get filters' fields and values to avoid function repitition */
  local!filterFields: index(local!queryInformation.filters, "field", {}),
  local!filterValues: index(local!queryInformation.filters, "value", {}),
  
  /* Get unique idenifier if present in filters */
  local!uniqueIdentifier: displayvalue(
    "rp!id",
    local!filterFields,
    local!filterValues,
    null
  ),
  
  /* Get filtered party */
  local!party: displayvalue(
    "party",
    local!filterFields,
    local!filterValues,
    ""
  ),
  
  /* Get filtered chamber */
  local!chamber: displayvalue(
    "chamber",
    local!filterFields,
    local!filterValues,
    ""
  ),
  
  /*
   * Get filtered state(s).
   * Need to cast to each item individually because the query filter wraps the type
   */
  local!states: a!forEach(
    displayvalue(
      "state_name",
      local!filterFields,
      local!filterValues,
      ""
    ),
    tostring(fv!item)
  ),
  
  /* 
    If there is no unique identifier, get the record list data;
    otherwise, get the specific record using the id.  
  */
  local!response: a!fromJson(
    if(
      isnull(local!uniqueIdentifier),
      rule!getLegislatorRecordListData(
        searchText: local!queryInformation.searchText,
        party: local!party,
        chamber: local!chamber,
        states: local!states
      ).result.body,
      rule!getLegislatorById(bioguide_id: local!uniqueIdentifier).result.body
    )
  ),
  
  /* Get the actual data */
  local!data: local!response.results,
  
  /*
    Generate the data subset. bioguide_id is our unique identifier.
    User placeholder configurations for paging and sorting.
  */
  a!dataSubset(
    data: local!data,
    identifiers: index(local!data, "bioguide_id", {}),
    totalCount: local!response.count,
    startIndex: 1,
    batchSize: 50,
   	sortInfo: a!sortInfo("bioguide_id", true)
  )
)

Now, users can search, apply filters, and navigate and link to records. The last piece is to adding paging and sorting. If you're using a list style, this piece is not important.

Default filters added in the Record definition will be added to rsp!query the same way that user filters are added. In general, though, it is much simpler to just add any default filtering directly to the source expression.

Add Paging and Sorting to the Grid View

Adding paging to the grid view is fairly straightforward. First, like with entity-backed and process-backed records, you need to ensure that your record type is configured for sorting. This means that all columns must have the appropriate sort field selected.

Next, open getLegislatorRecordListData and add a new input called pagingInfo of type PagingInfo. After adding ri!pagingInfo, alter the definition to use the new input:

  1. Find the query parameter with the name page. Click Edit as Expression for its value, and set the value to tointeger(rounddown(ri!pagingInfo.startIndex / ri!pagingInfo.batchSize)) + 1.
  2. Find the query parameter with the name per_page. Click Edit as Expression for its value, and set the value to ri!pagingInfo.batchSize.
  3. Find the query parameter with the name order. Click Edit as Expression for its value, and set the value to ri!pagingInfo.sort.field & if(ri!pagingInfo.sort.ascending, "__asc", "__desc").

Finally, open up the source expression, legislatorsSourceExpression, and make two small edits:

with(
  local!queryInformation: rule!parseRspQuery(ri!rspQuery),
  
  /* Get filters' fields and values to avoid function repitition */
  local!filterFields: index(local!queryInformation.filters, "field", {}),
  local!filterValues: index(local!queryInformation.filters, "value", {}),
  
  /* Get unique idenifier if present in filters */
  local!uniqueIdentifier: displayvalue(
    "rp!id",
    local!filterFields,
    local!filterValues,
    null
  ),
  
  /* Get filtered party */
  local!party: displayvalue(
    "party",
    local!filterFields,
    local!filterValues,
    ""
  ),
  
  /* Get filtered chamber */
  local!chamber: displayvalue(
    "chamber",
    local!filterFields,
    local!filterValues,
    ""
  ),
  
  /*
   * Get filtered state(s).
   * Need to cast to each item individually because the query filter wraps the type
   */
  local!states: a!forEach(
    displayvalue(
      "state_name",
      local!filterFields,
      local!filterValues,
      ""
    ),
    tostring(fv!item)
  ),
  
  /* 
    If there is no unique identifier, get the record list data;
    otherwise, get the specific record using the id.  
  */
  local!response: a!fromJson(
    if(
      isnull(local!uniqueIdentifier),
      rule!getLegislatorRecordListData(
        searchText: local!queryInformation.searchText,
        party: local!party,
        chamber: local!chamber,
        states: local!states,
!        pagingInfo: local!queryInformation.pagingInfo
      ).result.body,
      rule!getLegislatorById(bioguide_id: local!uniqueIdentifier).result.body
    )
  ),
  
  /* Get the actual data */
  local!data: local!response.results,
  
  /*
    Generate the data subset. bioguide_id is our unique identifier.
    User placeholder configurations for paging and sorting.
  */
  a!dataSubset(
    data: local!data,
    identifiers: index(local!data, "bioguide_id", {}),
    totalCount: local!response.count,
!    startIndex: local!queryInformation.pagingInfo.startIndex,
!    batchSize: local!queryInformation.pagingInfo.batchSize,
!    sortInfo: local!queryInformation.pagingInfo.sort
  )
)

Note: You now must provide test paging information for your Integration to function.

Now, your grid view is fully functional with paging and sorting. We've only talked about this new record type in terms of the web interface, but if you open the Appian for Mobile Devices application on your mobile device and log into the site you've been using, you'll see that what you've built is also automatically mobile enabled and available to you wherever you go.

Considerations When Using the Record Picker Component

If you want to use a record picker with your service-backed record, it should just work as expected. You should not need to change your record definition. But, in case something is not working as desired, here are a few notes on how the record picker works and how you can tweak your record design to optimize for a record picker.

  • When a user types text into the picker field, it is equivalent to a user typing text into the search bar of the record list. The difference is that every new letter in the picker is a new call to the web service, whereas in the record list, the user must hit enter before any calls are made. This means increased load on the web service.
  • Once a record is picked, the component makes one call to the web service for every picked item to get the information displayed in the label. This could be problematic because there is often overhead in web service calls. For that reason, only use the record picker when the number of items you expect users to pick at one time is low.
  • Filters applied in the component are processed in the same way that default filters and user filters are processed. This means that any new filters in the picker needed to be handled accordingly in your source expression and data retrieval rule.

If this picker does not meet your needs in some way, use a custom picker

Example

Say you have a form that requires a user pick a legislator from a specific state. First, you will need a constant that points to the record. Create a constant for the record type and call it LEGISLATOR_RECORD. Now, you can configure your record picker. Below is a working snippet that you can use in your form.

load(
  local!pickedLegislator,
  a!pickerFieldRecords(
    recordType: cons!LEGISLATOR_RECORD,
    label: "Virginia Legislators",
    filters: {
      a!queryFilter(field: "state_name", operator: "=", value: "Virginia")
    },
    value: local!pickedLegislator,
    saveInto: local!pickedLegislator
  )
)

Because we already have a filtering set up for state_name, there is nothing left to do! If, however, we wanted to filter on a new field, we would need to update the Integration and source expression to be able to handle filtering on that field.

FEEDBACK