Select to view content in your preferred language

Webtools: Prompt to download output file?

620
6
Jump to solution
02-26-2026 12:33 PM
ShareUser
Esri Community Manager

I have a webtool  (geoprocessing service) that generates a file at the end of its workflow.

How do I get it to automatically open the "download" window so that I can retrieve the file?

Thanks!

0 Kudos
1 Solution

Accepted Solutions
ShareUser
Esri Community Manager

Well, this is both annoying and embarrassing.

As it turns out, the error lay not in anything crazy with permissions, but in the custom report name. One of the fields used in the report name (${casetype}) has values that contain spaces. So the report named "WYWY123456789__Telephone Lines.pdf" becomes "WYWY123456789__Telephone%20Lines.pdf" when output as a URL. However, there isn't any file named "WYWY123456789__Telephone%20Lines.pdf", so the url doesn't work.

Changing the report title scheme fixed the problem.

The other thing I found is that the step of copying the report seems to be load-bearing. I removed it after I got things working originally and it appears that the output was invalid unless you also copy.

 

In answer to my original question, setting the output to a DOCX will prompt that download window. I assume it's also the case for a zip file. PDFs like to open in the browser.

The final question that this workflow leaves is whether I need to somehow clear out the output folder somehow, but I feel like that's a problem for another day.

Thanks for the help everyone.

 

Edit: Here is my solution to get rid of those pesky bad characters by reversing the URL and whitelisting valid characters

 

import arcpy
import arcgis
import os
import shutil
import urllib
class reportGenerator:
    def __init__(self):
        """Define the tool (tool name is the name of the class)."""
        self.label = "Report Generator"
        self.description = ""

    def getParameterInfo(self):
        """Define the tool parameters."""
        inFeature = arcpy.Parameter(displayName='Input Feature',
                                 name='inFeature',
                                 datatype='GPFeatureRecordSetLayer',
                                 parameterType='Required',
                                 #multiValue = True,
                                 direction='Input')

        outFile = arcpy.Parameter(displayName='Output file',
                                 name='outFile',
                                 datatype='DEFile',
                                 parameterType='Optional',
                                 #multiValue = True,
                                 direction='Output')
        outFile.enabled = False

        outType = arcpy.Parameter(displayName='Output file format',
                                 name='outType',
                                 datatype='GPString',
                                 parameterType='Required',
                                 #multiValue = True,
                                 direction='Input')
        outType.filter.list = ["docx", "pdf"]
        outType.value = "pdf"
        #
        params = [
                  inFeature,
                  outFile,
                  outType
                  ]
        return params

    def isLicensed(self):
        return True

    def updateParameters(self, params):
        return

    def updateMessages(self, params):
        return

    def execute(self, params, messages):
        """The source code of the tool."""
        def main(oids: list, outType: str)-> None:
            '''
            Takes the input features and exports them to a feature
            report in either PDF or Word format.

            :param oids: List of objectids to export
            :param outType: Output file format. Either PDF or DOCX.
            :return: None
            '''

            scratchenv = arcpy.env.scratchFolder
            gis = arcgis.gis.GIS("home")
            sman = arcgis.apps.survey123.SurveyManager(gis)
            sID = sman.get('YourFormIDHere')#Form Item itemID.
            templates = sID.report_templates
            # Set the request message
            oids = [str(o) for o in oids]
            oids = ", ".join(oids)
            where_clause = f"objectid in ({oids})"
            # No case type because they contain spaces and that breaks
            # the URL.
            reportTitle = "${cse_nr}-${leg_cse_nr}${solotpass}${preappname}"#${inspecdate}"
            # Generate the report(s)
            pdf_report = sID.generate_report(templates[0],
                                             where= where_clause,
                                             output_format=outType,#)
                                             report_title = reportTitle)
            ''' Copy to the scratch folder
                This is load-bearing and must be done.
            '''
            # Get rid of all bad characters by reverse-engineering the
            # url and then passing the name through the whitelist
            validchars = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
                          'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
                          'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D',
                          'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
                          'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
                          'Y', 'Z', '1', '2', '3', '4', '5', '6', '7', '8',
                          '9', '0', "_", "-", "."]

            out_name = os.path.basename(str(pdf_report))
            out_name = urllib.parse.unquote(out_name)
            out_pdf = ""
            for char in out_name:
                if char in validchars:
                    out_pdf += char
            out_pdf = out_pdf.strip("-_")
            out_pdf = os.path.join(scratchenv, out_pdf)
            shutil.copy(pdf_report, out_pdf)

            arcpy.AddMessage(f"Successfully exported. "
                             f"Check the parameters to download.")
            # Set the output parameter to the address so that they can
            # find it easily.
            arcpy.SetParameter(1, out_pdf)

            return

        if __name__ == "__main__":
            # return
            # Set everything to a Featureset to work in Portal.
            inFeature = getPName(params, "inFeature").value
            inFeature = arcpy.FeatureSet(inFeature)
            geoJSON = json.loads(inFeature.JSON)
            totCount = arcpy.management.GetCount(r"https://pathtofeatureservice")
            totCount = int(totCount[0])
            oids = [f["attributes"]["objectid"] for f in geoJSON["features"]]
            # Webtools aren't aware of map selections so this checks to
            # see if you actually made one.
            if len(oids) == totCount:
                arcpy.AddWarning("No selection was made. Tool not run.")
                return
            outType = getPName(params, "outType").value
            main(oids, outType)
        return

    def postExecute(self, params):
        return

I haven't thought of a good way to format any date fields in a way I like (They print to March 6, 2026  and I'd like 20260306) but the good news is this appears to export all my records just fine.

View solution in original post

0 Kudos
6 Replies
ShareUser
Esri Community Manager

I've had success with making the file an output parameter, this outputs a URL in the old Web AppBuilder geoprocessing widget (I hope the ExB widget does the same thing, can't test that yet) and if you're doing manual HTTP work you should get the URL somewhere in the response data (sync) or you follow the URL trail from the final status update (async). What's the client you're using to run the tool?

0 Kudos
ShareUser
Esri Community Manager

Sooooo, I'm running this through Experience Builder on Enterprise.

I do get a url in the output parameter, but it goes to someplace that doesn't exist when I open it. 

My tool is an attempt to get around the various issues I've been having with the feature report widget. By leaving the "folder_id" parameter blank for generate function, it allegedly should go to the scratch space by default? The output URLs seem to point to a scratch space, although I get a 400 error.

Screenshot 2026-02-26 152839.png

https://.../hosted/rest/directories/arcgisjobs/test_report_generator_gpserver/.../scratch/myreport.pdf   

I'm trying to ask a colleague how he got his to work, but he's out of the office and also his tool works primarily via AGOL, so even though it may truly live on Enterprise, that might change things. 

I know his tool uses the scratch environment (arcpy.env.scratchFolder). 

I guess a potential workaround to try might be to like, manually copy the output to "scratch environment" and return that? Idk. 

 

In any case, I think it'd be useful to force the download window open so that my users actually retrieve the report.

 

0 Kudos
ShareUser
Esri Community Manager

I double-checked a few of my tools and yes, you want to get your file into arcpy.env.scratchFolder and then set a derived File output parameter to the full path to that file. I forget exactly how to configure the parameter during publishing but I don't remember any gotchas. You can even leverage the zipfile module to write your output(s) into a nice little file for the user.

0 Kudos
ShareUser
Esri Community Manager

We have a geoprocessing service that outputs a pdf and/or a zip file, depending on the users needs. We send Survey123 records to the gp service from a Web AppBuilder application in ArcGIS Online. We return url's for the outputs to the web application, and the user clicks on the url's to pop open a new browser tab where the pdf/zip can be viewed/downloaded. I assume we could host the gp service on our Enterprise and have it configured identically. I don't think the web application "cares" where the gp service is running.

The gp service uses the ScratchFolder for outputs. When run locally during development the output is in some temp scratch folder in the user profile. When published to the server as a gp service the output adjusts to become the job scratch folder.

Screenshot 2026-02-27 100528.png

Here is an example output url, minus a few details specific to our implementation, showing the service job id and scratch in the path.

https://xyzxy/rest/directories/arcgisjobs/geoprocessingservices/XXXreportprocessphase2_gpserver/jc3f...

 

Screenshot of the application with a url returned from the process for customer use

Screenshot 2026-02-27 094452.png

Our tool in Pro is configured like this (screenshot)

Screenshot 2026-02-27 100239.png

Hope some of that might be useful.

0 Kudos
ShareUser
Esri Community Manager

I feel like I have to be missing something pretty basic here.

Here is the tool from the pyt. (If I'm missing an import just assume that it's actually there).

import arcgis
import arcpy
import os
import shutil
class lafiReportGenerator:
    def __init__(self):
        """Define the tool (tool name is the name of the class)."""
        self.label = "Report Generator"
        self.description = ""

    def getParameterInfo(self):
        """Define the tool parameters."""
        inFeature = arcpy.Parameter(displayName='Input Feature',
                                 name='inFeature',
                                 datatype='GPFeatureRecordSetLayer',
                                 parameterType='Required',
                                 #multiValue = True,
                                 direction='Input')

        outFile = arcpy.Parameter(displayName='Output file',
                                 name='outFile',
                                 datatype='DEFile',
                                 parameterType='Optional',
                                 #multiValue = True,
                                 direction='Output')
        outFile.enabled = False

        outType = arcpy.Parameter(displayName='Output file format',
                                 name='outType',
                                 datatype='GPString',
                                 parameterType='Required',
                                 direction='Input')
        outType.filter.list = ["docx", "pdf"]
        outType.value = "pdf"

        params = [
                  inFeature,
                  outFile,
                  outType
                  ]
        # params = None
        return params

    def isLicensed(self):
        """Set whether the tool is licensed to execute."""
        return True

    def updateParameters(self, params):
        """Modify the values and properties of parameters before internal
        validation is performed.  This method is called whenever a parameter
        has been changed."""
        return

    def updateMessages(self, params):
        """Modify the messages created by internal validation for each tool
        parameter. This method is called after internal validation."""
        return

    def execute(self, params, messages):
        """The source code of the tool."""
        def main(oids, outType):
            scratchenv = arcpy.env.scratchFolder
            gis = arcgis.gis.GIS("home")
            sman = arcgis.apps.survey123.SurveyManager(gis)
            sID = sman.get('ItemIDHere')#Form Item itemID.
            templates = sID.report_templates

            oids = [str(o) for o in oids]
            oids = ", ".join(oids)
            where_clause = f"objectid in ({oids})"
            reportTitle = "${cse_nr}_${leg_cse_nr}_${solotpass}_${casetype}"

            pdf_report = sID.generate_report(templates[0],
                                             where= where_clause,
                                             output_format=outType,#)
                                             report_title = reportTitle)

            out_pdf = os.path.join(scratchenv,
                                   os.path.basename(str(pdf_report)))
            shutil.copy(pdf_report, out_pdf)
            arcpy.AddMessage(out_pdf)
            arcpy.AddMessage(f"Successfully exported. "
                             f"Check the parameters to download.")
            arcpy.SetParameter(1, out_pdf)

            return

        if __name__ == "__main__":
            # return
            inFeature = getPName(params, "inFeature").value
            inFeature = arcpy.FeatureSet(inFeature)
            geoJSON = json.loads(inFeature.JSON)
            totCount = arcpy.management.GetCount(r"https://myservicethat thesurveybelongsto/FeatureServer/0")
            totCount = int(totCount[0])
            oids = [f["attributes"]["objectid"] for f in geoJSON["features"]]
            if len(oids) == totCount:
                arcpy.AddWarning("No selection was made. Tool not run.")
                return
            outType = getPName(params, "outType").value
            main(oids, outType)
        return

    def postExecute(self, params):
        """This method takes place after outputs are processed and
        added to the display."""
        return

It works great in Pro. In EB I run the tool, it runs successfully, and I get a link of some sort in the output parameter. Clicking on the link sends me to a 400 error as I showed in my earlier post.

Screenshot 2026-02-27 130024.png

Good news is I copied the GP service to AGOL and it failed there too.

So, I'm reasonably sure this is user error on my end but I can't figure out what.

0 Kudos
ShareUser
Esri Community Manager

Well, this is both annoying and embarrassing.

As it turns out, the error lay not in anything crazy with permissions, but in the custom report name. One of the fields used in the report name (${casetype}) has values that contain spaces. So the report named "WYWY123456789__Telephone Lines.pdf" becomes "WYWY123456789__Telephone%20Lines.pdf" when output as a URL. However, there isn't any file named "WYWY123456789__Telephone%20Lines.pdf", so the url doesn't work.

Changing the report title scheme fixed the problem.

The other thing I found is that the step of copying the report seems to be load-bearing. I removed it after I got things working originally and it appears that the output was invalid unless you also copy.

 

In answer to my original question, setting the output to a DOCX will prompt that download window. I assume it's also the case for a zip file. PDFs like to open in the browser.

The final question that this workflow leaves is whether I need to somehow clear out the output folder somehow, but I feel like that's a problem for another day.

Thanks for the help everyone.

 

Edit: Here is my solution to get rid of those pesky bad characters by reversing the URL and whitelisting valid characters

 

import arcpy
import arcgis
import os
import shutil
import urllib
class reportGenerator:
    def __init__(self):
        """Define the tool (tool name is the name of the class)."""
        self.label = "Report Generator"
        self.description = ""

    def getParameterInfo(self):
        """Define the tool parameters."""
        inFeature = arcpy.Parameter(displayName='Input Feature',
                                 name='inFeature',
                                 datatype='GPFeatureRecordSetLayer',
                                 parameterType='Required',
                                 #multiValue = True,
                                 direction='Input')

        outFile = arcpy.Parameter(displayName='Output file',
                                 name='outFile',
                                 datatype='DEFile',
                                 parameterType='Optional',
                                 #multiValue = True,
                                 direction='Output')
        outFile.enabled = False

        outType = arcpy.Parameter(displayName='Output file format',
                                 name='outType',
                                 datatype='GPString',
                                 parameterType='Required',
                                 #multiValue = True,
                                 direction='Input')
        outType.filter.list = ["docx", "pdf"]
        outType.value = "pdf"
        #
        params = [
                  inFeature,
                  outFile,
                  outType
                  ]
        return params

    def isLicensed(self):
        return True

    def updateParameters(self, params):
        return

    def updateMessages(self, params):
        return

    def execute(self, params, messages):
        """The source code of the tool."""
        def main(oids: list, outType: str)-> None:
            '''
            Takes the input features and exports them to a feature
            report in either PDF or Word format.

            :param oids: List of objectids to export
            :param outType: Output file format. Either PDF or DOCX.
            :return: None
            '''

            scratchenv = arcpy.env.scratchFolder
            gis = arcgis.gis.GIS("home")
            sman = arcgis.apps.survey123.SurveyManager(gis)
            sID = sman.get('YourFormIDHere')#Form Item itemID.
            templates = sID.report_templates
            # Set the request message
            oids = [str(o) for o in oids]
            oids = ", ".join(oids)
            where_clause = f"objectid in ({oids})"
            # No case type because they contain spaces and that breaks
            # the URL.
            reportTitle = "${cse_nr}-${leg_cse_nr}${solotpass}${preappname}"#${inspecdate}"
            # Generate the report(s)
            pdf_report = sID.generate_report(templates[0],
                                             where= where_clause,
                                             output_format=outType,#)
                                             report_title = reportTitle)
            ''' Copy to the scratch folder
                This is load-bearing and must be done.
            '''
            # Get rid of all bad characters by reverse-engineering the
            # url and then passing the name through the whitelist
            validchars = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
                          'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
                          'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D',
                          'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
                          'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
                          'Y', 'Z', '1', '2', '3', '4', '5', '6', '7', '8',
                          '9', '0', "_", "-", "."]

            out_name = os.path.basename(str(pdf_report))
            out_name = urllib.parse.unquote(out_name)
            out_pdf = ""
            for char in out_name:
                if char in validchars:
                    out_pdf += char
            out_pdf = out_pdf.strip("-_")
            out_pdf = os.path.join(scratchenv, out_pdf)
            shutil.copy(pdf_report, out_pdf)

            arcpy.AddMessage(f"Successfully exported. "
                             f"Check the parameters to download.")
            # Set the output parameter to the address so that they can
            # find it easily.
            arcpy.SetParameter(1, out_pdf)

            return

        if __name__ == "__main__":
            # return
            # Set everything to a Featureset to work in Portal.
            inFeature = getPName(params, "inFeature").value
            inFeature = arcpy.FeatureSet(inFeature)
            geoJSON = json.loads(inFeature.JSON)
            totCount = arcpy.management.GetCount(r"https://pathtofeatureservice")
            totCount = int(totCount[0])
            oids = [f["attributes"]["objectid"] for f in geoJSON["features"]]
            # Webtools aren't aware of map selections so this checks to
            # see if you actually made one.
            if len(oids) == totCount:
                arcpy.AddWarning("No selection was made. Tool not run.")
                return
            outType = getPName(params, "outType").value
            main(oids, outType)
        return

    def postExecute(self, params):
        return

I haven't thought of a good way to format any date fields in a way I like (They print to March 6, 2026  and I'd like 20260306) but the good news is this appears to export all my records just fine.

0 Kudos