Missing replyToId in Activity Object – Clarification Required

Shravan Das | CM.com 0 Reputation points
2025-07-17T07:30:07.3066667+00:00

Hi Team,

We are currently leveraging the Bot Framework and have observed an issue related to how replies to posts are handled. Specifically, when someone replies to an existing post (as shown in the attached screenshot), the request body — or more precisely, the Activity object — is missing the replyToId property.
User's image

As background, the replyToId field in the Activity object is critical for our use case. We need to differentiate between:

  • A new post (initial message), and

A reply to an existing post.

This distinction helps our bot determine whether to respond (in the case of a new post) or ignore the message (in the case of a reply not directed at the bot).

However, what we’re seeing is that replies are sometimes treated as entirely new messages — without a replyToId — which defeats the purpose of the replyToId property. This behavior seems inconsistent and makes it difficult for us to handle conversation context appropriately in our middleware logic.

Could someone help clarify:

Is this expected behavior in the Bot Framework?

  • Are there specific scenarios where replyToId is intentionally omitted?
  • How are replies distinguished from new messages if replyToId isn’t present?

Appreciate any insights or documentation references you can share.

The request body(Activity Object)

{
    "text": "Testing Parent",
    "textFormat": "plain",
    "attachments": [
        {
            "contentType": "text/html",
            "content": "<p>Testing Parent</p>"
        }
    ],
    "type": "message",
    "timestamp": "2025-07-15T12:08:49.3024423Z",
    "localTimestamp": "2025-07-15T17:38:49.3024423+05:30",
    "id": "x",
    "channelId": "msteams",
    "serviceUrl": "x",
    "from": {
        "id": "x",
        "name": "Shravan Das | CM.com",
        "aadObjectId": "x"
    },
    "conversation": {
        "isGroup": true,
        "conversationType": "channel",
        "tenantId": "x",
        "id": "x"
    },
    "recipient": {
        "id": "28:2c159495-51f6-478b-b645-f2f36cc85fcc",
        "name": "Halo Test"
    },
    "entities": [
        {
            "locale": "en-US",
            "country": "US",
            "platform": "Windows",
            "timezone": "Asia/Calcutta",
            "type": "clientInfo"
        }
    ],
    "channelData": {
        "teamsChannelId": "x",
        "teamsTeamId": "x",
        "channel": {
            "id": "x"
        },
        "team": {
            "id": "x"
        },
        "tenant": {
            "id": "x"
        }
    },
    "locale": "en-US",
    "localTimezone": "Asia/Calcutta"
}

Azure AI Bot Service
Azure AI Bot Service
An Azure service that provides an integrated environment for bot development.
0 comments No comments
{count} votes

1 answer

Sort by: Most helpful
  1. Amira Bedhiafi 39,106 Reputation points Volunteer Moderator
    2025-09-05T21:26:16.1133333+00:00

    Hello Sharavan !

    Thank you for posting on Microsoft Q&A.

    In Microsoft Teams the replyToId field is optional in the Bot Framework schema and Teams often doesn’t set it on incoming activities even for thread replies. In channel conversations, Teams encodes the thread (root message) id inside activity.conversation.id, and that’s the reliable way to tell if it is a new top-level post or a reply.

    You can use the thread id embedded in conversation.id. If the current activity id equals the embedded root id so it’s the root post, otherwise it’s a reply.

    using System.Text.RegularExpressions;
    using Microsoft.Bot.Builder;
    using Microsoft.Bot.Schema;
    static string GetRootMessageId(Activity a)
    {
        var convId = a.Conversation?.Id ?? "";
        var m = Regex.Match(convId, @";messageid=([^;]+)", RegexOptions.IgnoreCase);
        return m.Success ? m.Groups[1].Value : null;
    }
    var activity = turnContext.Activity;
    bool isChannel = activity.Conversation?.ConversationType == "channel";
    var rootId = GetRootMessageId(activity);
    bool isThread = isChannel && rootId != null;
    bool isRootPost = isThread && activity.Id == rootId;
    bool isReply = isThread && activity.Id != rootId;
    bool botMentioned = activity.GetMentions()?
        .Any(m => m.Mentioned?.Id == activity.Recipient?.Id) == true;
    if (isRootPost) {
        // respond
    } else if (isReply && !botMentioned) {
        // ignore
    }
    using System.Text.RegularExpressions;
    using Microsoft.Bot.Builder;
    using Microsoft.Bot.Schema;
    static string GetRootMessageId(Activity a)
    {
        var convId = a.Conversation?.Id ?? "";
        var m = Regex.Match(convId, @";messageid=([^;]+)", RegexOptions.IgnoreCase);
        return m.Success ? m.Groups[1].Value : null;
    }
    var activity = turnContext.Activity;
    bool isChannel = activity.Conversation?.ConversationType == "channel";
    var rootId = GetRootMessageId(activity);
    bool isThread = isChannel && rootId != null;
    bool isRootPost = isThread && activity.Id == rootId;
    bool isReply = isThread && activity.Id != rootId;
    bool botMentioned = activity.GetMentions()?
        .Any(m => m.Mentioned?.Id == activity.Recipient?.Id) == true;
    if (isRootPost) {
        // respond
    } else if (isReply && !botMentioned) {
        // ignore
    }
    == activity.Recipient?.Id) == true;
    if (isRootPost) {
        // respond
    } else if (isReply && !botMentioned) {
        // ignore
    }
    

    So what you are seeing is a normal behavior because replyToId is not guaranteed by channels sinceTeams commonly omits it.

    It is always omitted for new posts frequently for replies in Teams channel threads also for 1:1/group chats (no threading).

    To distinguish replies if replyToId isn’t present, you can parse the root message id from conversation.id and compare to activity.id as shown above.


Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.