Adaptive Card Input Validation for Self-Service Teams Forms
Bad input breaks self-service ops bots. Adaptive Cards have built-in client-side validation for inputs — here is how to use it well and still validate on the server.
- #microsoft-teams
- #adaptive-cards
- #validation
- #self-service
- #json
I shipped a self-service “request a sandbox environment” card to a Teams channel and within an hour someone submitted a request with an empty project name, a TTL of forever, and a region of mars-east-1. The card collected garbage and my flow dutifully tried to provision it. The lesson was immediate: an input form is only as good as its validation, and Adaptive Cards give you real client-side validation if you bother to wire it up.
This guide covers the validation attributes Adaptive Cards support, how Action.Submit enforces them, and — the part people skip — why you still validate everything again on the server. I built the first draft of these cards with an AI assistant, which is great for generating the repetitive Input.* JSON. But the model treats validation as decoration; it will mark a field “required” in the UI and never check it server-side. Treat AI like a fast junior engineer: it scaffolds, you review, and the security-relevant checks are yours to own before anything hits a tenant.
Required fields and the validation contract
Every Input.* element supports isRequired and errorMessage. When an Action.Submit (or Action.Execute) has "associatedInputs": "auto", the Teams client validates all inputs in scope before sending. If a required field is empty, the client blocks submission and shows your error message inline.
{
"type": "Input.Text",
"id": "projectName",
"label": "Project name",
"isRequired": true,
"errorMessage": "Project name is required",
"maxLength": 40,
"regex": "^[a-z][a-z0-9-]{2,39}$"
}
The regex attribute is the one people miss. It runs client-side and rejects anything that does not match before the submit fires. Here I am enforcing lowercase-kebab project names, which is exactly the constraint my provisioning flow needs. Pair regex with a clear errorMessage so the user knows what shape you want.
Constrained choices instead of free text
Free text is where bad data comes from. Wherever the valid set is small and known, use Input.ChoiceSet so the user physically cannot type mars-east-1.
{
"type": "Input.ChoiceSet",
"id": "region",
"label": "Region",
"style": "compact",
"isRequired": true,
"errorMessage": "Pick a region",
"value": "us-east-1",
"choices": [
{ "title": "US East (us-east-1)", "value": "us-east-1" },
{ "title": "EU West (eu-west-1)", "value": "eu-west-1" }
]
}
For numeric bounds, Input.Number takes min and max, and the client enforces them:
{
"type": "Input.Number",
"id": "ttlHours",
"label": "Time to live (hours)",
"min": 1,
"max": 72,
"value": 8,
"isRequired": true,
"errorMessage": "TTL must be between 1 and 72 hours"
}
That single block would have caught my forever TTL. The client will not let the value fall outside one through seventy-two.
Pro Tip: Input.Text has a style of "Email", "Tel", "Url", and "Password". These change the mobile keyboard and apply light format hints, but they are not strong validation. Always back an email field with a regex and a server check — the style is a UX nicety, not a guarantee.
Wiring the submit action to enforce validation
Validation only runs if the action opts into it. The key is associatedInputs:
{
"type": "ActionSet",
"actions": [
{
"type": "Action.Submit",
"title": "Request sandbox",
"associatedInputs": "auto",
"data": { "action": "provisionSandbox" }
},
{
"type": "Action.Submit",
"title": "Cancel",
"associatedInputs": "none",
"data": { "action": "cancel" }
}
]
}
Notice the Cancel action uses "associatedInputs": "none". You do not want validation to block a cancel just because a required field is empty. This split is a common bug in AI-generated cards: the model applies auto to every action, and suddenly users cannot back out of a half-filled form.
Why the server must validate again
Client-side validation in Adaptive Cards is a UX layer. It improves the experience but it is trivially bypassable — the payload is just JSON sent to your bot or flow endpoint, and nothing stops a crafted request from skipping the client entirely. So your handler validates everything a second time, server-side, and rejects what does not pass.
function validateSandboxRequest(data) {
const errors = [];
if (!/^[a-z][a-z0-9-]{2,39}$/.test(data.projectName || "")) {
errors.push("projectName failed format check");
}
const allowedRegions = ["us-east-1", "eu-west-1"];
if (!allowedRegions.includes(data.region)) {
errors.push("region not in allow-list");
}
const ttl = Number(data.ttlHours);
if (!Number.isInteger(ttl) || ttl < 1 || ttl > 72) {
errors.push("ttlHours out of range");
}
return errors;
}
When validation fails server-side, respond by replacing the card with the same form plus an error summary, rather than silently dropping the request. The user re-submits and you have a clean audit trail of what was rejected and why.
Letting AI generate the cards without trusting it blindly
The repetitive Input.* JSON is exactly the kind of thing an AI is fast at. I describe the form in plain language and let ChatGPT or Claude produce the first card, then I add the regex, min/max, and associatedInputs it tends to leave loose. The starter prompts in my prompts library are tuned for exactly this, and I keep iterating in the prompt workspace until the card validates the way the backend expects. The non-negotiable rule: the server-side validator is hand-written and reviewed, never copy-pasted from the model into production.
Conclusion
Adaptive Cards give you real client-side validation — isRequired, regex, min/max, constrained ChoiceSet, and associatedInputs on the submit action. Use all of it for a clean user experience, but never confuse it with security. Validate again on the server, every field, every time, and let a human review the AI-drafted JSON before it goes near a tenant. More Teams self-service patterns live in the Microsoft Teams category.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.