Following on from Dirks post [âś… Solution] Sending data from Tape to an external app via webhooks? - #8 by dirk_s and this thread Discord integration as team communication.
The use case Dirk mentions seemed a good and interesting one so I thought I would take the concept (hopefully he doesn’t mind) and build it out. It covers a number of different areas that can be taken and used for other things. As an example, I will cover:
- Configuring Tape workflows to report errors to a verified URL
- In Tape convert the webhook payload into an HTML table
- Display the HTML table reliably on a Tape record
- Use a record calculation field to build a record name
- Use an
http.post
script to send relevant formatted data from Tape to an external Webhook
What are we going to build
We want Tape to report automation errors and have notifications of those errors delivered to an external messaging service.
Now for various reasons, I don’t want to use Teams for this, so I will use Slack. I don’t use it and I don’t have any clients that do either so it’s nice to use something different.
Tape has a built-in system to send automation errors to a webhook however we can’t use that to send the information directly to Slack for one thing the payload will not be formatted the way Slack wants it. So we need something to sit in between and we will just use a Tape app.
Initial App setup
We don’t really know yet what fields we want so we will just go with the default. What we do need is an Automation with a webhook trigger however again we don’t know yet what we are going to do with it fully so just create it and leave it as default.
What this gives us is the webhook URL which we need in the next stage.
The error report
We now have what we need to set up the error reporting to do this we go into Automations
and then Run history
in the top right there is the ...
button. Then click on the Webhook notification option:
When this comes up you can paste in the Webhook URL generated in the first step:
At this point, it will say
Inactive
as it needs to be verified.
Verification
Go back to the automation you created to receive the notifications and if the payload
window is still empty hit the refresh button. you should then have something that looks like:
Which gives us the information we need to send back to Tape to verify.
I find it easiest to do this in Visual Studio Code but use whatever tools you are happy with to edit and then run a cURL:
curl -X POST https://api.tapeapp.com/v1/hook/{{Replace with the hook_id}}/verify/validate \
-u {{replace with your Tape API key}} \
-H "Content-Type: application/json" \
--data '{
"code": "{{replace with the payload/code}}"
}'
you should then get a response that looks something like:
{
"hook_id": 557,
"status": "active",
"type": "run.failed",
"url": "https://tapeapp.com/api/catch/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ3b3JrZmxvd0RlZklk.ZjHEjF-8u0Uiz0tJEXxwRw7suDw2s3u8kGjwZRB0k0s"
}
Now your Webhook Notification
should be marked as active and it is ready to send an error to your error log app.
What information do we get
It is time to see what information we get from an error so we can make some decisions as to what we send to Slack etc.
You need to make an automation error so we get a notification sent once you have done this go back to your Webhook trigger automation the payload should now look like this:
We now have the information we need to build out the App.
The Notification App build
If you change the payload view option from Variables
to Payload
you get the following:
{
"hook_id": 479,
"type": "run.failed",
"app_id": 37746,
"organization_id": 1484,
"organization_name": "Melotte Consulting",
"organization_slug": "jmc",
"workflow_def_id": 128437,
"workflow_def_name": "Send new record details to Slack",
"workflow_def_broken": false,
"workflow_def_url": "https://tapeapp.com/jmc/(focus//root-modal:workflow-center/edit-workflow/128437)",
"workflow_run_id": 6669588,
"error_message": "Invalid URL",
"triggered_on_record_id": 60067223,
"triggered_on_record_url": "https://tapeapp.com/jmc/(focus//main-modal:record/60067223)"
}
Wouldn’t it be nice if rather than creating a field for each item on your record you could just display all the information in one formatted table like:
and that is what we are going to do.
Building the table
To do this we need two fields on our record:
- The message =
Calculation
- payload =
Multiple line text
- Always hidden
Until the Tape team gives us some sort of variable field or multiline plain text field (which i am sure they will as they are amazing) we have some hoops to jump through but it is still not too bad.
The Calculation field
what you need in the calculation field is:
const d = Buffer.from(@payload, 'base64').toString('utf-8');
`${d}`
If you have named the fields differently obviously replace
@payload
with whatever you have called your field.
I will explain the whole
Buffer.from
thing later but this is one of the hoops we have to jump through.
The Automation
If we move back to the automation we can build the table:
We use a script block (mine doesn’t normally look this neat, I have added lots of comments to try and explain what is going on):
let payload = webhook_payload_parsed;
/**
* Generates an HTML table from a JSON object.
* @param {Object} data - The JSON object.
* @returns {string} - The HTML table.
*/
function generateHTMLFromJSON(data) {
let html = '<table style="border-collapse: collapse; width: 100%;">';
// Loop through each key in the data object
for (let key in data) {
html += '<tr>';
// Add the table header
html += `<th style="border: 1px solid #1B98A6; text-align: left; padding: 8px; background-color: #f2f2f2; color: #1E4359">${key}</th>`;
// Check if the value is an object
if (typeof data[key] === 'object') {
// Recursively call the function to generate HTML for nested objects
html += '<td style="border: 1px solid #1B98A6; text-align: left; padding: 8px;">' + generateHTMLFromJSON(data[key]) + '</td>';
} else {
// Add the table data
html += `<td style="border: 1px solid #1B98A6; text-align: left; padding: 8px;">${data[key]}</td>`;
}
html += '</tr>';
}
html += '</table>';
return html;
}
const htmlString = generateHTMLFromJSON(payload);
// console.warn('____');
// console.log('html', htmlString);
var_html = htmlString
We have our table now we need a record to display it so we use a Create a record
block and add our variable html
to it:
- Create a record
- Set to
Calculation
- We
base64
encode our HTML to protect it
This is the other half of our Buffer.from
when you put data into a Multiple line text
field it messes with it adding additional tags and we don’t want that. You can use stripTags
, Loadash
or plain Regex to try and tidy it but the easiest and most reliable way I have found (although possibly not the most resource-friendly) of dealing with this is to encode the data and then decode it which is what we have done and we now have a nice formatted table in our record.
One of the advantages of this method for things like notifications is that if the payload changes say for a different type of notification it doesn’t matter it will just generate a table with the new data regardless of the Key names.
The rest of the app
What other fields you need are going to depend on what information you want to filter your errors on for example if you are building one error log app for multiple Organisations you are probably going to want some way of filtering on the Organisation so pulling out one or all of the organization_id
, organization_name
, organization_slug
is going to be required likewise if it is for a single org you don’t really need any of that.
So your automation could look like:
Which would provide you with a record looking like this:
The Name
Before we move on, the record Name field is a calculation field that pulls data from different areas:
const e = `ERR${String(@Unique ID).padStart(3, '0')}`;
const d = date_fns.format(date_fns_tz.utcToZonedTime(@Created on,'Europe/london'), 'yyMMdd-HHmm');
const s = @slug;
`${e} - ${d} ${s}`
Slack
Next, we need to build our connection to Slack. Head over to Your apps and click the create new app button (top right), you will get a popup:
Select
From scratch
and fill in the form that comes up:click
Create App
Configure the Slack app
Near the top of the page that comes up is:
Select the
Incoming Webhooks
option which will open a new window, you need to turn on the Activate Incoming Webhooks
At which point further options will appear and you need to click on the
Add New Webhook to Workspace
button when you click the button you will be asked for a channel you want to use, select the channel you want to post to and click Allow
You will be taken back to your
Incoming Webhooks
page and the Webhook URL will be available to copy.
Extending the Tape automation
Webhook URL
You need to put your Slack webhook URL somewhere for this example we are just going to drop it in a calculation block:
NOTE: The whole URL must be enclosed if it is not it will not work.
Sending to Slack
Next, we need a script block, which is going to build our message and send it to Slack:
// Make some variables from our Payload
// Build a Title
const title = `Error on record ${jsonata('triggered_on_record_id').evaluate(webhook_payload_parsed)} with automation ${jsonata('workflow_def_name').evaluate(webhook_payload_parsed)}`;
// Get the Organisation name
const orgName = jsonata('organization_name').evaluate(webhook_payload_parsed);
// The Workflow Name
const workflowName = jsonata('workflow_def_name').evaluate(webhook_payload_parsed);
//The Workflow URL
const workflowURL = jsonata('workflow_def_url').evaluate(webhook_payload_parsed);
//The ofending Record ID
const recordID = jsonata('triggered_on_record_id').evaluate(webhook_payload_parsed);
//The offending Record URL
const recordURL = jsonata('triggered_on_record_url').evaluate(webhook_payload_parsed);
// Build a URL for this record
const errorURL = `https://tapeapp.com/jmc/(focus//main-modal:record/${created_automation_error_ID})`;
//Send the message to Slack
const response = await http.post(
var_slackadd,
{
headers: {
"Content-Type": "application/json",
},
data: {
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": `*${title}*\nHi an automation in Tape has errored\nThis error needs to be checked and fixed:`
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": `The error has occurred in the organisation: *${orgName}*`
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": `The workflow is <${workflowURL}|*${workflowName}*>`
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": `The record that generated the error is <${recordURL}|*${recordID}*>`
}
}
{
"type": "divider"
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": `The Error log record is: <${errorURL}|*${created_automation_error_ID}*>`
}
]
}
});
console.info(JSON.stringify(response));
Even though the section types are
mrkdwn
they do not accept normal markdown.
The result
Now if we run the tape automation it sends a message to Slack which looks like this:
A formatted message with all the relevant information including links back to the relevant Tape pages
Recap
So what have we done above:
- Configured Tape workflows to report errors to a verified URL
- In Tape take a webhook payload and convert it into an HTML table
- Displayed the HTML reliably on a Tape record
- Used a record calculation field to build a record name
- Configured Slack to receive messages from Tape
- Used an
http.post
script to send relevant formatted data from Tape to a Webhook
Update
There are a couple of things that should be added:
- If you are using this Error Log within the same reporting Organisation it is very easy to end up in a loop.
- These lines:
"triggered_on_record_id": 60067223,
"triggered_on_record_url": "https://tapeapp.com/jmc/(focus//main-modal:record/60067223)"
do not always exist in the Payload as a workflow is not always related to a record.
Fixes
You can add a filter at the start of your Webhook automation to check that the workflow generating the error is not itself:
you can find the workflow ID of your workflow with:
console.info(current_workflow_id)
If you change any fields you are trying to add record information to use a calculation instead of adding the @
variable:
jsonata('triggered_on_record_ur').evaluate(webhook_payload_parsed)
it doesn’t error when it can’t find that key unlike when you use the @
and Add Value