Extending your first integration: Iterating over a collection

Iterating over a collection is a simple, yet powerful logic in workflows. A ZIS Flow supports iterating over an array of objects using the Map state. The state executes a certain piece of logic for each item in the array.

In this tutorial, you'll learn how the Map state can be used in a flow to redact (or remove) all attachments from a ticket when the ticket's status is changed to Solved. You’ll apply concepts explained in previous tutorials, such as using JQ to manipulate JSON and making authenticated API calls using named connections.

To complete this tutorial, you’ll need a valid OAuth token stored in ZIS Connections. You should be able to use the OAuth access token generated in Building your first integration - Part 2: Obtaining an OAuth token.

First, let's look at the Map state. It has the following format:

{  "Type": "Map",  "ItemsPath": "<json_path_to_the_array_of_objects>",  "ResultPath": "<json_path_to_store_results>",  "Iterator": {    "StartAt": "",    "States": {}  }}

where:

  • ItemsPath: A JSON path syntax pointing to an array of objects
  • ResultPath: A JSON path syntax to store the results from the Map state
  • Iterator: This contains the sequence of states that’s executed for each object in ItemsPath state machine, much like a flow

The state machine inside the Iterator object only has access to the object it’s iterating on. For example, consider this input JSON object to the Flow with a Map state:

{  "ticket": {    "id": 123,    "comments": [      {        "id": "334324",        "added_by": "foo",        "text": "my printer is on fire"      },      {        "id": "334325",        "added_by": "foo",        "text": "pls help"      }    ]  }}

When the ItemsPath property is set to the array $.input.ticket.comments, the states inside Iterator only sees the $.input.ticket.comments object. That is, one of the two comments at any given time. During an iteration, you would need access to items that are present outside of the array such as ticket.id.

In order to make ticket.id available for the Iterator, you add ticket.id to each object in the `ItemsPath array. You can do this by using the JQ action state.

Example:

{  "Type": "Action",  "ActionName": "zis:common:transform:Jq",  "Parameters": {    "expr.$": "[.comments[] | (.ticket_id={{$.input.ticket.id}})]",    "data.$": "$.input"  },  "ResultPath": "$.input.ticket.comments"}

The ResultPath now contains the ticket.id in each object:

[  {    "id": "334324",    "added_by": "foo",    "text": "my printer is on fire",    "ticket_id": 123  },  {    "id": "334324",    "added_by": "foo",    "text": "pls help",    "ticket_id": 123  }]

Note: In future, we intend to enhance the Map state so the Iterator will have access to other items in the input JSON object. This will no longer require a JQ state to add items of interest to each object as in the above example.

Creating and uploading a ZIS Bundle

  1. In your text editor, create a delete_attachments.json file and paste the following information:

    {  "zis_template_version": "2019-10-14",  "name": "Redact attachments",  "description": "Redact attachments when ticket status change to Solved",  "resources": {    "JobSpec": {      "type": "ZIS::JobSpec",      "properties": {        "name": "RedactAttachmentsOnSolved",        "event_source": "support",        "event_type": "ticket.StatusChanged",        "flow_name": "zis:<YOUR_INTEGRATION>:flow:RedactAttachmentsOnSolved"      }    },    "Example_flow": {      "type": "ZIS::Flow",      "properties": {        "name": "RedactAttachmentsOnSolved",        "definition": {          "StartAt": "CheckStatus",          "States": {            "CheckStatus": {              "Type": "Choice",              "Choices": [                {                  "Variable": "$.input.ticket_event.current",                  "StringEquals": "solved",                  "Next": "GetComments"                }              ],              "Default": "End"            },            "GetComments": {              "Type": "Action",              "ActionName": "zis:<YOUR_INTEGRATION>:action:GetComments",              "Parameters": {                "ticket_id.$": "$.input.ticket_event.ticket.id",                "access_token.$": "$.connections.zendesk.access_token"              },              "ResultPath": "$.input.ticket_comments",              "Next": "SanitiseTicketComments"            },            "SanitiseTicketComments": {              "Type": "Action",              "ActionName": "zis:common:transform:Jq",              "Parameters": {                "expr": "[.comments[] | (.attachments[] | (.id? // (.[] | .id))) as $id | {\"comment_id\": \"\\(.id)\", \"attachment_id\":  \"\\($id)\"}]",                "data.$": "$.input.ticket_comments"              },              "ResultPath": "$.input.ticket_comments",              "Next": "IncludeTicketId"            },            "IncludeTicketId": {              "Type": "Action",              "ActionName": "zis:common:transform:Jq",              "Parameters": {                "expr.$": "[.ticket_comments[] | (.ticket_id={{$.input.ticket_event.ticket.id}})]",                "data.$": "$.input"              },              "ResultPath": "$.input.ticket_comments",              "Next": "IncludeAccessToken"            },            "IncludeAccessToken": {              "Type": "Action",              "ActionName": "zis:common:transform:Jq",              "Parameters": {                "expr.$": "[.ticket_comments[] | (.access_token=\"{{$.connections.zendesk.access_token}}\")]",                "data.$": "$.input"              },              "ResultPath": "$.input.ticket_comments",              "Next": "RedactAttachment"            },            "RedactAttachment": {              "Type": "Map",              "ItemsPath": "$.input.ticket_comments",              "ResultPath": "$.redact_attachment_output",              "Iterator": {                "StartAt": "Redact",                "States": {                  "Redact": {                    "Type": "Action",                    "ActionName": "zis:<YOUR_INTEGRATION>:action:RedactAttachment",                    "Parameters": {                      "attachment_id.$": "$.attachment_id",                      "comment_id.$": "$.comment_id",                      "ticket_id.$": "$.ticket_id",                      "access_token.$": "$.access_token"                    },                    "Next": "Done"                  },                  "Done": {                    "Type": "Succeed"                  }                }              },              "Next": "End"            },            "Finish": {              "Type": "Succeed"            }          }        }      }    },    "GetComments": {      "type": "ZIS::Action::Http",      "properties": {        "name": "GetComments",        "definition": {          "method": "GET",          "path.$": "/api/v2/tickets/{{$.ticket_id}}/comments.json",          "headers": [            {              "key": "Authorization",              "value.$": "Bearer {{$.access_token}}"            },            {              "key": "Content-Type",              "value": "application/json"            }          ]        }      }    },    "RedactAttachment": {      "type": "ZIS::Action::Http",      "properties": {        "name": "RedactAttachment",        "definition": {          "method": "PUT",          "path.$": "/api/v2/tickets/{{$.ticket_id}}/comments/{{$.comment_id}}/attachments/{{$.attachment_id}}/redact",          "headers": [            {              "key": "Authorization",              "value.$": "Bearer {{$.access_token}}"            },            {              "key": "Content-Type",              "value": "application/json"            }          ]        }      }    }  }}

    The bundle makes use of the JQ state in order to sanitize and add items to each object in the array.

    • SanitiseTicketComments: This state will prepare the array for looping. The expr.$ transforms the input to the desired output
    • IncludeTicketId: This state adds input.ticket_event.ticket.id to each object in the array
    • IncludeAccessToken: This state adds connections.zendesk.access_token to each object in the array. This access token is used to make a call to the Redact Attachment API.

    Make sure you replace <YOUR_INTEGRATION> and <SUBDOMAIN> with the appropriate values and save the bundle.

  2. In your command line tool, make a cURL request to the Upload or Update Bundle endpoint to update the bundle:

    curl \--request POST \--url https://<SUBDOMAIN>.zendesk.com/api/services/zis/registry/<YOUR_INTEGRATION>/bundles \--user '<EMAIL>:<PASSWORD>' \--header 'content-type: application/json' \-d @delete_attachments.json
  3. From the command line, make a cURL request to the Registry API to enable the JobSpec:

    curl \--request POST \--url "https://<SUBDOMAIN>.zendesk.com/api/services/zis/registry/job_specs/install?job_spec_name=zis:<YOUR_INTEGRATION>:job_spec:RedactAttachmentsOnSolved" \--user '<EMAIL>:<PASSWORD>'

Testing the integration

You can test the integration in Zendesk Support by adding attachments to a ticket and then setting the ticket status as Solved. When you refresh the page, you should see the comments in the ticket are deleted and replaced with redacted.txt.

Next: Extending your first integration: Using ZIS Links