Skip to main content

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 TypePurposeDescription
Action.ExecuteServer‑side processingSend data to your bot for processing. Best for forms & multi‑step workflows.
Action.SubmitSimple data submissionLegacy action type. Prefer Execute for new projects.
Action.OpenUrlExternal navigationOpen a URL in the user's browser.
Action.ShowCardProgressive disclosureDisplay a nested card when clicked.
Action.ToggleVisibilityUI state managementShow/hide card elements dynamically.
info

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",
)
note

The data values are accessible as a dictionary and can be accessed using .get() method for safe access.

An Input.ChoiceSet can fetch its choices dynamically as the user types, instead of listing them statically. Leave choices as an empty array and add a choices.data Data.Query that names a dataset. As the user types, Teams sends your bot an application/search invoke, and you return the matching results.

from microsoft_teams.cards import AdaptiveCard

card = AdaptiveCard.model_validate(
{
"type": "AdaptiveCard",
"version": "1.5",
"body": [
{
"type": "Input.ChoiceSet",
"id": "game",
"label": "Game",
"placeholder": "Search for a game",
"style": "filtered",
"choices": [],
"choices.data": {"type": "Data.Query", "dataset": "nintendoGames"},
}
],
"actions": [
{"type": "Action.Execute", "title": "Submit", "data": {"action": "submit_game"}}
],
}
)

Register a handler for the application/search invoke to return results as { title, value } pairs, where title is the display text and value is what gets submitted when the choice is selected:

from microsoft_teams.api import SearchInvokeActivity
from microsoft_teams.api.models.search import (
SearchInvokeResponseValue,
SearchInvokeResult,
SearchResponse,
)
from microsoft_teams.apps import ActivityContext

GAMES = ["Super Mario Odyssey", "Metroid Dread", "Splatoon 3"]


@app.on_card_search
async def handle_search(ctx: ActivityContext[SearchInvokeActivity]) -> SearchResponse:
query = (ctx.activity.value.query_text or "").lower()
results = [
SearchInvokeResult(title=g, value=g) for g in GAMES if query in g.lower()
]
return SearchResponse(value=SearchInvokeResponseValue(results=results))

The activity value also carries the query options (skip/top for paging) and the originating dataset, so a single handler can branch on the dataset when several Input.ChoiceSet inputs share it.