Lesson 11 - Loop Aggregation

Goal

In this lesson we’ll learn how to aggregate output from a loop.

Get Started

We’ll create a new task to simulate ordering equipment. Internally it will randomly decide whether a piece of equipment is available or not. Then we’ll run that task in a loop from the main flow and record the cost of the ordered equipment and which items were unavailable. Create a new file named order.sl in the tutorials/hiring folder to house the new operation we’ll write and get the new_hire.sl file ready because we’ll need to add a task to the main flow.

Operation

The order operation, as we’ll call it, looks very similar to our check_availability operation. It uses a random number to simulate whether a given item is available. If the item is available, it will return the cost of the item as one output and the unavailable output will be empty. If the item is unavailable, it will return 0 for the cost of the item and the name of the item in the unavailable output.

namespace: tutorials.hiring

operation:
  name: order

  inputs:
    - item
    - price

  action:
    python_script: |
      print 'Ordering: ' + item
      import random
      rand = random.randint(0, 2)
      available = rand != 0
      not_ordered = item + ';' if rand == 0 else ''
      price = 0 if rand == 0 else price
      if rand == 0: print 'Unavailable'

  outputs:
    - unavailable: not_ordered
    - cost: price

  results:
    - UNAVAILABLE: rand == 0
    - AVAILABLE

Task

First, we’ll go back to our flow and create a task, between create_email_address and print_finish, to call our operation in a loop. This time we’ll loop through a map of items and their prices.

- get_equipment:
    loop:
      for: item, price in order_map
      do:
        order:
          - item
          - price

We’ll also need to create some input variables first. One variable, that we’ll call order_map, will contain the map we’re looping on. Also, each time through the loop we want to aggregate the data that is output. We’ll create two variables, missing and total_cost, for this purpose, defining them as overridable and giving them default values to start with.

inputs:
  - first_name
  - middle_name:
      required: false
  - last_name
  - missing:
      default: "''"
      overridable: false
  - total_cost:
      default: 0
      overridable: false
  - order_map: >
      {'laptop': 1000, 'docking station':200, 'monitor': 500, 'phone': 100}

Now we can perform the aggregation. In get_equipment task’s publish section, we’ll add the output variables to the ones we just created in the flow inputs and publish them back to the flow. This will run for each iteration after the operation has completed, aggregating all the data. Notice the usage of the self[''] syntax to indicate that we’re referring to the variable that exists on the flow level and not a variable with the same name that might have been returned from the operation.

publish:
  - missing: self['missing'] + unavailable
  - total_cost: self['total_cost'] + cost

Finally we have to rewire all the navigation logic to take into account our new task.

We need to change the create_email_address task to forward successful email address creations to get_equipment.

navigate:
  CREATED: get_equipment
  UNAVAILABLE: print_fail
  FAILURE: print_fail

And we need to add navigation to the get_equipment task. We’ll always go to print_finish no matter what happens.

navigate:
  AVAILABLE: print_finish
  UNAVAILABLE: print_finish

Finish

The last thing left to do is print out a finish message that also reflects the status the equipment order.

- print_finish:
    do:
      base.print:
        - text: >
            'Created address: ' + address + ' for: ' + first_name + ' ' + last_name + '\n' +
            'Missing items: ' + missing + ' Cost of ordered items: ' + str(total_cost)

Run It

We can save the files, run the flow and see that the ordering takes place, the proper information is aggregated and then it is printed.

run --f <folder path>/tutorials/hiring/new_hire.sl --cp <folder path>/tutorials/base,<folder path>/tutorials/hiring --i first_name=john,middle_name=e,last_name=doe

Up Next

In the next lesson we’ll see how to use existing content in your flows.

New Code - Complete

new_hire.sl

namespace: tutorials.hiring

imports:
  base: tutorials.base

flow:
  name: new_hire

  inputs:
    - first_name
    - middle_name:
        required: false
    - last_name
    - missing:
        default: "''"
        overridable: false
    - total_cost:
        default: 0
        overridable: false
    - order_map: >
        {'laptop': 1000, 'docking station':200, 'monitor': 500, 'phone': 100}

  workflow:
    - print_start:
        do:
          base.print:
            - text: "'Starting new hire process'"

    - create_email_address:
        loop:
          for: attempt in range(1,5)
          do:
            create_user_email:
              - first_name
              - middle_name
              - last_name
              - attempt
          publish:
            - address
          break:
            - CREATED
            - FAILURE
        navigate:
          CREATED: get_equipment
          UNAVAILABLE: print_fail
          FAILURE: print_fail

    - get_equipment:
        loop:
          for: item, price in order_map
          do:
            order:
              - item
              - price
          publish:
            - missing: self['missing'] + unavailable
            - total_cost: self['total_cost'] + cost
        navigate:
          AVAILABLE: print_finish
          UNAVAILABLE: print_finish

    - print_finish:
        do:
          base.print:
            - text: >
                'Created address: ' + address + ' for: ' + first_name + ' ' + last_name + '\n' +
                'Missing items: ' + missing + ' Cost of ordered items: ' + str(total_cost)

    - on_failure:
      - print_fail:
          do:
            base.print:
              - text: "'Failed to create address for: ' + first_name + ' ' + last_name"

order.sl

namespace: tutorials.hiring

operation:
  name: order

  inputs:
    - item
    - price

  action:
    python_script: |
      print 'Ordering: ' + item
      import random
      rand = random.randint(0, 2)
      available = rand != 0
      not_ordered = item + ';' if rand == 0 else ''
      price = 0 if rand == 0 else price
      if rand == 0: print 'Unavailable'

  outputs:
    - unavailable: not_ordered
    - cost: price

  results:
    - UNAVAILABLE: rand == 0
    - AVAILABLE