Iterating over a collection in a ZIS flow

In this tutorial, you'll create a ZIS integration that iterates over an array of JSON objects. To do this, you'll include a Map state in the integration's ZIS flow. The Map state runs each item in the array through a sequence of flow states that you specify.

What you'll need

To complete this tutorial, you'll need the following:

Creating the integration

The integration you create listens for Ticket Status Changed events in Zendesk. If the event's current ticket status is "solved", the integration redacts any attachments in the ticket's comments. If the event has another current ticket status, the integration takes no action.

  1. Create an OAuth connection named "zendesk". You'll use the connection to authenticate Zendesk API requests in the integration's ZIS flow.

    In a typical setup, an admin uses a private Zendesk app to create OAuth connections. For this tutorial, you'll create the "zendesk" connection without an app.

    To create the "zendesk" connection, follow the steps in Creating an OAuth connection for Zendesk. Then return here.

  2. Create a JSON file named my_zis_bundle.json.

  3. Add the following bundle skeleton to my_zis_bundle.json:

    {  "name": "Example ZIS integration with Map state",  "description": "Redact attachments when ticket status changes to solved",  "zis_template_version": "2019-10-14",  "resources": {    "GetComments": {      "_placeholder_": "ZIS custom action definition goes here"    },    "RedactAttachment": {      "_placeholder_": "ZIS custom action definition goes here"    },    "RedactAttachmentsOnSolvedFlow": {      "_placeholder_": "ZIS flow goes here"    },    "RedactAttachmentsOnSolvedJobSpec": {      "_placeholder_": "ZIS job spec goes here"    }  }}

    You'll define the custom actions, ZIS flow, and job spec in the next steps.

  4. In my_zis_bundle.json, replace the GetComments placeholder with the following custom action definition.

    "GetComments": {  "type": "ZIS::Action::Http",  "properties": {    "name": "GetComments",    "definition": {      "method": "GET",      "path.$": "/api/v2/tickets/{{$.ticket_id}}/comments.json",      "connectionName": "zendesk",      "headers": [        {          "key": "Content-Type",          "value": "application/json"        }      ]    }  }},

    When called, the custom action makes a request to the Zendesk Support API's List Comments endpoint. The request requires a Zendesk ticket id. The request's response contains data for the ticket's comments, including any attachments.

  5. Replace the RedactAttachment placeholder with the following custom action definition.

    "RedactAttachment": {  "type": "ZIS::Action::Http",  "properties": {    "name": "RedactAttachment",    "definition": {      "method": "PUT",      "path.$": "/api/v2/tickets/{{$.ticket_id}}/comments/{{$.comment_id}}/attachments/{{$.attachment_id}}/redact",      "connectionName": "zendesk",      "headers": [        {          "key": "Content-Type",          "value": "application/json"        }      ]    }  }},

    When called, the custom action makes a request to the Zendesk Support API's Redact Comment Attachment endpoint. The request removes an attachment from a ticket comment. It replaces the attachment with an empty "redacted.txt" file.

  6. Replace the RedactAttachmentsOnSolvedFlow placeholder with the following ZIS flow definition. In the definition, replace "INTEGRATION" with your integration key.

    The flow contains a Map state, which is highlighted.

    "RedactAttachmentsOnSolvedFlow": {  "type": "ZIS::Flow",  "properties": {    "name": "RedactAttachmentsOnSolvedFlow",    "definition": {      "StartAt": "CheckStatus",      "States": {        "CheckStatus": {          "Type": "Choice",          "Choices": [            {              "Variable": "$.input.ticket_event.current",              "StringEquals": "solved",              "Next": "GetComments"            }          ],          "Default": "Finish"        },        "GetComments": {          "Type": "Action",          "ActionName": "zis:INTEGRATION:action:GetComments",          "Parameters": {            "ticket_id.$": "$.input.ticket_event.ticket.id"          },          "ResultPath": "$.input.ticket_comments",          "Next": "SanitizeTicketComments"        },        "SanitizeTicketComments": {          "Type": "Action",          "ActionName": "zis:common:transform:Jq",          "Parameters": {            "expr": "[.comments[] | (.attachments[] | (.id? // (.[] | .id))) as $id | {\"attachment_id\": \"\\($id)\", \"comment_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": "RedactAttachment"        },        "RedactAttachment": {          "Type": "Map",          "ItemsPath": "$.input.ticket_comments",          "ResultPath": "$.redact_attachment_output",          "Iterator": {            "StartAt": "Redact",            "States": {              "Redact": {                "Type": "Action",                "ActionName": "zis:INTEGRATION:action:RedactAttachment",                "Parameters": {                  "attachment_id.$": "$.attachment_id",                  "comment_id.$": "$.comment_id",                  "ticket_id.$": "$.ticket_id"                },                "Next": "Done"              },              "Done": {                "Type": "Succeed"              }            }          },          "Next": "Finish"        },        "Finish": {          "Type": "Succeed"        }      }    }  }},

    Note: In the above example, the Done state is a Succeed type, so an End property is not required. See Common state properties for more information.

    A Map state requires an array of JSON objects as input. The state can only access data in this input array. Before the Map state, the flow checks whether the event's current ticket status is "solved". If so, the flow gets data for the ticket's comments. The flow then uses Transform actions to change the comment data into an array of JSON objects with the following structure:

    [  {    "attachment_id": 1234567890123,    "comment_id": 123456,    "ticket_id": 1  },  {    "attachment_id": 4567890123456,    "comment_id": 789012,    "ticket_id": 1  }]

    Each object in the array represents an attachment in the ticket's comments. If a comment doesn't contain an attachment, the flow omits the comment from the array.

    The flow's Map state runs each object in the array through an Action state. The Action state uses the RedactAttachment custom action to redact the object's attachment.

  7. Replace the RedactAttachmentsOnSolvedJobSpec placeholder with the following job spec definition. In the definition, replace "INTEGRATION" with your integration key.

    "RedactAttachmentsOnSolvedJobSpec": {  "type": "ZIS::JobSpec",  "properties": {    "name": "RedactAttachmentsOnSolvedJobSpec",    "event_source": "support",    "event_type": "ticket.StatusChanged",    "flow_name": "zis:INTEGRATION:flow:RedactAttachmentsOnSolvedFlow"  }}

    The job spec tells ZIS to run the ZIS flow when it detects a Ticket Status Changed event.

  8. Save my_zis_bundle.json. The file should now look like this:

    {  "name": "Example ZIS integration with Map state",  "description": "Redact attachments when ticket status changes to solved",  "zis_template_version": "2019-10-14",  "resources": {    "GetComments": {      "type": "ZIS::Action::Http",      "properties": {        "name": "GetComments",        "definition": {          "method": "GET",          "path.$": "/api/v2/tickets/{{$.ticket_id}}/comments.json",          "connectionName": "zendesk",          "headers": [            {              "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",          "connectionName": "zendesk",          "headers": [            {              "key": "Content-Type",              "value": "application/json"            }          ]        }      }    },    "RedactAttachmentsOnSolvedFlow": {      "type": "ZIS::Flow",      "properties": {        "name": "RedactAttachmentsOnSolvedFlow",        "definition": {          "StartAt": "CheckStatus",          "States": {            "CheckStatus": {              "Type": "Choice",              "Choices": [                {                  "Variable": "$.input.ticket_event.current",                  "StringEquals": "solved",                  "Next": "GetComments"                }              ],              "Default": "Finish"            },            "GetComments": {              "Type": "Action",              "ActionName": "zis:INTEGRATION:action:GetComments",              "Parameters": {                "ticket_id.$": "$.input.ticket_event.ticket.id"              },              "ResultPath": "$.input.ticket_comments",              "Next": "SanitizeTicketComments"            },            "SanitizeTicketComments": {              "Type": "Action",              "ActionName": "zis:common:transform:Jq",              "Parameters": {                "expr": "[.comments[] | (.attachments[] | (.id? // (.[] | .id))) as $id | {\"attachment_id\": \"\\($id)\", \"comment_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": "RedactAttachment"            },            "RedactAttachment": {              "Type": "Map",              "ItemsPath": "$.input.ticket_comments",              "ResultPath": "$.redact_attachment_output",              "Iterator": {                "StartAt": "Redact",                "States": {                  "Redact": {                    "Type": "Action",                    "ActionName": "zis:INTEGRATION:action:RedactAttachment",                    "Parameters": {                      "attachment_id.$": "$.attachment_id",                      "comment_id.$": "$.comment_id",                      "ticket_id.$": "$.ticket_id"                    },                    "Next": "Done"                  },                  "Done": {                    "Type": "Succeed"                  }                }              },              "Next": "Finish"            },            "Finish": {              "Type": "Succeed"            }          }        }      }    },    "RedactAttachmentsOnSolvedJobSpec": {      "type": "ZIS::JobSpec",      "properties": {        "name": "RedactAttachmentsOnSolvedJobSpec",        "event_source": "support",        "event_type": "ticket.StatusChanged",        "flow_name": "zis:INTEGRATION:flow:RedactAttachmentsOnSolvedFlow"      }    }  }}
  9. Upload the bundle to ZIS.

    curl -X POST https://{subdomain}.zendesk.com/api/services/zis/registry/{integration}/bundles \  -u {email}/token:{api_token} \  -H "Content-Type: application/json" \  -d @my_zis_bundle.json
  10. Install the job spec to enable the integration.

    curl -X POST "https://{subdomain}.zendesk.com/api/services/zis/registry/job_specs/install?job_spec_name=zis:{integration}:job_spec:RedactAttachmentsOnSolvedJobSpec" \  -u {email}/token:{api_token}

Testing the integration

To test the integration, create an open ticket with an attachment. Then verify the integration redacts the attachment when you change the ticket's status to "solved".

  1. Right-click the following image link and save it to your computer as camera-pieces.png:

    [camera-pieces.png]

  2. In your shell, navigate to the folder where you saved the image. For example:

    $ cd ~/Downloads
  3. Use the following request to upload the image as an attachment.

    curl -X POST https://{subdomain}.zendesk.com/api/v2/uploads?filename=camera-pieces.png \  -u {email}/token:{api_token} \  -H "Content-Type: application/binary" \  --data-binary @camera-pieces.png

    Save the upload.token value from the response. You'll use the token in the next step.

    {  "upload": {    "token": "abcxyz",    "expires_at": "2099-05-06T00:00:00Z",    ...  }}
  4. Use the following request to create an open ticket with the attachment. Replace "TOKEN" with the token you saved in the previous step.

    curl -X POST https://{subdomain}.zendesk.com/api/v2/tickets \  -u {email}/token:{api_token} \  -H "Content-Type: application/json" \  -d '{    "ticket": {      "subject": "My homemade camera fell apart!",      "status": "open",      "comment": {        "body": "See attached.",        "uploads": ["TOKEN"]      }    }  }'

    Save the ticket.id value from the response. You'll use the id in the following steps.

    {  "ticket": {    "url": "https://SUBDOMAIN.zendesk.com/api/v2/tickets/30.json",    "id": 34,    "external_id": null,    ...  },  ...}
  5. Use the following request to fetch comments for the ticket you created. Replace "{ticket_id}" with the id you saved in the previous step.

    curl -X GET https://{subdomain}.zendesk.com/api/v2/tickets/{ticket_id}/comments.json \  -u {email}/token:{api_token}

    The response should contain an object in the comments.attachments property. The object's file_name property should contain the "camera-pieces.png" string.

    {  "comments": [    {      "id": 4567890123456,      ...      "attachments": [        {          "url": "https://SUBDOMAIN.zendesk.com/api/v2/attachments/1234567890123.json",          "id": 1234567890123,          "file_name": "camera-pieces.png",          ...        }      ],      ...    }  ],  ...}

    You also can verify the attachment in the Zendesk Support agent interface.

  6. Use the following request to change the ticket's status to "solved." Replace "{ticket_id}" with the id you saved in step 4.

    curl -X PUT https://{subdomain}.zendesk.com/api/v2/tickets/{ticket_id}.json \  -u {email}/token:{api_token} \  -H "Content-Type: application/json" \  -d '{    "ticket": {      "status": "solved",      "comment": {        "body": "Thanks for choosing Acme Co.",        "public": true      }    }  }'
  7. Fetch the ticket's comments again. Replace "{ticket_id}" with the id you saved in step 4.

    curl -X GET https://{subdomain}.zendesk.com/api/v2/tickets/{ticket_id}.json \  -u {email}/token:{api_token}

    The file_name property should now contain "redacted.txt".

    {  "comments": [    {      "id": 4567890123456,      ...      "attachments": [        {          "url": "https://SUBDOMAIN.zendesk.com/api/v2/attachments/1234567890123.json",          "id": 1234567890123,          "file_name": "redacted.txt",          ...        }      ],      ...    }  ],  ...}

    You also can verify the redaction in the Zendesk Support agent interface.

Congratulations! You've created a ZIS integration that uses a Map state to iterate through a collection. For another example of a ZIS integration, see the Zendesk app as an admin interface tutorial series.