Wednesday, March 10, 2021

UltiPro Sync User Attributes to Active Directory (Report-as-a-Service + SQL)

UltiPro has two methods of REST API access. For the base APIs, you can find my prior post. This post will deal with the custom "Report-as-a-Service" (RaaS) API. This allows HR staff to author custom reports via Cognos Analytics. Most commonly, they'll email these around or do FTP. Many people don't realize that these APIs exist and streamline data exchange with improved security.


For more info on setting up Azure Automation variables and where to find the API documentation, please see my original post. This post will concentrate on the scripting for RaaS as well as exporting the data to SQL rather than CSV.


RaaS

The process is documented but not especially intuitive in the UltiPro API library. The process requires 5 steps to retrieve a report. Some of these steps may seem redundant. Don't try to take shortcuts. I tried, it won't work.

  1. Logon (create a session)
  2. Get the Report List
  3. Execute the Report
  4. Retrieve the Report
  5. Logoff (end the session)

The Script

Most of the parameters are at the start of the script. But be sure to update the SQL server names if you plan to use that section. Those aren't set as variables (don't remember why, maybe laziness).

You can break this into 4 distinct sections:

  • UltiPro data retrieval
  • AD data retrieval (fetch distinguished names of managers)
  • SQL Import for UltiPro data
  • SQL Import for AD data
*FYI, we just filter out certain employee IDs like "999999" which for us just indicate a contractor. Feel free to remove it.

SQL Setup

I worked with a DBA to create a DB and tables. I just provided a spreadsheet of columns, data types, and expected character limits. Be sure that your data set matches the destination.

Our DBA also created a stored procedure to call during the import that completely overwrites the data set every time. For our purposes, we didn't need to keep the data. We wanted it fresh every X hours.

We use two tables + a view

  1. Contains the UltiPro
  2. Contains the AD manager DN (so we can match UltiPro manager ID to a usable attribute.
  3. The view combines the two (which we use ManageEngine AD Manager to process)





#initialize variables from automation library
$dataserviceUrl = Get-AutomationVariable -Name 'UltiPro Data Service URL'
$streamingserviceUrl = Get-AutomationVariable -Name 'UltiPro Streaming Service URL'
$ClientAccessKey = Get-AutomationVariable -Name 'UltiPro ClientAccessKey'
$UserAccessKey = Get-AutomationVariable -Name 'UltiPro UserAccessKey'
$apiuser = Get-AutomationPSCredential -Name 'UltiPro Web API User'
$reportname = Get-AutomationVariable -Name 'UltiPro Report Name'

#Extract secure password string from account storage to plain text
$username = $apiuser.username
$password = [System.Net.NetworkCredential]::new("", $apiuser.password).Password

#configure reusable http header
$headers =  @{'Content-Type' = 'application/soap+xml; charset=utf-8'}

#reset variables
$serviceID = ""
$token = ""
$InstanceKey = ""

##########################################
#Logon to UltiPro Report-as-a-Service API#
##########################################
write-output "Connecting to Report-as-a-Service API"
$body = @"
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing">
    <s:Header>
        <a:Action s:mustUnderstand="1">http://www.ultipro.com/dataservices/bidata/2/IBIDataService/LogOn</a:Action>
        <a:To s:mustUnderstand="1">$dataserviceUrl</a:To>
    </s:Header>
    <s:Body>
        <LogOn xmlns="http://www.ultipro.com/dataservices/bidata/2">
            <logOnRequest xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
                <UserName>$username</UserName>
                <Password>$password</Password>
                <ClientAccessKey>$ClientAccessKey</ClientAccessKey>
                <UserAccessKey>$UserAccessKey</UserAccessKey>
            </logOnRequest>
        </LogOn>
    </s:Body>
</s:Envelope>
"@
[xml]$logon = Invoke-RestMethod -Method POST -Uri $dataserviceUrl -headers $headers -Body $body -Credential $cred

##########################################
#         Set Session Specific IDs       #
##########################################
write-output "Fetching session IDs"
$serviceID = $logon.Envelope.body.LogOnResponse.LogOnResult.serviceID
$token = $logon.Envelope.body.LogOnResponse.LogOnResult.Token
$InstanceKey = $logon.Envelope.body.LogOnResponse.LogOnResult.InstanceKey

write-output "Session ID: $serviceID"

##########################################
#             Get Report List            #
##########################################
write-output "Enumerating report list"
$body = @"
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing">
    <s:Header>
        <a:Action s:mustUnderstand="1">http://www.ultipro.com/dataservices/bidata/2/IBIDataService/GetReportList</a:Action>
        <a:To s:mustUnderstand="1">$dataserviceUrl</a:To>
    </s:Header>
    <s:Body>
        <GetReportList xmlns="http://www.ultipro.com/dataservices/bidata/2">
            <context xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
                <ServiceId>$serviceID</ServiceId>
                <ClientAccessKey>$ClientAccessKey</ClientAccessKey>
                <Token>$token</Token>
                <Status>Ok</Status>
                <StatusMessage i:nil="true"/>
                <InstanceKey>$instanceKey</InstanceKey>
            </context>
        </GetReportList>
    </s:Body>
</s:Envelope>
"@
[xml]$ReportList = Invoke-RestMethod -Method POST -Uri $dataserviceUrl -headers $headers -Body $body -Credential $cred

$reportscount = ($reportlist.Envelope.body.GetReportListResponse.GetReportListResult.reports.Report).count

write-output "Reports Found: $reportscount"

##########################################
#             Execute Report             #
##########################################
write-output "Excecuting AD Sync Report"
#Get report based on name defined in UltiPro
$reportPath = $ReportList.Envelope.body.GetReportListResponse.GetReportListResult.Reports.Report | `
    Where-Object{$_.reportname -eq "$reportname"} | `
    Select-Object -ExpandProperty ReportPath

#Specify delimiter in header (if not specified, defaults to XML)
$executeHeaders =  @{ `
      'Content-Type' = 'application/soap+xml; charset=utf-8'; `
      'US-DELIMITER' = ","; `
  }

#Configure XML Header/Body payload for Execute Invoke REST
$body = @"
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing">
    <s:Header>
        <a:Action s:mustUnderstand="1">http://www.ultipro.com/dataservices/bidata/2/IBIDataService/ExecuteReport</a:Action>
        <a:To s:mustUnderstand="1">$dataserviceUrl</a:To>
    </s:Header>
    <s:Body>
        <ExecuteReport xmlns="http://www.ultipro.com/dataservices/bidata/2">
            <request xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
                <ReportPath>$ReportPath</ReportPath>
                <ReportParameters/>
            </request>
            <context xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
                <ServiceId>$serviceID</ServiceId>
                <ClientAccessKey>$ClientAccessKey</ClientAccessKey>
                <Token>$token</Token>
                <Status>Ok</Status>
                <StatusMessage i:nil="true"/>
                <InstanceKey>$instanceKey</InstanceKey>
            </context>
        </ExecuteReport>
    </s:Body>
</s:Envelope>
"@

[xml]$execute = Invoke-RestMethod -Method POST -Uri $dataserviceUrl -headers $executeHeaders -Body $body

#Set the report key retrieved during execution to be used in the report streaming steps next
$reportKey = $execute.Envelope.body.ExecuteReportResponse.ExecuteReportResult.ReportKey

write-output "ReportKey: $reportkey"

##########################################
#             RetrieveReport             #
##########################################
write-output "Fetching report results output"
#Set retrieval header and specify stream output
$retrieveReportURL = $streamingserviceUrl + '?EmploymentStatus=A'
$retrieveHeaders =  @{ `
    'Content-Type' = 'application/soap+xml; charset=utf-8'; `
    'Accept-Encoding' = 'gzip, deflate'; `
}

#Stream output SOAP body
$body = @"
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing">
    <s:Header>
        <a:Action s:mustUnderstand="1">http://www.ultipro.com/dataservices/bistream/2/IBIStreamService/RetrieveReport</a:Action>
        <h:ReportKey xmlns:h="http://www.ultipro.com/dataservices/bistream/2" xmlns="http://www.ultipro.com/dataservices/bistream/2">$reportKey</h:ReportKey>
        <a:To s:mustUnderstand="1">$streamingserviceUrl</a:To>
    </s:Header>
    <s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
        <RetrieveReportRequest xmlns="http://www.ultipro.com/dataservices/bistream/2"/>
    </s:Body>
</s:Envelope>
"@

$response = Invoke-RestMethod -Method POST -Uri $retrieveReportURL -headers $retrieveHeaders -Body $body

##########################################
#                LogOff                  #
##########################################
write-output "Disconnecting session"

#Disconnect from the data service API
$body = @"
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing">
    <s:Header>
        <a:Action s:mustUnderstand="1">http://www.ultipro.com/dataservices/bidata/2/IBIDataService/LogOff</a:Action>
        <a:To s:mustUnderstand="1">$dataserviceUrl</a:To>
    </s:Header>
    <s:Body>
        <LogOff xmlns="http://www.ultipro.com/dataservices/bidata/2">
            <context xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
                <ServiceId>$serviceID</ServiceId>
                <ClientAccessKey>$ClientAccessKey</ClientAccessKey>
                <Token>$token</Token>
                <Status>Ok</Status>
                <StatusMessage i:nil="true"/>
                <InstanceKey>$InstanceKey</InstanceKey>
            </context>
        </LogOff>
    </s:Body>
</s:Envelope>
"@

Invoke-RestMethod -Method POST -Uri $dataserviceUrl -headers $headers -Body $body

##########################################
#        Convert Base64 to Array         #
##########################################
write-output "Converting report stream data from Base64 to array/table format"
#Convert output from Base64 to string data (comes out as XML from )
$bytes = [Convert]::FromBase64String($response.Envelope.body.StreamReportResponse.ReportStream)
$csvdata = [System.Text.Encoding]::ASCII.GetString($bytes)
$arraydata = $csvdata | convertFrom-csv

##########################################
#             Find Managers              #
##########################################

$managers = get-aduser -filter * -Properties employeeID,Manager | where-object {
    $_.employeeID -notlike "666666" -and `
    $_.employeeID -notlike "888888" -and `
    $_.employeeID -notlike "999999" -and `
    $_.employeeID -ne $null
} | Select-Object employeeID, Manager, distinguishedname

##########################################
#          Output UltiPro to SQL         #
##########################################

write-output "Creating empty SQL data table in memory"
#build an empty data table in the expected SQL format
[System.Data.DataTable]$table = New-Object('system.Data.DataTable')
$table.Columns.Add("givenName", "System.String")
$table.Columns.Add("middleName", "System.String")
$table.Columns.Add("lastName", "System.String")
$table.Columns.Add("telephoneNumber", "System.String")
$table.Columns.Add("homePhone", "System.String")
$table.Columns.Add("streetAddress", "System.String")
$table.Columns.Add("city", "System.String")
$table.Columns.Add("state", "System.String")
$table.Columns.Add("postalCode", "System.String")
$table.Columns.Add("physicalDeliveryOfficeName", "System.String")
$table.Columns.Add("department", "System.String")
$table.Columns.Add("Title", "System.String")
$table.Columns.Add("company", "System.String")
$table.Columns.Add("employeeID", "System.String")
$table.Columns.Add("division", "System.String")
$table.Columns.Add("orglevel2code", "System.String")
$table.Columns.Add("orglevel3code", "System.String")
$table.Columns.Add("employeeStatusCode", "System.String")
$table.Columns.Add("ManagerEmployeeID", "System.String")
$table.Columns.Add("preferredName", "System.String")
$table.Columns.Add("mail", "System.String")
$table.Columns.Add("location", "System.String")
$table.Columns.Add("jobCode", "System.String")

write-output "Adding report data to PowerShell SQL data table object"
foreach($aRow in $arraydata)
{
    $row = $table.NewRow()
        $row.givenName = $arow.FirstName

        if($arow.MiddleName.length -ne 0)
        {$row.middleName = $arow.MiddleName.Substring(0,1)}

        $row.lastName = $arow.LastName
        $row.telephoneNumber = $arow.WorkPhone
        $row.homePhone = $arow.HomePhone
        $row.streetAddress = $arow.Address
        $row.city = $arow.City
        $row.state = $arow.State
        $row.postalCode = $arow.Zip
        $row.physicalDeliveryOfficeName = $arow.Address
        $row.department = $arow.OrgLevel2
        $row.Title = $arow.Title
        $row.company = $arow.Company
        $row.employeeID = $arow.EmployeeNumber
        $row.division = $arow.OrgLevel1Code
        $row.orglevel2code = $arow.OrgLevel2Code
        $row.orglevel3code = $arow.OrgLevel3Code
        $row.employeeStatusCode = $arow.EmployeeStatus
        $row.ManagerEmployeeID = $arow.EmployeeNumberSupervisor
        #$row.preferredName = $arow.
        #$row.mail = $arow.
        #$row.location = $arow.Location
        #$row.jobCode = $arow.JobCode

        $table.Rows.Add($row)
}

$tablecount = $table.Rows.count
write-output "Discovered $tablecount employee records"

write-output "Connecting to SQL Instance"
#Create Connection string
$cnString = 'Integrated Security=SSPI;Persist Security Info=False;Initial Catalog=DBMaintenance;Data Source=SERVERNAMEHERE'

#define empty system objects for command and connection strings
[System.Data.SqlClient.SqlCommand]$cmd = New-Object('system.data.sqlclient.sqlcommand')
[System.Data.SqlClient.SqlConnection]$c = New-Object('system.data.sqlclient.sqlconnection')

#set $c to connection string and open connection
$c.connectionstring = $cnString;
$c.open();

write-output "Executing SQL stored procedure"
#configure command type as stored procedure
$cmd.Connection = $c;
$cmd.CommandType = [System.Data.CommandType]::StoredProcedure
$cmd.commandText = "dbo.ADSync_InsertAll"
$perfResultsParam = New-Object('system.data.sqlclient.sqlparameter')

#sql table type param
$perfResultsParam.ParameterName = "ADSyncT"
$perfResultsParam.SqlDBtype = [System.Data.SqlDbType]::Structured
$perfResultsParam.Direction = [System.Data.ParameterDirection]::Input
$perfResultsParam.value = $table

$cmd.parameters.add($perfResultsParam);       
$cmd.executeNonQuery(); 

write-output "Disconnecting from SQL Instance"
$c.close();

write-output "Data retrieval and import complete"

##########################################
#          Output AD Data to SQL         #
##########################################

write-output "Creating empty SQL data table in memory"
#build an empty data table in the expected SQL format
[System.Data.DataTable]$table2 = New-Object('system.Data.DataTable')
$table2.Columns.Add("employeeID", "System.String")
$table2.Columns.Add("distName", "System.String")

write-output "Adding data to PowerShell SQL data table object"
foreach($manager in $managers)
{
    $row = $table2.NewRow()
        $row.employeeID = $manager.employeeID
        $row.distName = $manager.distinguishedname

        $table2.Rows.Add($row)
}

$table2count = $table2.Rows.count
write-output "Discovered $table2count employee records"

write-output "Connecting to SQL Instance"
#Create Connection string
$cnString = 'Integrated Security=SSPI;Persist Security Info=False;Initial Catalog=DBMaintenance;Data Source=SERVERNAMEHERE'

#define empty system objects for command and connection strings
[System.Data.SqlClient.SqlCommand]$cmd = New-Object('system.data.sqlclient.sqlcommand')
[System.Data.SqlClient.SqlConnection]$c = New-Object('system.data.sqlclient.sqlconnection')

#set $c to connection string and open connection
$c.connectionstring = $cnString;
$c.open();

write-output "Executing SQL stored procedure"
#configure command type as stored procedure
$cmd.Connection = $c;
$cmd.CommandType = [System.Data.CommandType]::StoredProcedure
$cmd.commandText = "dbo.ADDist_InsertAll"
$perfResultsParam = New-Object('system.data.sqlclient.sqlparameter')

#sql table type param
$perfResultsParam.ParameterName = "ADDistT"
$perfResultsParam.SqlDBtype = [System.Data.SqlDbType]::Structured
$perfResultsParam.Direction = [System.Data.ParameterDirection]::Input
$perfResultsParam.value = $table2

$cmd.parameters.add($perfResultsParam);       
$cmd.executeNonQuery(); 

write-output "Disconnecting from SQL Instance"
$c.close();

write-output "AD Data retrieval and import complete"

14 comments:

  1. Thank you! I was able to adapt this for my use and it is working great!

    ReplyDelete
    Replies
    1. Glad it helped. This is what it's all about.

      Delete
  2. Absolutely incredible, thank you very much. The original documentation was way over my head especially with only having C# examples, and this is very plug and play in the way you've laid it out.

    ReplyDelete
  3. I gotta give you props this came in super helpful, you the Man!

    ReplyDelete
  4. What language is this? can I use it in Visual Studio?

    ReplyDelete
    Replies
    1. It’s PowerShell, and yes. VS or VS Code will work.

      Delete
  5. Thank you, I hope to pass on the knowledge in the future as you have.

    ReplyDelete
  6. Hey, thanks a lot for the post, it is very helpful!!!!
    I have a inquiry, do you know what could be the difference in the response using the same parameters to get the report, you make the reference to the response in:"$response.Envelope.body.StreamReportResponse.ReportStream", however, the response I get is the following:

    $response.definitions
    name : BIStreamService
    targetNamespace : http://tempuri.org/
    wsdl : http://schemas.xmlsoap.org/wsdl/
    xsd : http://www.w3.org/2001/XMLSchema
    soapenc : http://schemas.xmlsoap.org/soap/encoding/
    wsu : http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd
    soap : http://schemas.xmlsoap.org/wsdl/soap/
    soap12 : http://schemas.xmlsoap.org/wsdl/soap12/
    tns : http://tempuri.org/
    wsa : http://schemas.xmlsoap.org/ws/2004/08/addressing
    wsx : http://schemas.xmlsoap.org/ws/2004/09/mex
    wsap : http://schemas.xmlsoap.org/ws/2004/08/addressing/policy
    wsaw : http://www.w3.org/2006/05/addressing/wsdl
    msc : http://schemas.microsoft.com/ws/2005/12/wsdl/contract
    i0 : http://www.ultipro.com/dataservices/bistream/2
    wsp : http://schemas.xmlsoap.org/ws/2004/09/policy
    wsa10 : http://www.w3.org/2005/08/addressing
    wsam : http://www.w3.org/2007/05/addressing/metadata
    Policy : Policy
    import : import
    types :
    binding : {WSHttpBinding_IBIStreamService, BasicHttpBinding_IBIStreamService}
    service: service

    ReplyDelete
    Replies
    1. I'm using:
      ##########################################
      # RetrieveReport #
      ##########################################
      write-output "Fetching report results output"
      #Set retrieval header and specify stream output
      $retrieveReportURL = $streamingserviceUrl# + '?EmploymentStatus=A'
      $retrieveHeaders = @{ `
      'Content-Type' = 'application/soap+xml; charset=utf-8'; `
      'Accept-Encoding' = 'gzip, deflate'; `
      }

      #Stream output SOAP body
      $body = @"


      http://www.ultipro.com/dataservices/bistream/2/IBIStreamService/RetrieveReport
      $reportKey
      $streamingserviceUrl





      "@

      $response = Invoke-RestMethod -Method POST -Uri $retrieveReportURL -headers $retrieveHeaders -Body $body

      Delete
    2. Hey, thanks a lot for the post, it is very helpful!!!!
      I have a inquiry, do you know what could be the difference in the response using the same parameters to get the report, you make the reference to the response in:"$response.Envelope.body.StreamReportResponse.ReportStream", however, the response I get is the following:

      $response.definitions
      name : BIStreamService
      targetNamespace : http://tempuri.org/
      wsdl : http://schemas.xmlsoap.org/wsdl/
      xsd : http://www.w3.org/2001/XMLSchema
      soapenc : http://schemas.xmlsoap.org/soap/encoding/
      wsu : http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd
      soap : http://schemas.xmlsoap.org/wsdl/soap/
      soap12 : http://schemas.xmlsoap.org/wsdl/soap12/
      tns : http://tempuri.org/
      wsa : http://schemas.xmlsoap.org/ws/2004/08/addressing
      wsx : http://schemas.xmlsoap.org/ws/2004/09/mex
      wsap : http://schemas.xmlsoap.org/ws/2004/08/addressing/policy
      wsaw : http://www.w3.org/2006/05/addressing/wsdl
      msc : http://schemas.microsoft.com/ws/2005/12/wsdl/contract
      i0 : http://www.ultipro.com/dataservices/bistream/2
      wsp : http://schemas.xmlsoap.org/ws/2004/09/policy
      wsa10 : http://www.w3.org/2005/08/addressing
      wsam : http://www.w3.org/2007/05/addressing/metadata
      Policy : Policy
      import : import
      types :
      binding : {WSHttpBinding_IBIStreamService, BasicHttpBinding_IBIStreamService}
      service : service

      Delete
  7. Hey, thanks a lot for the post, it is very helpful!!!!
    I have a inquiry, do you know what could be the difference in the response using the same parameters to get the report, you make the reference to the response in:"$response.Envelope.body.StreamReportResponse.ReportStream", however, the response I get is the following:

    $response.definitions
    name : BIStreamService
    targetNamespace : http://tempuri.org/
    wsdl : http://schemas.xmlsoap.org/wsdl/
    xsd : http://www.w3.org/2001/XMLSchema
    soapenc : http://schemas.xmlsoap.org/soap/encoding/
    wsu : http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd
    soap : http://schemas.xmlsoap.org/wsdl/soap/
    soap12 : http://schemas.xmlsoap.org/wsdl/soap12/
    tns : http://tempuri.org/
    wsa : http://schemas.xmlsoap.org/ws/2004/08/addressing
    wsx : http://schemas.xmlsoap.org/ws/2004/09/mex
    wsap : http://schemas.xmlsoap.org/ws/2004/08/addressing/policy
    wsaw : http://www.w3.org/2006/05/addressing/wsdl
    msc : http://schemas.microsoft.com/ws/2005/12/wsdl/contract
    i0 : http://www.ultipro.com/dataservices/bistream/2
    wsp : http://schemas.xmlsoap.org/ws/2004/09/policy
    wsa10 : http://www.w3.org/2005/08/addressing
    wsam : http://www.w3.org/2007/05/addressing/metadata
    Policy : Policy
    import : import
    types :
    binding : {WSHttpBinding_IBIStreamService, BasicHttpBinding_IBIStreamService}
    service : service

    Im using:
    ##########################################
    # RetrieveReport #
    ##########################################
    write-output "Fetching report results output"
    #Set retrieval header and specify stream output
    $retrieveReportURL = $streamingserviceUrl# + '?EmploymentStatus=A'
    $retrieveHeaders = @{ `
    'Content-Type' = 'application/soap+xml; charset=utf-8'; `
    'Accept-Encoding' = 'gzip, deflate'; `
    }

    #Stream output SOAP body
    $body = @"


    http://www.ultipro.com/dataservices/bistream/2/IBIStreamService/RetrieveReport
    $reportKey
    $streamingserviceUrl





    "@

    $response = Invoke-RestMethod -Method POST -Uri $retrieveReportURL -headers $retrieveHeaders -Body $body

    ReplyDelete
  8. Looks like you have an extra "#". You have $retrieveReportURL = $streamingserviceUrl# + '?EmploymentStatus=A'. It should be $retrieveReportURL = $streamingserviceUrl + '?EmploymentStatus=A'. Take a look at the source script again. Hope that helps.

    ReplyDelete
    Replies
    1. I ran the script with/without the additional #, but it's the same result, in the script response, I don't get Envelope as output I get definitions

      Delete
  9. Awesome, Thanks a lot !!

    ReplyDelete