SAIL Design

Overview

Appian SAIL (Self-Assembling Interface Layer) enables "write once, deploy anywhere" user interfaces across all modern web browsers and the major native mobile platforms.

The sections below detail design information and core concepts for SAIL interfaces.

See also:

Frequently Asked Questions

What is Appian SAIL?

SAIL stands for Self-Assembling Interface Layer. Commonly referred to as SAIL interfaces, they make up the look and feel for Tempo reports, record views, and SAIL process forms. Each interface only needs to be configured once to make it fully functional in all web browsers supported by Appian and Appian for Mobile Devices.

Where can SAIL interfaces be used?

SAIL interfaces can be used to build record views, Tempo reports, and process forms.

Do SAIL interfaces support reuse and modularity?

Yes, a core design principle of Appian SAIL is that all interfaces are modular and easily reused. Designers can quickly create common template libraries and reuse them across different SAIL interfaces.

How do I design Appian SAIL interfaces?

Appian SAIL interfaces are designed using Appian’s expression language. Due to the dynamic nature of the expression language, it is easy to design interfaces and immediately preview and test them before deployment. See also: Interface Designer and Style Guide (PDF)

Can I extend and customize SAIL components?

SAIL components are built on top of Appian’s data type system (CDTs) and expressions. Using Appian’s Data Designer and Function Plug-ins, the SAIL interfaces you build can use your data schemas and your business logic.

SAIL Concepts

SAIL Interface

SAIL interfaces are interfaces that display in the Work Platform environment and are made up of one or more SAIL components. You can define them using Appian's expression language.

See also: Expressions

SAIL Component

SAIL components are the building blocks of a SAIL interface that together make up the look and feel of the interface. They are configured using SAIL Functions.

See also: SAIL Components

SAIL Dashboard

A SAIL dashboard is a specific type of SAIL interface you can define for record views and Tempo reports.

SAIL Form

A SAIL form is a specific type of SAIL interface you can define for process start forms and task forms.

Enabling User Interaction

Some SAIL components allow users to interact with them. Interactions can take the form of filling out form inputs, like typing in a text field or making a choice in a dropdown, but can also mean clicking links or buttons. When a user interacts with a SAIL interface, the interface expression is reevaluated and the resulting interface displayed. This means the interface can dynamically respond to user interactions, such as changing the options in a dropdown based on an earlier dropdown, showing a section after a link is clicked, sorting columns in a grid, and so on.

Every SAIL component that supports user interaction has a parameter called saveInto that defines what changes to make when the user interacts with the component. The only way for a user to cause changes is through a component's saveInto parameter.

There are three ways to respond to user interactions with the saveInto parameter:

  • Save the user's input to a variable, e.g. local!nameor ri!amountPaid
  • Save a modified or alternative value into a variable, e.g. a!save(ri!username, lower(save!value))
  • Execute a smart service, e.g. a!deleteDocument(document: ri!requestForm)

Saving Input into Variables

When a user interacts with a component and that component's saveInto parameter is configured with a variable, the component's updated value will be saved to that variable.

Simple Example

The most common way to configure a component is to set its value and saveInto to the same variable. To see how this works, copy and paste the following expression into the expression view of the interface designer:

=load(
  local!name,
  a!textField(
    label: "Name",
    instructions: "Your name has " & len(local!name) & " characters",
    value: local!name,
    saveInto: local!name
  )
)

When the user types a name into the text field, the text they type will be saved to local!name. This in turn will be displayed in the text field because the same variable is passed to the value parameter.

Notice the local!name variable is used in the expression for the instructions parameter, as well as the value and saveInto parameters.

In the live view, type into the text field then press the tab key. Notice how the character count in the field instructions updates when you are no longer focused on the text input. This is because the expression was evaluated again and this time local!name had the new value.

The value of the variable configured in the saveInto parameter does not automatically show up as the display value of the component. The value input must be set separately for the change to be displayed by the component. To see what happens when this is not done, try updating that expression so that the value parameter is null:

=load(
  local!name,
  a!textField(
    label: "Name",
    instructions: "Your name has " & len(local!name) & " characters",
    value: null,
    saveInto: local!name
  )
)

Now, when you click away from the text input, the text input becomes blank. This is because the value of the component is hard-coded to null. Notice, though, the character count was updated. That's because the variable has the correct data.

Local Variables

When a variable is defined in an expression instead of made available to the expression by the framework, it is called a local variable. Rule inputs, process variables, and record fields are examples of non-local variables that are provided to the expression because of where it is being evaluated.

There are two functions available to define local variables in expressions: load() and with().

load()

The load() function allows you to define one or more local variables, use the variables to evaluate the expression defined in its last parameter, and return the result. The load() function only sets its local variables to their configured values when it is first rendered, often when the user first navigates to the interface or refreshes the browser window.

The fact that load() only sets its local variables when the SAIL interface is first rendered has two key consequences:

  • A SAIL component can save its value into a local variable defined by load() just as it can save to process variables and node inputs.
  • When external data is queried and stored in a local variable, either as the variable's initial value or in a component saveInto, the data is retained during later reevaluations of the interface without being queried every time.

See also: load()

with()

The with() function also creates local variables, but whereas load() sets its variables only when the SAIL interface first loads, with() sets its variables every time the expression is reevaluated.

with() is useful when you need to reuse the result of a function in multiple places in your expression and you need the value to be recalculated on every reevaluation.

NOTE: The value of with() variables cannot be updated in component saveInto inputs because their value is overwritten whenever the expression reevaluates.

See also: with()

with() and load()

To better understand the difference between with() and load(), copy and paste the following expression into the expression view of the interface designer:

load(
  local!loadVariable: rand(),
  local!typedText,
  with(
    local!withVariable: rand(),
    {
      a!textField(label: "load() Variable", readOnly: true, value: local!loadVariable),
      a!textField(label: "with() Variable", readOnly: true, value: local!withVariable),
      a!textField(label: "Try typing here", value: local!typedText, saveInto: local!typedText),
      a!buttonLayout(
        secondaryButtons: {
          a!buttonWidget(
            label: "Increment load() variable",
            value: local!loadVariable+1,
            saveInto: local!loadVariable
          )
        }
      )
    }
  )
)

Each time you type in the text field and click out from it, you can see the value of the with() variable changes. That's because rand() returns a different value every time it is evaluated. The load() variable does not change when you type, however, because load() set the variable's value once, when it was first evaluated. After that, the value only changes if you save into it. Click the increment button to see this happen. Finally, click the interface designer's Test button to reset the interface and notice that then, and only then, the load() variable's value gets set to a new random number. Clicking Test the interface designer is equivalent to leaving the interface and coming back to it.

Arrays and Custom Data Types

In addition to saving to a variable, you can save into an array at a specific index using square brackets. For example:

saveInto: local!names[10]

or

saveInto: local!names[local!index]

This is especially useful when generating components based on a list of data, such as in the Add Multiple Text Components Dynamically recipe.

You can also save into a field of a custom data type using the dot operator. For example:

saveInto: local!person.firstName

This is useful when you want to populate a custom data type via user input, since you can display an appropriate component for each field.

The dot and bracket notation can also be combined:

saveInto: local!persons.firstNames[local!index]

For a more extensive example, see the Add and Populate Sections Dynamically recipe.

NOTE: You must use square brackets or the dot operator to index in a saveInto variable. You cannot use the index() function to save to a specific index.

Rule Inputs

When saving sections of your expression into rules, you can pass a load() local variable, a process variable, or a node input to the rule and save into the rule input. In such a scenario, the variable must always be passed to the rule input as is. The saveInto parameter will not work if the variable has been modified with a function or operator, nor will it work if something besides a valid variable is passed, like "hello", 3, or a with() variable.

For example, let’s say you have a local variable called local!name and a rule that returns a Text component. In the rule definition, you want to save into local!name. You would create a rule input of type Text, and map it to the local variable by passing the local variable to the rule.

=load(
  local!name,
  returnTextField(local!name)
)

Where the definition of returnTextField is the following:

=a!textField(
  label: "Name",
  instructions: "Your name has " & len(ri!name) & " characters",
  value: ri!name,
  saveInto: ri!name
)

Saving Modified or Alternative Values

Instead of saving the user's exact input, you can also modify the component's updated value before saving it into a variable. To do so, use the a!save() function. The first parameter of this function is the variable to be updated. The second parameter is the value to set. This parameter can be configured with an expression that can either modify the component's value or return an alternative value completely unrelated to that of the component. The component's new value can be accessed in the second parameter using the special variable save!value.

For example, if you want to remove leading and trailing spaces from the user's input before saving it, you can use the a!save() function along with the trim() function. Update your interface with the following expression:

=load(
  local!name,
  a!textField(
    label: "Name",
    instructions: "Your name has " & len(local!name) & " characters",
    value: local!name,
    saveInto: a!save(local!name, trim(save!value))
  )
)

Enter leading and trailing spaces into the text field and click away. Notice how the character count in the instructions does not count the spaces you entered.

You can modify the user’s input with as many functions or operators as you like. For example, update your interface with the following expression:

=load(
  local!name,
  a!textField(
    label: "Name",
    instructions: local!name,
    value: local!name,
    saveInto: a!save(local!name, fn!append("Hello ", trim(save!value)))
  )
)

To save into multiple variables, you can pass an array containing both variables to update and a!save() functions. This expression saves the user's input into one variable while updating a second variable the input prefixed with "Hello ":

=load(
  local!name,
  local!greeting,
  a!textField(
    label: "Name",
    instructions: local!greeting,
    value: local!name,
    saveInto: {
      local!name,
      a!save(local!greeting, append("Hello ", save!value))
    }
  )
)

You can also use multiple a!save() functions. This expression trims the user's input before saving it into the first variable, then updates the second by prefixing the first variable:

=load(
  local!name,
  local!greeting,
  a!textField(
    label: "Name",
    instructions: local!greeting,
    value: local!name,
    saveInto: {
      a!save(local!name, trim(save!value)),
      a!save(local!greeting, append("Hello ", local!name))
    }
  )
)

Try typing leading and trailing spaces into the field and notice how local!greeting is getting updated with the trimmed name, not the original user input.

The expression in the saveInto parameter evaluates when the user interacts with the component. Each item in the saveInto array evaluates one at a time. Therefore, if an a!save() parameter uses a variable that was updated higher in the list, a!save() evaluates with the variable's updated value.

Executing Smart Services

Much like a!save(), a single smart service function can be executed in the saveInto parameter. To see how this works, copy and paste the following expression into the expression view of the interface designer:

=a!buttonLayout(
  primaryButtons: {
    a!buttonWidget(
      label: "Update Name",
      saveInto: {
        a!updateUserProfile(
          user: loggedInUser(),
          firstName: "New name",
          lastName: user(loggedInUser(), "lastName"),
          email: user(loggedInUser(), "email")
        )
      }
    )
  }
)

Try clicking the button and then mousing over your avatar to see that your user account's first name has indeed been changed (assuming you have permission to modify your name). Don't forget to change it back when you're finished with this section!

The smart service can be combined with one or more a!save() functions. The following interface lets you type in a new name for yourself, then clears it from the text box when the button is clicked:

=load(
  local!newName,
  {
    a!textField(
      label: "New First Name",
      value: local!newName,
      saveInto: local!newName
    ),
    a!buttonLayout(
      primaryButtons: {
        a!buttonWidget(
          label: "Update Name",
          saveInto: {
            a!updateUserProfile(
              user: loggedInUser(),
              firstName: local!newName,
              lastName: user(loggedInUser(), "lastName"),
              email: user(loggedInUser(), "email")
            ),
            a!save(local!newName, null)
          }
        )
      }
    )
  }
)

NOTE: While you can use any number of saves, only one smart service can be executed in a SAIL saveInto parameter. To run multiple smart services, you can:

  • Use the Start Process smart service to launch a process
  • If on a record view, you can use a record related action
  • If on a start or task form, you can let the user submit the form and use activity chaining to bring them to another form

You can use the smart service's onSuccess parameter to configure one or more saves that will run if the smart service completes successfully. Any returned data from the smart service will be available in the onSuccess parameter via function variables. The following interface only clears the text field if the name has been successfully updated:

=load(
  local!newName,
  {
    a!textField(
      label: "New First Name",
      value: local!newName,
      saveInto: local!newName
    ),
    a!buttonLayout(
      primaryButtons: {
        a!buttonWidget(
          label: "Update Name",
          saveInto: {
            a!updateUserProfile(
              user: loggedInUser(),
              firstName: local!newName,
              lastName: user(loggedInUser(), "lastName"),
              email: user(loggedInUser(), "email"),
              onSuccess: {
                a!save(local!newName, null)
              }
            )
          }
        )
      }
    )
  }
)

You can see what happens when there's an error by using an illegal character like # in your new name. Note that the interface designer shows you what the error was, but users on an actual task, record, or report will not see these details.

To show them an error message, the smart service onError parameter can be configured with one or more saves that will update variables if the smart service fails. The following interface shows a validation message under the text field if an error occurs saving the name and clears it when it has saved successfully:

=load(
  local!newName,
  local!error: false,
  {
    a!textField(
      label: "New First Name",
      value: local!newName,
      validations: if(
        local!error,
        "Couldn't save the new first name",
        ""
      ),
      saveInto: local!newName
    ),
    a!buttonLayout(
      primaryButtons: {
        a!buttonWidget(
          label: "Update Name",
          saveInto: {
            a!updateUserProfile(
              user: loggedInUser(),
              firstName: local!newName,
              lastName: user(loggedInUser(), "lastName"),
              email: user(loggedInUser(), "email"),
              onSuccess: {
                a!save(local!newName, null),
                a!save(local!error, false)
              },
              onError: {
                a!save(local!error, true)
              }
            )
          }
        )
      }
    )
  }
)

Creating Reusable Custom Components

When you create generic rules that wrap SAIL components, such as a custom dollarField component, you need a rule input for the value parameter and a different rule input for the saveInto parameter.

The rule input that maps to the saveInto parameter of the a!textField() function must be an array of type Save. This setup allows fellow designers to use the a!save() function to modify the user input in the same way as if they were interacting with any other SAIL component.

Best Practice: As a best practice, we recommend using the same convention established by Appian components and calling your rule inputs value and saveInto. Following this practice will help your fellow designers configure your component correctly.

For example, your dollarField rule definition might look like this:

=a!textField(
  label: ri!label,
  instructions: ri!instructions,
  readOnly: ri!readOnly,
  disabled: ri!disabled,
  validations: ri!validations,
  value: if(isnull(ri!value), "", dollar(ri!value)),
  saveInto: a!save(ri!saveInto, todecimal(save!value))
)

The user's input is passed to the todecimal() function and the result is saved into the designer-configured variable so that the designer only deals with decimal values. The value argument is formatted using the dollar() function.

Validating User Inputs

SAIL interfaces take advantage of Appian's expression language to conditionally make components required or optional as the user interacts with the interface. Custom validation rules can similarly be attached to any user input to give feedback to the end user as they fill out their form.

SAIL components that take user input typically have a parameter called required, and a parameter called validations for custom validation rules.

  • The required and validations parameters determine how the component displays. For instance, required: true displays a red star next to the field, and validations: "Invalid" displays a red text with the message "Invalid" when the field's value is not null.

  • The button component configuration determines how the SAIL interface behaves when validation errors are present. For instance, the presence of validation errors prevent submission of a SAIL form.

Required fields that are null only display a validation message when the user clicks a button that enforces validation as follows:

a!buttonWidget(
  ...
  validate: true,
  ...
)

or

a!buttonWidgetSubmit(
  ...
  validate: null or true,
  ...
)

See also: Validation Groups

Custom validation messages display as soon as the field's value is not null. For example, the following text component displays the validation message when the user types fewer than 4 characters.

=load(
  local!a,
  a!textField(
    label: "Title",
    validations: if(len(local!a) < 4, "Enter at least 4 characters", ""),
    value: local!a,
    saveInto: local!a
  )
)

Note that validations are configured with a simple text message. Also note that you don't need a null check before the if() function.

Validation is only enforced when the component to which it is attached is visible on the SAIL interface. To walk through an example where a required field is conditionally displayed, see also the recipe: Configure a Dropdown with an Extra Option for Other.

Adding Validation to Button Components using Validation Groups

Validation groups allow designers to associate different validation behavior with different buttons on the same SAIL interface. A typical example is a form with approve and reject buttons, and a comments field that is only required if the user clicks reject.

To associate validation rules with a button, you need to use the validationGroup parameter. It is used to associate a set of components with a button. The validationGroup text is case- sensitive.

To walk through an example, see also the recipe: Configure Buttons with Conditional Requiredness

A required field doesn't display the red star when it is in a validation group. For example, copy and paste the following expression in a Tempo report and notice that the red star is not displayed on the "Conditionally Required" field.

=load(
  local!a,
  local!b,
  local!text1,
  local!text2,
  a!dashboardLayout(
    contents: {
      a!textField(
        label: "Conditionally Required",
        required: true,
        requiredMessage: "This field is required because you clicked Button A!",
        validationGroup: "buttonA",
        value: local!text1,
        saveInto: local!text1
      ),
      a!textField(
        label: "Always Required",
        required: true,
        value: local!text2,
        saveInto: local!text2
      ),
      a!buttonLayout(
        primaryButtons: {
          a!buttonWidget(
            label: "Button A",
            validate: true,
            validationGroup: "buttonA",
            value: "Button A!",
            saveInto: local!a
          ),
          a!buttonWidget(
            label: "Button B",
            validate: true,
            value: "Button B!",
            saveInto: local!b
          )
        }
      )
    }
  )
)

Click "Button A" and notice that the custom required message displays in the first text field. Appian recommends that you configure a custom required message for conditionally required fields.

Click "Button B" and notice that the first field is no longer required while the second one still is. This is because every component that is not in a validation group is considered to be in the default validation group, i.e. the component's value must be valid irrespective of the button clicked.

A component can only be in one validation group. To walk through an example where a validation rule always applies, while another validation rule only applies when a particular button is clicked, see also the recipe: Add Multiple Validation Rules to One Component.

Submitting User Inputs to Process

When a SAIL form is used as a process start form, the components on the form can update process variables configured as parameters.

Attempting to save into a process variable that is not a parameter does not cause an error. However, if you find that your process variable is not updating as expected, verify that the Parameter checkbox is selected in the Process Model Properties dialog. Local variables cannot be used to update process variable values.

Similarly, when a SAIL form is used as a task form, the components on the form can update node inputs configured on the node. However, process variables cannot be updated from a task form and local variables cannot be used to update node input values.

  • NOTE: Inline task approvals cannot currently be configured for SAIL forms.

Start form parameters are available to the form expression in the pv! domain. Task form inputs are available in the ac! domain. For example, a process parameter called "employee" is accessed via pv!employee and a task input with the same name would be accessed via ac!employee. These variables can then be passed as inputs to rules. They should not be accessed directly from within SAIL rules. As the user interacts with a SAIL form, they update these rule inputs, and when the user submits a form, the new values are submitted back to the process engine.

  • NOTE: The values that are submitted to the process engine are the values of the variables at the time of submission, not what is displayed in the SAIL interface. See also: Enabling User Interaction

To walk through an example of how to map a variable to a component via a rule input, see also: Process Model Tutorial

See Also

Now that you understand the fundamentals, the following resources can help you build great SAIL interfaces:

FEEDBACK