LLMs speak markdown. It’s simple and effective. But in a world graduating from timid LLM adoption to agent frenzy, markdown is no longer cutting it. The missing piece is interaction. What follows is a proposal for iMD — Interactive Markdown — which, if standardized, would make the lives of us builders much easier.
Content and interactivity are strangers
“Do you want to proceed?” and its Yes/No buttons are one unit of meaning. So why do we split them into separate data structures?
Today, agents treat content and interactivity as separate concerns:
send_message( body: "Choose a plan:", format: "markdown", actions: [{ type: "actions", elements: [{ type: "button", text: "Basic", action_id: "plan:basic" }] }] )
The actions structure mirrors Slack’s Block Kit. Platform-specific implementation details have leaked into domain logic. When you need Teams, Discord, or a web UI, that coupling forces changes throughout the stack.
Actions are always appended to the end. There’s no way to place a button contextually within the content where it belongs. And when you test or debug, you’re correlating two data structures to reconstruct what the user saw.
The root cause is structural: we’ve separated things that belong together.
The fix is embarrassingly simple.
Interactive Markdown
Extend GitHub Flavored Markdown with interactive elements using familiar HTML-like syntax:
Choose a plan: <button text="Basic" action_id="plan:basic"> <button text="Pro" action_id="plan:pro" style="primary">
One format. One data structure. One thing to log. The agent produces iMD; platform-specific converters handle the rest.
Design principles
How it works
┌─────────────────────────────────────────┐
│ Agent / Backend │
│ │
│ “Choose an option: │
│ <button text="Yes" action_id="y"> │
│ <button text="No" action_id="n">” │
└───────────────────┬─────────────────────┘
│ iMD
┌─────────────┼─────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Slack │ │ Teams │ │ Web │
│ Block Kit│ │ Adaptive │ │ React │
│ │ │ Cards │ │Components│
└──────────┘ └──────────┘ └──────────┘
Adding a platform requires two things: a converter and action handlers. The agent’s code doesn’t change.
Why this matters for agents
Include the syntax in a system prompt and the agent produces interactive messages without knowing anything about Block Kit or Adaptive Cards:
You can create interactive messages using iMD syntax: <button text="Label" action_id="unique-id"> <button text="Label" action_id="id" style="primary"> <button text="Label" action_id="id" style="danger"> <button text="Label" action_id="id" value='{"key":"value"}'>
That’s it. The agent learns one syntax. The value attribute lets agents embed structured JSON context that gets returned on click.
Action routing
For systems using the Model Context Protocol, iMD supports a clean request-response pattern: the agent requests a token, embeds it in a button, and the MCP server handles the callback.
Agent MCP Server Platform
│ │ │
│── get_action_token() ──>│ │
│<── { token: "abc123" } ──│ │
│ │ │
│── send_message() ──────────────────────────────> │
│ "<button action_id='confirm:abc123'>" │
│ │ │
│ │<── button_clicked ──────│
│ │ action_id: abc123 │
The button element
| Attribute | Required | Description |
|---|---|---|
text | Yes | Button display label |
action_id | Yes | Unique identifier for routing click events |
style | No | "primary" or "danger" |
value | No | JSON payload returned on click |
Tags inside fenced code blocks are not parsed. Use single quotes for value to avoid escaping JSON.
| Platform | Output | Limits |
|---|---|---|
| Slack | Block Kit actions | 5 per group, 25 total |
| Teams | Adaptive Card actions | 6 per card |
| Web | React components | None |
| Fallback to links | N/A |
What comes after buttons?
Agents frequently need structured data — not just “yes or no” but “which region, what amount, starting when.” Today most agents handle this through conversational back-and-forth. It works, but it’s slow and error-prone.
Configure your deployment: <select action_id="region" placeholder="Choose region"> <option value="us-east-1">US East</option> <option value="eu-west-1">EU Ireland</option> </select> <input action_id="instances" placeholder="Count">
The hard part isn’t imagining useful elements — it’s defining them at the right level of abstraction. Too specific and you’re back to platform coupling. Too generic and converters can’t produce good results.
There’s no guarantee that sweet spot exists for every element, and finding it will take real implementation experience — not just spec writing.
Considerations
Sanitize text and value attributes for target-platform injection. Action IDs alone should never authorize — verify the clicking user and consider single-use tokens for sensitive operations. Parsers should enforce limits on attribute lengths and nesting depth.
Closing thoughts
The insight behind iMD is small but consequential: a message and its buttons are one thing, not two. Once you treat them that way, a lot of accidental complexity disappears. Agents get simpler. Multi-platform support gets cheaper. Testing gets easier.
iMD is still experimental. If you’re building agent systems that work across messaging platforms, I’d love to hear your thoughts.