Conditional Fields

Sym gives you the ability to dynamically modify the access request form as a user is filling it out.

Overview

When making access requests, the information needed may vary. With on_change functions, you can change the fields displayed, which ones are required, and more-- all on the fly based on what the user has already entered into the form.

🚧

Terraform Provider version requirements

This feature requires that your Sym Terraform Provider version is >= 3.4.2. If it is not and you would like to use this feature, please upgrade your version with terraform init -upgrade.

Implementation

Only two things are needed to start using on_change functions:

  1. At least one prompt_field
  2. A new Python file where the on_change function will be defined

To start, add the Python logic to a prompt_field by setting the on_change attribute:

resource "sym_flow" "this" {
  ...

  params {
    prompt_field {
      name           = "urgency"
      type           = "string"
      required       = true
      allowed_values = ["Low", "Medium", "High"]
      
      # When the value of the "urgency" field changes in the request form,
      # the code defined in on_change.py will be run.
      on_change      = file("${path.module}/on_change.py")
    }
    
    prompt_field {
      name     = "reason"
      type     = "string"
      required = false
    }
  }
}

Then, add an on_change_<field_name> function to the new Python file. In this case, since the file is attached to the urgency field, we need to define an on_change_urgency field:

def on_change_urgency(username, form):
  # Right now, this function does nothing.
  return form

📘

If your prompt field name has dashes, then replace the dashes with underscores when naming your on_change function.

For example, if your prompt_field has name = "my-field", then your function should be named on_change_my_field.

All on_change functions take two arguments:

  1. username -- This will be the email of the user filling out the request form.
  2. form -- This is a sym.sdk.forms.SymPromptField Python object representing the current state of the form. See the SDK docs for a full API reference.

and should return the same form they were given after making any desired modifications. For example, to make the reason field required for "High" urgency requests, set the reason field to be required and then return the form containing it:

def on_change_urgency(username, form):
  # All fields defined on the sym_flow are available in form.fields at:
  # form.fields["<prompt_field.name>"]
  reason_field = form.fields["reason"]
  urgency_field = form.fields["urgency"]
  
  if urgency_field.value == "High":
    # If the value of the "urgency" field is "High", then the "reason" field
    # should be required.
    reason_field.required = True
  else:
    # If the value of the "urgency" field is NOT "High", then the "reason" field
    # should not be required.
    reason_field.required = False
  
  # Return the form containing modified field.
  return form

After running terraform apply to apply these changes, the Flow will call the on_change function whenever the urgency field value is changed:

A demo of `on_change_urgency` toggling whether the `reason` field is required.

A demo of on_change_urgency toggling whether the reason field is required.

🚧

Limitations

Because on_change functions are called on the fly while a user is filling out a form, they must execute very fast or the user will be left waiting. Due to this, sym.sdk.integrations functions are not available, nor is the requests library, as making REST API calls would slow down the experience immensely.

Examples

Conditionally display a field

To hide or display certain prompt_fields conditionally, toggle the visible attribute. The following example will display a required reason field only if the urgency is "High":

def on_change_urgency(username, form):
    # All fields defined on the sym_flow are available in form.fields at:
    # form.fields["<prompt_field.name>"]
    reason_field = form.fields["reason"]
    urgency_field = form.fields["urgency"]

    if urgency_field.value == "High":
        # If the value of the "urgency" field is "High", then the "reason" field
        # should be displayed.
        reason_field.visible = True
    else:
        # If the value of the "urgency" field is NOT "High", then the "reason" field
        # should not be displayed.
        reason_field.visible = False

    # Return the form containing modified field.
    return form
resource "sym_flow" "this" {
  ...

  params {
    prompt_field {
      name           = "urgency"
      type           = "string"
      required       = true
      allowed_values = ["Low", "Medium", "High"]
      
      # When the value of the "urgency" field changes in the request form,
      # the code defined in on_change.py will be run.
      on_change      = file("${path.module}/on_change.py")
    }
    
    prompt_field {
      name     = "reason"
      type     = "string"
      required = true
     
      # This field will not be displayed in the request form.
      # Invisible fields are always optional, and `required = true` will not be
      # enforced unless an `on_change` function sets `visible = true`.
      visible  = false
    }
  }
}

Conditionally change field options

It may be useful to change the options available in a select field based on a different field's value. The following example will not allow requesters to ask for "Admin" permissions to the "Production" AWS account, but will allow it for the "Staging" AWS account:

def on_change_aws_account_name(username, form):
    # All fields defined on the sym_flow are available in form.fields at:
    # form.fields["<prompt_field.name>"]
    aws_account_name_field = form.fields["aws_account_name"]
    access_level_field = form.fields["access_level"]

    if aws_account_name_field.value == "Production":
        # Get the allowed_values defined in Terraform, but filter out "Admin".
        all_options_except_admin = [option for option in access_level_field.original_allowed_values if option.value != "Admin"]

        # Set the current list of allowed_values to the filtered list.
        access_level_field.current_allowed_values = all_options_except_admin
    else:
        # If the "Production" account is not selected, all allowed_values defined in Terraform are allowed.
        access_level_field.current_allowed_values = access_level_field.original_allowed_values

    # Return the form containing modified field.
    return form
resource "sym_flow" "this" {
  ...

  params {
    prompt_field {
      name           = "aws_account_name"
      type           = "string"
      allowed_values = ["Staging", "Production"]
  
      # When the value of the "aws_account_name" field changes in the request form,
      # the code defined in on_change.py will be run.
      on_change      = file("${path.module}/on_change.py")
    }

    prompt_field {
      name           = "access_level"
      type           = "string"
      allowed_values = ["Admin", "Read", "Write"]
    }
  }
}

React to Target selection

To conditionally change fields or field options based on Target selection, include the target_id as a field in your sym_flow resource. This will make the target_id available as a prompt_field in your SDK implementation.

Then, in your on_change method, you can:

  1. Resolve the target_id_field to its select options.
  2. Validate that an option has been selected
  3. Fetch the target's label for comparison
  4. Apply any downstream logic from there
resource "sym_flow" "this" {
  ...

  params {
    prompt_field {  
      name = "target_id"  
      type = "string"  
      required = true  
      on_change = file("${path.module}/on_change.py")  
    }
    # Other prompt fields, etc.
  }
}
def on_change_target_id(username, form):
    # Get the target_id field from the form
    target_id_field = form.fields["target_id"]

    # Get any currently selected target option
    target_selection = [option for option in target_id_field.original_allowed_values if str(option.value) == str(target_id_field.value)]
    
    # If a Target has been selected...
    if target_selection:
        # Then get the Target's label (this is what is displayed in the dropdown)
        target_label = target_selection[0].label
        ...

Using Flow vars in on_change

If you have defined a vars map in your sym_flow resource, this will be exposed as the flow_vars dictionary in the SymPromptForm object used to invoke your on_change function. Remember, while you may pass in other primitives (e.g. bool, int) as a value to sym_flow.vars, they will be cast to strings when you apply your configuration. As a result, SymPromptForm.flow_vars is defined as a Dict[str, str]; if you wish to access these values as different types, remember to recast them before using them.

resource "sym_flow" "this" {
  ...
  vars = {
    a_string_var = "foo"
    an_int_var = 5
  }

  params {
    prompt_field {  
      name = "my_field"  
      type = "string"  
      required = true  
      on_change = file("${path.module}/on_change.py")  
    }
    # Other prompt fields, etc.
  }
}
def on_change_my_field(username, form):
  # The vars map is exposed under `form.flow_vars`
  
  # Prints "foo"
  print(form.flow_vars["a_string_var"])

  # Don't forget to cast other primitives if you wish to use them as their respective types!
  an_int_var = int(form.flow_vars["an_int_var"])

  # Prints "True"
  print(an_int_var > 1)

Troubleshooting

An argument named "on_change" is not expected here.

Please ensure that your on_change block is defined inside of a prompt_field block and that your Sym Provider version is >= 3.4.2.