Executing Actions
Adaptive Cards support interactive elements through actionsβbuttons, links, and input submission triggers that respond to user interaction. You can use these to collect form input, trigger workflows, show task modules, open URLs, and more.
Action Typesβ
The Teams SDK supports several action types for different interaction patterns:
| Action Type | Purpose | Description |
|---|---|---|
Action.Execute | Serverβside processing | Send data to your bot for processing. Best for forms & multiβstep workflows. |
Action.Submit | Simple data submission | Legacy action type. Prefer Execute for new projects. |
Action.OpenUrl | External navigation | Open a URL in the user's browser. |
Action.ShowCard | Progressive disclosure | Display a nested card when clicked. |
Action.ToggleVisibility | UI state management | Show/hide card elements dynamically. |
For complete reference, see the official documentation.
Creating Actions with the SDKβ
Single Actionsβ
The SDK provides builder helpers that abstract the underlying JSON. For example:
from microsoft_teams.cards.core import ExecuteAction
# ...
action = ExecuteAction(title="Submit Feedback")
.with_data({"action": "submit_feedback"})
.with_associated_inputs("auto")
Action Setsβ
Group actions together using ActionSet:
from microsoft_teams.cards.core import ActionSet, ExecuteAction, OpenUrlAction
# ...
action_set = ActionSet(
actions=[
ExecuteAction(title="Submit Feedback")
.with_data({"action": "submit_feedback"}),
OpenUrlAction(url="https://adaptivecards.microsoft.com").with_title("Learn More")
]
),
Raw JSON Alternativeβ
Just like when building cards, if you prefer to work with raw JSON, you can do just that. You get type safety for free in Python.
json = {
"type": "Action.OpenUrl",
"url": "https://adaptivecards.microsoft.com",
"title": "Learn More",
}
Working with Input Valuesβ
Associating data with the cardsβ
Sometimes you want to send a card and have it be associated with some data. Set the data value to be sent back to the client so you can associate it with a particular entity.
from microsoft_teams.cards import AdaptiveCard, ActionSet, ExecuteAction, OpenUrlAction
from microsoft_teams.cards.core import TextInput, ToggleInput
# ...
profile_card = AdaptiveCard(
schema="http://adaptivecards.io/schemas/adaptive-card.json",
body=[
TextInput(id="name").with_label("Name").with_value("John Doe"),
TextInput(id="email", label="Email", value="john@contoso.com"),
ToggleInput(title="Subscribe to newsletter").with_id("subscribe").with_value("false"),
ActionSet(
actions=[
ExecuteAction(title="Save")
# entity_id will come back after the user submits
.with_data({"action": "save_profile", "entity_id": "12345"}),
]
),
],
)
# Data received in handler:
"""
{
"action": "save_profile",
"entity_id": "12345", # From action data
"name": "John Doe", # From name input
"email": "john@doe.com", # From email input
"subscribe": "true" # From toggle input (as string)
}
"""
Input Validationβ
Input Controls provide ways for you to validate. More details can be found on the Adaptive Cards documentation.
from microsoft_teams.cards import AdaptiveCard, ActionSet, ExecuteAction, NumberInput, TextInput
# ...
def create_profile_card_input_validation():
age_input = NumberInput(id="age").with_label("age").with_is_required(True).with_min(0).with_max(120)
# Can configure custom error messages
name_input = TextInput(id="name").with_label("Name").with_is_required(True).with_error_message("Name is required")
card = AdaptiveCard(
schema="http://adaptivecards.io/schemas/adaptive-card.json",
body=[
age_input,
name_input,
TextInput(id="location").with_label("Location"),
ActionSet(
actions=[
ExecuteAction(title="Save")
# All inputs should be validated
.with_data({"action": "save_profile"})
.with_associated_inputs("auto")
]
),
],
)
return card
Routing & Handlersβ
Using SubmitDataβ
The SDK provides a SubmitData helper that sets the routing key for your action. This is the recommended way to wire up actions to specific handlers:
from microsoft_teams.cards import ExecuteAction, SubmitData
# ...
ExecuteAction(title="Submit Feedback") \
.with_data(SubmitData("submit_feedback")) \
.with_associated_inputs("auto")
# You can also pass extra static data alongside the action name
ExecuteAction(title="Save") \
.with_data(SubmitData("save_profile", {"entity_id": "12345"})) \
.with_associated_inputs("auto")
SubmitData sets a reserved action key in the card's data payload. When the user clicks the button, the SDK router reads this key to dispatch to the matching handler.
Action-Specific Handlersβ
Register handlers for specific actions. When you use SubmitData to set the action name on the card, the SDK routes directly to the matching handler:
from microsoft_teams.apps import App, ActivityContext
from microsoft_teams.api import AdaptiveCardInvokeActivity, AdaptiveCardActionMessageResponse, AdaptiveCardInvokeResponse
# ...
# 'submit_feedback' matches the identifier passed to SubmitData('submit_feedback')
@app.on_card_action_execute("submit_feedback")
async def handle_submit_feedback(ctx: ActivityContext[AdaptiveCardInvokeActivity]) -> AdaptiveCardInvokeResponse:
data = ctx.activity.value.action.data
await ctx.send(f"Feedback received: {data.get('feedback')}")
return AdaptiveCardActionMessageResponse(
status_code=200,
type="application/vnd.microsoft.activity.message",
value="Action processed successfully",
)
@app.on_card_action_execute("save_profile")
async def handle_save_profile(ctx: ActivityContext[AdaptiveCardInvokeActivity]) -> AdaptiveCardInvokeResponse:
data = ctx.activity.value.action.data
await ctx.send(f"Profile saved!\nName: {data.get('name')}\nEmail: {data.get('email')}")
return AdaptiveCardActionMessageResponse(
status_code=200,
type="application/vnd.microsoft.activity.message",
value="Action processed successfully",
)
The decorator argument matches the value passed to SubmitData. This is cleaner than a catch-all with a switch statement, and scales better as you add more actions.
Catch-All Handlerβ
If you need to handle all card actions in one place, you can use the catch-all handler:
from microsoft_teams.api import AdaptiveCardInvokeActivity, AdaptiveCardActionErrorResponse, AdaptiveCardActionMessageResponse, HttpError, InnerHttpError, AdaptiveCardInvokeResponse
from microsoft_teams.apps import ActivityContext
# ...
@app.on_card_action
async def handle_card_action(ctx: ActivityContext[AdaptiveCardInvokeActivity]) -> AdaptiveCardInvokeResponse:
data = ctx.activity.value.action.data
if not data.get("action"):
return AdaptiveCardActionErrorResponse(
status_code=400,
type="application/vnd.microsoft.error",
value=HttpError(
code="BadRequest",
message="No action specified",
inner_http_error=InnerHttpError(
status_code=400,
body={"error": "No action specified"},
),
),
)
print("Received action data:", data)
if data["action"] == "submit_feedback":
await ctx.send(f"Feedback received: {data.get('feedback')}")
elif data["action"] == "purchase_item":
await ctx.send(f"Purchase request received for game: {data.get('choiceGameSingle')}")
elif data["action"] == "save_profile":
await ctx.send(
f"Profile saved!\nName: {data.get('name')}\nEmail: {data.get('email')}\nSubscribed: {data.get('subscribe')}"
)
else:
return AdaptiveCardActionErrorResponse(
status_code=400,
type="application/vnd.microsoft.error",
value=HttpError(
code="BadRequest",
message="Unknown action",
inner_http_error=InnerHttpError(
status_code=400,
body={"error": "Unknown action"},
),
),
)
return AdaptiveCardActionMessageResponse(
status_code=200,
type="application/vnd.microsoft.activity.message",
value="Action processed successfully",
)
The data values are accessible as a dictionary and can be accessed using .get() method for safe access.