Post Actionable Cards to MS Teams Using Incoming Webhooks, APEX & ORDS

Search for a command to run...

No comments yet. Be the first to comment.
Introduction One of the marquee features of APEX 26.1 was AI Interactive Reports. When I started testing this new feature, I was interested to see what was going on behind the scenes. In this post, I

Introduction One of the marquee features of APEX 26.1 is APEX AI Agents. APEX AI Agents allow you to use an LLM and your own PL/SQL and JS tools to perform actions on your data instead of just chattin

Introduction In my previous post, I looked at logging APEX AI Agent requests and responses with Request and Response handlers. Logging is the first step because it shows you what is actually moving th

Introduction Logging is one of the first things you need when building serious AI Agents. Without it, debugging quickly becomes guesswork. You can see the final answer, but not always why the model ch

Context management patterns from building agents in Oracle APEX

In my post Use MS Teams to Chat with the World via Oracle REST Data Services, I discussed posting messages from MS Teams to an ORDS Web Service and returning a response to the MS Teams chat (essentially a lightweight bot). This post will focus on sending actionable cards from the Oracle Database to MS Teams using a Teams Incoming Webhook. User input to these cards can then be posted back to an ORDS REST service in real-time.
Example use cases include:
The demo code for this post is available in this GitHub Repository.
This post will follow the example of an Alert Notification to let APEX Developers know about slow-running APEX pages. The process works like this:
apex_workspace_activity_log tablePOSTs it to a URL generated when we create the Teams Incoming WebhookSubmit button on the Message CardSubmit button on the MS Teams card initiates a POST request to an ORDS REST service which captures the users' commentHere is a screenshot of an example card sent via the above process.
The Teams user receives the card, enters their comment, and clicks the 'Submit' button. The user's comments are then posted to an ORDS REST service, where they can be processed.
Let us start by creating the MS Teams Incoming Webhook. Creating the Incoming Webhook will give us the URL we need to POST messages to a specific Teams Channel. This post assumes you have already created a Team and are the owner.








Before getting into the code, I recommend testing the webhook using Postman (or a similar tool). This testing will allow you to iterate quickly through different card formats and test responses sent to the ORDS Web Service. Setup Postman as follows, the URL is the Incoming Webhook URL generated above.
You should expect a 200 OK response with an empty response body. You can use the sample JSON in the 'Generating the JSON' section below to test from Postman.
This is also a good time to design and test the card format you want to send to the Teams Channel. I am creating a card of the type 'MessageCard', also known as an 'Office 365 Connector card' in the demo code. Although this is a 'Legacy' card type, it is currently the only card type that supports posting back to a custom URL (e.g., ORDS REST Endpoint) using the HttpPOST method. You can use this web site to help design 'MessageCards'.
If you do not need to post a reply to an ORDS web service, then I suggest you use the Adaptive Card type. Adaptive Cards seem to be the future direction for Microsoft.
See this link for a description of the different card types supported by MS Teams.
Now that we have our Inbound Webhook created and tested, we need to build code to send the card to the Teams Channel. In this section, I will highlight some critical aspects of the demo code. See the Github Repository for the complete demo code for this post.
In my example, I have created an APEX Automation that periodically calls a procedure to check the apex_workspace_activity_log table for any pages that have taken two or more seconds to render. Here are two screenshots that show the Automation:
Please read my post for more on APEX Automations.
The Automation calls a procedure automation_apex_alert, passing in the date and time the Automation last ran. This procedure first checks if there were any slow-running page views since the Automation last ran. If there are, it proceeds to create a JSON payload and send it to the Teams Channel.
If there are slow-running pages, then the procedure automation_apex_alert builds a JSON document conforming to the 'MessageCard' or 'Office 365 Connector card' format. I won't go through all of the logic here. I will focus on the code that generates the JSON to render the Comments field on the Teams Card, and POSTs a response to our ORDS Web Service when the user clicks the Submit button on the Teams Card.
apex_json.open_array('potentialAction');
apex_json.open_object; -- {
apex_json.write('@type', 'ActionCard');
apex_json.write('name', 'Add Comments');
apex_json.open_array('inputs');
apex_json.open_object; -- {
apex_json.write('@type', 'TextInput');
apex_json.write('id', 'comment');
apex_json.write('title', 'Comment');
apex_json.write('isMultiline', TRUE);
apex_json.close_object; -- }
apex_json.close_array; -- } inputs
apex_json.open_array('actions');
apex_json.open_object; -- {
apex_json.write('@type', 'HttpPOST');
apex_json.write('name', 'Submit');
-- Send a response URL on the card. This ORDS endpoint will be called when the user clicks the Acknowledge button on the card.
apex_json.write('target', 'https://www.example.com/ords/demo/teams/incoming_webhook_response?response_token='||GC_CARD_RESPONSE_TOKEN);
-- You can put anything you want in the body; this will be passed back as a JSON Payload when the above URL is called.
apex_json.write ('body', '{"comment":"{{comment.value}}"}');
apex_json.close_object; -- }
apex_json.close_array; -- ] actions
apex_json.close_object; -- }
apex_json.close_array; -- ] potentialAction
actions array, the code that starts apex_json.write('@type', 'HttpPOST'); generates JSON which indicates to Teams that we want to render a button called 'Submit' that when clicked, should send a HttpPOST request to the URL `https://www.example.com/ords/demo/teams/incoming_webhook_response'?response_token='||GC_CARD_RESPONSE_TOKEN. The constant GC_CARD_RESPONSE_TOKEN is a token used by the ORDS service during the response to Authenticate the request (see below for details)apex_json.write ('body', '{"comment":"{{comment.value}}"}'); generates JSON which tells Teams what should be sent in the body of the HttpPOST request. The value {{comment.value}} is replaced with the value entered by the Teams user in the Comment field of the cardHere is an example of a JSON payload sent to Teams by my sample code:
{
"@type": "MessageCard",
"@context": "https:\/\/schema.org\/extensions",
"title": "APEX Slow Page Alert [PROD]",
"summary": "APEX Slow Page Alert",
"themeColor": "0078D7",
"sections": [{
"startGroup": true,
"activityTitle": "Application ID: [4000]",
"activitySubtitle": "Page ID: [7021]",
"activityImage": "https:\/\/objectstorage.us-ashburn-1.oraclecloud.com\/n\/nueva\/b\/nueva-public\/o\/Blog%20Public%20Files%2FTeams%20Incoming%20Webhooks%2FAPEX%20Rounded%20Corners.png",
"facts": [{
"name": "Number of Ocurrences",
"value": 3
}, {
"name": "Max. Duration (seconds)",
"value": 2.49
}]
}, {
"startGroup": true,
"activityTitle": "Application ID: [4850]",
"activitySubtitle": "Page ID: [100]",
"activityImage": "https:\/\/objectstorage.us-ashburn-1.oraclecloud.com\/n\/nueva\/b\/nueva-public\/o\/Blog%20Public%20Files%2FTeams%20Incoming%20Webhooks%2FAPEX%20Rounded%20Corners.png",
"facts": [{
"name": "Number of Ocurrences",
"value": 1
}, {
"name": "Max. Duration (seconds)",
"value": 2.01
}]
}, {
"startGroup": true,
"activitySubtitle": "Issues detected between 13-JUL-2022 05:38:42 pm and 13-JUL-2022 08:44:43 pm UTC"
}],
"potentialAction": [{
"@type": "ActionCard",
"name": "Add Comments",
"inputs": [{
"@type": "TextInput",
"id": "comment",
"title": "Comment",
"isMultiline": true
}],
"actions": [{
"@type": "HttpPOST",
"name": "Submit",
"target": "https:\/\/www.example.com\/ords\/demo\/teams\/incoming_webhook_response?response_token=S3TwFW283839qQCRAwwt18=",
"body": "{\"comment\":\"{{comment.value}}\"}"
}]
}]
}
Once the JSON has been built, it is just a matter of POSTing it to the URL Teams generated when we created the Incoming Webhook. This can be done with apex_web_service.make_rest_request:
l_teams_response := apex_web_service.make_rest_request
(p_url => GC_TEAMS_CHANNEL_URL,
p_http_method => 'POST',
p_body => l_teams_json);
The URL for the Incoming Webhook will look something like this https://cloudnueva.webhook.office.com/webhookb2/fe1f1b7e-123r-4d65-8c48-b3d5b732f987@990876cd-77d7-4fab-8cd2-57020376ed11/IncomingWebhook/34d33b0195204a0c9aba6ae8b240ef4f/111a2f8-d26c-4fc1-b1c0-ffbaf2af1337
If everything goes well, you should expect a 200 OK response from Teams.
In the demo code, you will see an ORDS handler called incoming_webhook_response. This handler receives a request from Teams when a user enters a comment and clicks the Submit button on the card. The handler calls the procedure capture_card_response, which does the following:
400 un-authorized responseI am not doing anything of note with the response payload in my demo code, but you could.
Instead of using a constant token value, I recommend generating a random string for each card generated, storing it in a table then checking for the existence of that string in your ORDS service when a response is received. Remember to invalidate or delete the token once it has been used. See the Microsoft documentation on Security Requirements Action Message Cards for more details.
Being able to send messages to MS Teams opens up a whole new way of communicating with your employees (and potentially your customers). Being able to take action on Teams user responses to these messages opens the door to frictionless automation.
But, as the saying goes...
With great power comes great responsibility. You should only use this approach for important and urgent communications and avoid spamming people in Teams with unimportant system-generated messages.