-
Name the file using kebab-case in the cards/ directory (e.g., cards/close_ticket.json). Verify cards/ exists with ls cards/ before creating.
-
Scaffold the root envelope — always this exact shape:
{
"$schema": "https://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.5",
"body": []
}
-
Add a header row using a nested ColumnSet with an Icon + TextBlock pair, matching the pattern in cards/add_ticket.json lines 3–50:
{
"type": "ColumnSet",
"spacing": "None",
"columns": [{
"type": "Column", "width": "stretch",
"items": [{
"type": "ColumnSet", "spacing": "ExtraSmall",
"columns": [
{ "type": "Column", "width": "auto", "verticalContentAlignment": "Center",
"items": [{ "type": "Icon", "name": "<FluentIconName>", "color": "Accent", "size": "Small" }] },
{ "type": "Column", "width": "stretch", "spacing": "ExtraSmall", "verticalContentAlignment": "Center",
"items": [{ "type": "TextBlock", "text": "<Title>", "wrap": true, "weight": "Bolder", "color": "Accent", "size": "Large" }] }
]
}]
}]
}
-
Add Input.ChoiceSet fields inside ColumnSet columns for side-by-side dropdowns. Required fields: id, label, placeholder, choices (array of {title, value}), isRequired: true, errorMessage, value (default), spacing: "None".
-
Add Input.Text fields at the body level for single-line or multiline text. Required fields: id, label, placeholder, errorMessage, spacing: "None". Add "isMultiline": true for multi-line.
-
Add optional toggle sections using Action.ToggleVisibility on a TextRun inside a RichTextBlock. The toggled element must have a matching id and "isVisible": false in the JSON. Include paired chevronDown/chevronUp Icon elements with isVisible: false on the up-chevron.
-
Add the ActionSet inside a right-aligned ColumnSet column:
{
"type": "ActionSet",
"separator": true,
"spacing": "None",
"horizontalAlignment": "Right",
"actions": [
{ "type": "Action.Submit", "title": "Submit", "style": "positive",
"data": { "msteams": { "type": "<cardNameSubmit>" } } },
{ "type": "Action.Submit", "title": "Cancel", "style": "destructive",
"data": { "msteams": { "type": "<cardNameCancel>" } } }
]
}
Use camelCase for msteams.type values (e.g., addTicketSubmit, addTicketCancel).
-
Add bot handler branches in server/bot/botActivityHandler.js inside the this.onMessage handler. Check the msteams.type field in context.activity.value and read field values from the same object. Use context.updateActivity() to replace the card on success (see lines 201–206 and 227–231 for the exact pattern).
-
Add the trigger command — add an else if (lcText === '<trigger phrase>') branch that reads the card via fs.readFileSync, sends it, then calls this.updateActionSubmitData(element, sentActivity) recursively to inject activityId before updating the activity. Follow lines 248–263 exactly.
Verify the new card file parses cleanly: node -e "JSON.parse(require('fs').readFileSync('cards/add_ticket.json','utf8'))" (substitute add_ticket.json with your card's filename) — must exit 0.