teams-bot

n8n node for sending messages to Microsoft Teams via Teams Bot API

Package Information

Released: this month
Downloads: 0 weekly / 0 monthly
Latest Version: 1.2.0
Author: John Van Muyen

Documentation

n8n Teams Bot Node

Custom n8n node for sending messages to Microsoft Teams chats and channels via the Teams Bot API.

Features

  • Chat Messages: Send direct messages to users by email address or by a known Teams conversation ID
  • Channel Messages: Send messages to Teams channels by team name and channel name, or by a known channel ID
  • @Mentions: Notify people by writing @[their.email@example.com] in a text message
  • Adaptive Cards: Send rich, interactive Adaptive Cards with buttons, images, and formatted content
  • Auto-population: Automatically loads available users, teams, and channels from your bot's database
  • API Key Authentication: Secure authentication using API keys

Installation

Community Nodes (Recommended)

For self-hosted n8n instances:

  1. Go to Settings → Community nodes.
  2. Click Install a community node.
  3. Enter n8n-nodes-teams-bot and confirm you understand the risks of installing unverified code.
  4. Click Install.

The Teams Bot node is then available in the nodes panel.

Manual Installation

In your n8n installation directory (or ~/.n8n/nodes for a self-hosted instance):

npm install n8n-nodes-teams-bot

Restart n8n afterwards.

Local Development

Important: If you're using WSL, run these commands from WSL bash, not Windows PowerShell/CMD.

npm install
npm run build
npm link

If npm run build fails due to Windows/WSL path issues, use:

bash build.sh

Then in your n8n installation directory:

npm link n8n-nodes-teams-bot

Note: See BUILD_INSTRUCTIONS.md for troubleshooting Windows/WSL build issues.

Railway Deployment

For Railway-hosted n8n instances, you have several options:

Option 1: Install from Git Repository (Recommended for Testing)

  1. Push your node to a Git repository (GitHub, GitLab, etc.)

    git init
    git add .
    git commit -m "Initial commit"
    git remote add origin https://github.com/john-vanmuyen/n8n-teams-node.git
    git push -u origin main
    
  2. In your Railway n8n service, modify the startup command:

    Go to your Railway n8n service → Settings → Deploy

    Option A: Modify the Start Command

    Change the start command from:

    n8n start
    

    To:

    npm install git+https://github.com/john-vanmuyen/n8n-teams-node.git && n8n start
    

    Option B: Use Railway's Build Command

    In Railway service settings, set the Build Command to:

    npm install && npm install git+https://github.com/john-vanmuyen/n8n-teams-node.git
    

    And keep the Start Command as:

    n8n start
    
  3. Important: Make sure your node is built before installing. If installing from Git, you may need to:

    • Build it locally and commit the dist/ folder, OR
    • Add a build step in Railway

Option 2: Publish to npm (Recommended for Production)

  1. Build and publish to npm:

    npm run build
    npm publish
    

    For private npm packages, use:

    npm publish --access restricted
    
  2. Install in Railway n8n:

    Add to your Railway service's environment or startup:

    npm install n8n-nodes-teams-bot
    

Option 3: Use Railway Build Command

If you have access to modify Railway's build process:

  1. Add a build script to your Railway n8n service that:

    • Clones/installs your node
    • Builds it
    • Copies it to the n8n nodes directory
  2. Example Railway build command:

    npm install && \
    npm install git+https://github.com/john-vanmuyen/n8n-teams-node.git && \
    cd node_modules/n8n-nodes-teams-bot && \
    npm install && npm run build
    

Option 4: Custom Dockerfile (Most Control)

Create a custom Dockerfile for your Railway n8n service:

FROM n8nio/n8n:latest

# Install your custom node
RUN npm install -g git+https://github.com/john-vanmuyen/n8n-teams-node.git

# Or if published to npm:
# RUN npm install -g n8n-nodes-teams-bot

Then configure Railway to use this Dockerfile.

Verifying Installation

After deployment, verify the node is loaded:

  1. Check n8n logs for any node loading errors
  2. In n8n UI, the "Teams Bot" node should appear in the node list
  3. Try creating a workflow with the node to confirm it works

Configuration

Credentials

  1. API Key: Your Teams Bot API key (set in your bot's API_KEY environment variable)
  2. Bot API URL: The base URL of your Teams Bot API (e.g., https://your-app.railway.app)

Node Operations

The node exposes a single Message → Send operation. Message Type selects
whether it goes to a chat or a channel.

Send Message to Chat

  • Recipient Type: How to identify the recipient
    • By Email — look the user up by email address
    • By Conversation ID — send straight to a known Teams conversation ID
  • User Email (Recipient Type = By Email): Select from dropdown (auto-populated
    via /api/list-users) or supply an expression
  • Conversation ID (Recipient Type = By Conversation ID): The user's 1-on-1
    conversation ID with the bot, e.g. 19:abc123@thread.v2. Returned in the
    conversation_id field of /api/list-users.
  • Content Type: Choose between "Text" (plain text) or "Adaptive Card" (rich JSON card)
  • Message: The message content to send (when Content Type is "Text")
  • Adaptive Card JSON: The Adaptive Card JSON to send (when Content Type is "Adaptive Card")

Send Message to Channel

  • Channel Target: How to identify the channel
    • By Team & Channel Name — look the channel up by team and channel name
    • By Channel ID — send straight to a known Teams channel ID
  • Team Name (Channel Target = By Team & Channel Name): Select from dropdown
    (auto-populated via /api/list-channels)
  • Channel Name (Channel Target = By Team & Channel Name): Select from dropdown,
    filtered by the selected team
  • Channel ID (Channel Target = By Channel ID): The Teams channel ID, e.g.
    19:xxx@thread.tacv2
  • Content Type: Choose between "Text" (plain text) or "Adaptive Card" (rich JSON card)
  • Message: The message content to send (when Content Type is "Text")
  • Adaptive Card JSON: The Adaptive Card JSON to send (when Content Type is "Adaptive Card")

Targeting by ID skips the bot's name lookup, so it is both faster and immune to
teams or channels being renamed.

Mentions

Write @[someone@example.com] anywhere in a Message to turn it into a real
Teams @mention that notifies that person. It works in both chat and channel
messages, and composes with expressions:

@[jane@corp.ca] ticket {{ $json.id }} was escalated — @[{{ $json.owner }}] fyi

The node extracts the addresses and sends them alongside the unmodified text.
Your bot resolves each one to a Teams user and rewrites the token, so the
recipient sees @Jane Doe, not the bracket syntax.

Requires bot support. A Teams mention has to be built into the outgoing Bot
Framework activity, which only the bot can do. If your bot does not handle the
mentions field, tokens arrive as literal @[...] text. See
API Endpoints Used for the contract.

Resolution order: the bot checks its stored users first, then falls back to
the team roster. So you can mention anyone on the team, not only people who have
already messaged the bot.

Unresolved addresses degrade rather than fail. An address the bot cannot
match is delivered as plain text with no notification, and is listed in
unresolved_mentions on the node's output item — branch on it if a missed ping
matters to your workflow. The message itself always sends.

Some details worth knowing:

  • Only text messages. Mentions inside Adaptive Cards use a different mechanism
    and are not supported — sending both is rejected rather than silently ignored.
  • Only email addresses. @[TODO], @[see notes], and an @[] left by an
    expression that resolved to nothing are all left alone as ordinary text.
  • There is no escape syntax, so a literal @[someone@example.com] cannot be
    written as-is.

Adaptive Cards

Adaptive Cards let you send rich, interactive content to Microsoft Teams. You can create cards with:

  • Formatted text and images
  • Action buttons (open URLs, submit data)
  • Forms and input fields
  • Tables and data displays

Design your cards: Use the Adaptive Cards Designer to visually create cards, then copy the JSON into the node.

Example Adaptive Card:

{
  "type": "AdaptiveCard",
  "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
  "version": "1.4",
  "body": [
    {
      "type": "TextBlock",
      "text": "🚀 Deployment Notification",
      "size": "Large",
      "weight": "Bolder"
    },
    {
      "type": "FactSet",
      "facts": [
        {"title": "Service", "value": "API Gateway"},
        {"title": "Version", "value": "2.1.0"},
        {"title": "Status", "value": "✅ Complete"}
      ]
    }
  ],
  "actions": [
    {
      "type": "Action.OpenUrl",
      "title": "View Logs",
      "url": "https://example.com/logs"
    }
  ]
}

Requirements

Your Teams Bot API must have the following endpoints:

  • POST /api/send-message - Send messages to users
  • POST /api/send-to-channel - Send messages to channels
  • Both send endpoints must accept the optional mentions array for @mention support
  • GET /api/list-channels - List available channels (for auto-population)
  • GET /api/list-users - List available users (for auto-population)

Note: The /api/list-users endpoint is required for auto-populating user emails. If it doesn't exist in your bot, you can manually enter email addresses, but it's recommended to add this endpoint to your bot API.

Adding the /api/list-users Endpoint

To enable auto-population of user emails, add this endpoint to your Teams Bot API (app.py):

# List all users with personal conversations
async def list_users(req: Request) -> Response:
    """
    List all users who have personal conversations with the bot.
    Returns users with email addresses for use in integrations.
    """
    try:
        from utils.postgres_storage import get_postgres_store
        store = get_postgres_store()
        
        async with store.pool.acquire() as conn:
            # Get only personal conversations (not channels)
            rows = await conn.fetch("""
                SELECT user_id, user_name, user_email, conversation_id
                FROM users 
                WHERE conversation_type = 'personal' 
                AND user_email IS NOT NULL
                AND user_email !~ '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
                ORDER BY user_email
            """)
            
            users = [dict(row) for row in rows]
            
            return json_response({
                "total": len(users),
                "users": users
            })
        
    except Exception as e:
        print(f"Error listing users: {str(e)}", file=sys.stderr)
        return json_response(
            {"error": f"Failed to list users: {str(e)}"},
            status=HTTPStatus.INTERNAL_SERVER_ERROR
        )

# Add to protected endpoints
PROTECTED_ENDPOINTS = [
    "/api/send-message",
    "/api/send-to-channel",
    "/api/update-email",
    "/api/list-users",  # Add this
]

# Add route
APP.router.add_get("/api/list-users", list_users)

API Endpoints Used

Send Message to Chat (Plain Text)

POST /api/send-message
Headers:
  X-API-Key: <your-api-key>
Body:
  {
    "user_email": "user@example.com",
    "message": "Your message here"
  }

Send Message with Mentions

The node adds a mentions array when it finds @[email] tokens in the text.
The message is sent unchanged — the bot does the rewriting.

POST /api/send-message
Headers:
  X-API-Key: <your-api-key>
Body:
  {
    "user_email": "user@example.com",
    "message": "@[jane@corp.ca] ticket #4471 was escalated",
    "mentions": [
      {"email": "jane@corp.ca"}
    ]
  }

The same field works on /api/send-to-channel. The response reports what was
resolved:

{
  "status": "success",
  "mentions_resolved": 1,
  "unresolved_mentions": []
}

When no tokens are found the node omits mentions entirely, and the request is
byte-identical to one from a node without mention support.

Send Message to Chat (Adaptive Card)

POST /api/send-message
Headers:
  X-API-Key: <your-api-key>
Body:
  {
    "user_email": "user@example.com",
    "adaptive_card": {
      "type": "AdaptiveCard",
      "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
      "version": "1.4",
      "body": [
        {"type": "TextBlock", "text": "Hello!", "size": "Large"}
      ]
    }
  }

Send Message to Chat (By Conversation ID)

POST /api/send-message
Headers:
  X-API-Key: <your-api-key>
Body:
  {
    "conversation_id": "19:abc123@thread.v2",
    "message": "Your message here"
  }

Send Message to Channel (Plain Text)

POST /api/send-to-channel
Headers:
  X-API-Key: <your-api-key>
Body:
  {
    "team_name": "Engineering Team",
    "channel_name": "General",
    "message": "Your message here"
  }

Send Message to Channel (Adaptive Card)

POST /api/send-to-channel
Headers:
  X-API-Key: <your-api-key>
Body:
  {
    "team_name": "Engineering Team",
    "channel_name": "General",
    "adaptive_card": {
      "type": "AdaptiveCard",
      "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
      "version": "1.4",
      "body": [
        {"type": "TextBlock", "text": "Channel Announcement!", "size": "Large"}
      ]
    }
  }

Send Message to Channel (By Channel ID)

POST /api/send-to-channel
Headers:
  X-API-Key: <your-api-key>
Body:
  {
    "channel_id": "19:xxx@thread.tacv2",
    "message": "Your message here"
  }

Either targeting style accepts adaptive_card in place of message.

List Channels (for auto-population)

GET /api/list-channels
Headers:
  X-API-Key: <your-api-key>

Development

# Install dependencies
npm install

# Build the node
npm run build

# Watch for changes during development
npm run dev

# Lint code
npm run lint

# Format code
npm run format

Node Structure

This node uses n8n's programmatic style — requests are issued from an
execute() method rather than declared with routing:

  • Node properties are declared in the description object
  • Resources and operations are defined as options
  • Field visibility is controlled using displayOptions
  • Auto-population methods are defined in the methods.loadOptions object
  • Requests are built and sent in execute(), which honours continueOnFail()

License

MIT

Discussion