Thank You Nivedipa-MSFT, Teddie-D and Jeremy Saps for your quick responses,
I was able to resolve the issue. The problem was not with the Adaptive Card itself, but with how the invoke response was being returned in the M365 Agent SDK for Python. Task Modules in Teams require a very specific response flow, and the SDK does not automatically handle it unless you wire the pipeline correctly.
Below is the working pattern.
1. Main request handler (teams_main.py)
The key point:
-
task/fetchmust return a JSON response containing the dialog payload. -
task/submitshould return a202(no UI update).
if activity.type == "invoke" or activity.delivery_mode == DeliveryModes.expect_replies:
# Submit dialog → no content expected by Teams
if not response.body:
return Response(status_code=202)
# Fetch dialog → return your Task Module JSON payload
return JSONResponse(status_code=response.status, content=response.body)
return Response(status_code=202)
This ensures Teams gets the correct response format for each stage of the Task Module flow.
2. Invoke routing (teams_event.py)
@agent_app.activity("invoke")
async def on_invoke(context: TurnContext, state: TurnState):
return await event_handler.handle_invoke_event(context, state)
3. Invoke event handler (teams_event_handler.py)
async def handle_invoke_event(self, context: TurnContext, state: TurnState):
activity = context.activity
name = getattr(activity, "name", None)
if name == "task/fetch":
return await self._handle_task_fetch(context)
if name == "task/submit":
return await self._handle_task_submit(context)
return InvokeResponse(status=200, body={})
4. Handling task/fetch
This builds the Adaptive Card, wraps it in the required Task Module structure, and returns it as an invokeResponse.
async def _handle_task_fetch(self, context: TurnContext):
adaptive_card = self._build_sample_form_card()
invoke_activity = self._build_task_module_response(
title="Form",
height="medium",
width="medium",
card=adaptive_card,
)
await self.adapter.send_activities(context, [invoke_activity])
5. Adaptive Card
def _build_sample_form_card(self):
return {
"type": "AdaptiveCard",
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.4",
"body": [
{"type": "TextBlock", "text": "Sample task module card", "weight": "Bolder", "size": "Medium"},
{"type": "TextBlock", "text": "This is a sample dialog.", "wrap": True},
{"type": "Input.Text", "id": "notes", "isMultiline": True, "placeholder": "Add notes here"},
],
"actions": [
{
"type": "Action.Submit",
"title": "Submit",
"data": {
"msteams": {"type": "task/submit"},
"action": "sample_task_submit"
}
}
]
}
Note the required:
"msteams": { "type": "task/submit" }
Teams uses this to identify a dialog submission.
6. Building the Task Module response
def _build_task_module_response(self, title, height, width, card):
return Activity(
type="invokeResponse",
value={
"status": 200,
"body": {
"task": {
"type": "continue",
"value": {
"title": title,
"height": height,
"width": width,
"card": {
"contentType": "application/vnd.microsoft.card.adaptive",
"content": card
}
}
}
}
}
)