Einheit 5 — Microsoft Teams
Was du nach dieser Einheit kannst: Den passenden Weg für deinen Anwendungsfall wählen, einen Azure Bot für Teams einrichten, seine Nachrichten in 42°OS empfangen und über die Bot-Schnittstelle antworten — sowie Benachrichtigungen per Workflows-Webhook in einen Kanal schicken.
Drei Wege — und welcher wofür
Microsoft Teams bietet mehrere Möglichkeiten, externe Systeme anzubinden. Sie unterscheiden sich grundlegend:
| Outgoing Webhook | Workflows-Webhook | Azure Bot | |
|---|---|---|---|
| Richtung | Teams → Workflow, Antwort nur sofort | nur Workflow → Teams | beide Richtungen |
| Wo | nur Kanäle, nur per @Erwähnung | Kanäle, Chats | Einzelchat, Gruppenchat, Kanäle |
| Antwort später möglich | nein — nur innerhalb von 5 Sekunden | — | ja |
| Aufwand | gering | gering | hoch (Azure, App-Paket, Freigabe) |
| Geeignet für | — (siehe unten) | Benachrichtigungen, Berichte | Assistenten, Dialoge |
Outgoing Webhook — Teams erwartet die inhaltliche Antwort direkt in der HTTP-Antwort, innerhalb von fünf Sekunden. Der Webhook Agent in 42°OS antwortet aber mit einem festen Text, und spätere Antworten sind nicht vorgesehen. Für 42°OS-Workflows ist dieser Weg deshalb nur als Auslöser brauchbar („Auftrag angenommen“), die eigentliche Antwort müsste über einen Workflows-Webhook separat in den Kanal gehen. Für einen Assistenten ist das zu umständlich.
Workflows-Webhook — der Nachfolger der „Incoming Webhooks“ über Office-365-Connectors, die Microsoft im Mai 2026 eingestellt hat. Ideal, um Ergebnisse, Warnungen oder Berichte in einen Kanal zu schicken. Er kann keine Nachrichten empfangen.
Azure Bot — der vollwertige Weg. Der Bot empfängt Nachrichten, der Workflow antwortet, wann er fertig ist. Die Einrichtung ist aufwendiger, aber nur einmal nötig.
Diese Einheit zeigt den Azure Bot ausführlich und den Workflows-Webhook für Benachrichtigungen am Ende.
Doku: Bot-Ressource anlegen · Nachrichten senden und empfangen · Authentifizierung · Benutzerdefinierte Apps hochladen · Workflows-Webhook
Teil A — Azure Bot
Du brauchst: ein Azure-Abonnement, Rechte zum Anlegen von App-Registrierungen in Microsoft Entra ID, und in Teams die Berechtigung, eigene Apps hochzuladen (oder eine Person aus der Teams-Administration, die die App freigibt).
Schritt 1 — Den Eingang in 42°OS anlegen
Workflow Kanal | Teams, darin ein Webhook Agent teams_eingang:
| Option | Wert |
|---|---|
secret | lange Zufallszeichenkette |
verbs | post |
payload_path | . |
response | OK |
code | 200 |
Eine IP-Freigabe ist hier nicht sinnvoll — Microsoft sendet aus großen, wechselnden Adressbereichen.
Schritt 2 — Die Bot-Ressource anlegen
- Im Azure-Portal: Ressource erstellen → nach Azure Bot suchen → Erstellen.
- Bot-Handle: ein eindeutiger technischer Name, z. B.
firma-assistent. - Abonnement, Ressourcengruppe und Preisstufe wählen.
- Typ der App: Einzelmandant (Single Tenant). Neue mandantenübergreifende Bots können seit dem 31. Juli 2025 nicht mehr angelegt werden.
- Erstellungstyp: neue Microsoft-App-ID erstellen lassen.
- Überprüfen + erstellen.
Schritt 3 — Endpunkt und Zugangsdaten
- Öffne die neue Bot-Ressource → Konfiguration.
- Trage bei Messaging-Endpunkt die Webhook-URL aus Schritt 1 ein und speichere.
- Notiere die Microsoft-App-ID und die App-Mandanten-ID (Tenant ID).
- Über Verwalten neben der App-ID gelangst du zur App-Registrierung. Lege dort unter Zertifikate & Geheimnisse einen neuen geheimen Clientschlüssel an und kopiere den Wert sofort — er wird nur einmal angezeigt.
- Zurück in der Bot-Ressource → Kanäle → Microsoft Teams hinzufügen und die Bedingungen bestätigen.
Hinterlege in 42°OS drei Credentials:
| Name | Inhalt |
|---|---|
teams_app_id | Microsoft-App-ID |
teams_tenant_id | App-Mandanten-ID |
teams_client_secret | Wert des geheimen Clientschlüssels |
Geheime Clientschlüssel laufen ab (je nach Einstellung nach 6 bis 24 Monaten). Trag das Ablaufdatum in deine Überwachung ein (Kurs 7, Einheit 8) — sonst verstummt der Bot eines Tages ohne Vorwarnung.
Schritt 4 — Die Teams-App erstellen
Ein Bot ist in Teams nur über eine App erreichbar.
- Öffne das Teams Developer Portal → Apps → Neue App.
- Unter Grundlegende Informationen: Name, Beschreibungen, Entwicklerangaben, Links zu Datenschutz und Nutzungsbedingungen ausfüllen.
- Unter App-Features → Bot → Vorhandene Bot-ID eintragen (die Microsoft-App-ID).
- Bereiche wählen: Persönlich (Einzelchat mit dem Bot), bei Bedarf Team und Gruppenchat.
- Veröffentlichen: entweder In Ihrer Organisation veröffentlichen (Freigabe im Teams Admin Center nötig) oder App-Paket herunterladen und in Teams über Apps → Apps verwalten → App hochladen einspielen.
Ob das Hochladen eigener Apps erlaubt ist, legt die Teams-Administration über Richtlinien fest.
Schritt 5 — Nachrichten übersetzen
Teams liefert eine Activity:
{
"type": "message",
"id": "1485983408511",
"serviceUrl": "https://smba.trafficmanager.net/emea/",
"channelId": "msteams",
"from": { "id": "29:1XJ…", "name": "Maria Müller", "aadObjectId": "7faf…" },
"conversation": { "conversationType": "personal", "id": "a:17I0…" },
"recipient": { "id": "28:c9e8…", "name": "Assistent" },
"text": "Wie lief der Umsatz im Oktober?",
"channelData": { "tenant": { "id": "72f9…" } }
}
In Kanälen steht die Erwähnung im Text: <at>Assistent</at> Wie lief ….
Der JavaScript Agent teams_uebersetzen:
Agent.receive = function() {
var p = this.incomingMessage().payload || {};
var a = p.web_request || p;
if (a.type !== 'message' || !a.from || !a.conversation) { return; }
// Nur Nachrichten aus dem eigenen Mandanten
var tenant = this.credential('teams_tenant_id');
var t = (a.channelData && a.channelData.tenant && a.channelData.tenant.id) || (a.conversation && a.conversation.tenantId) || '';
if (!tenant || String(t) !== String(tenant)) { return; }
// serviceUrl nur von Microsoft akzeptieren — dorthin geht später das Zugriffstoken!
var serviceUrl = String(a.serviceUrl || '');
if (!/^https:\/\/smba\.trafficmanager\.net\//.test(serviceUrl)) { return; }
// Erwähnungen entfernen, HTML-Reste bereinigen
var text = String(a.text || '').replace(/<at>.*?<\/at>/g, '').replace(/<[^>]+>/g, ' ').replace(/ /g, ' ').trim();
var konv = String(a.conversation.id || '').replace(/['"\\]/g, '');
var id = String(a.id || '').replace(/[^A-Za-z0-9:._-]/g, '');
if (!konv || !id || !text) { return; }
var ziel = {
service_url: serviceUrl.replace(/\/?$/, '/'),
konversation: konv,
bot_id: String((a.recipient || {}).id || ''),
nutzer_id: String(a.from.id || ''),
typ: String(a.conversation.conversationType || '')
};
this.createMessage({
kanal: 'teams',
nachricht_id: 'teams-' + id,
unterhaltung_id: konv,
absender_id: String(a.from.aadObjectId || a.from.id || '').replace(/[^A-Za-z0-9:-]/g, ''),
absender_name: String(a.from.name || ''),
text: text,
datei: null,
antwort_auf: id,
ziel_sql: JSON.stringify(ziel).split("'").join('’')
});
};
Dahinter speichert ein Internal Storage Agent teams_ziel_merken (keep_message: true) die Antwortangaben für diese Unterhaltung — mit dem Upsert aus Einheit 2 —, und danach geht die Nachricht an deinen Workflow.
Microsoft signiert jede Nachricht mit einem JWT (RS256) im Header Authorization. Eine vollständige Prüfung braucht RSA-Signaturprüfung und die öffentlichen Schlüssel von Microsoft — das ist in einem JavaScript Agent ohne Krypto-Bibliothek nicht sinnvoll umsetzbar. Der Eingang oben sichert sich deshalb anders ab:
- Die geheime Webhook-URL steht nur in der Azure-Konfiguration.
- Nur Nachrichten aus dem eigenen Mandanten werden angenommen.
- Die
serviceUrlmuss zu Microsoft gehören. Das ist entscheidend: An diese Adresse schickt der Ausgang später das Zugriffstoken des Bots. Eine gefälschte Nachricht mit fremderserviceUrlkönnte sonst das Token abgreifen. - Der Router prüft die Person gegen die Liste berechtigter Nutzer.
Für Assistenten mit Zugriff auf sensible Daten oder Aktionen der Risikostufe 3 solltest du zusätzlich eine vollständige JWT-Prüfung vorschalten, zum Beispiel über einen kleinen vorgelagerten Dienst. Sprich das mit deiner IT-Sicherheit ab.
Schritt 6 — Antworten senden
Eine Antwort braucht drei Schritte: Antwortangaben nachschlagen, ein Zugriffstoken holen, die Antwort senden.
teams_ziel_lesen — Internal Storage Agent, keep_message: true:
SELECT ziel FROM kanal_unterhaltung WHERE unterhaltung_id = '{{ unterhaltung_id }}' AND kanal = 'teams'
teams_token — Post Agent:
| Option | Wert |
|---|---|
post_url | https://login.microsoftonline.com/{% credential teams_tenant_id %}/oauth2/v2.0/token |
method | post |
content_type | form |
payload | {"grant_type": "client_credentials", "client_id": "{% credential teams_app_id %}", "client_secret": "{% credential teams_client_secret %}", "scope": "https://api.botframework.com/.default"} |
emit_messages | true |
parse_response_json | true |
output_mode | merge |
Mit output_mode: merge enthält die Ausgabe sowohl die Antwort von Microsoft (body.access_token) als auch alle Felder der ursprünglichen Nachricht.
teams_aufbereiten — JavaScript Agent:
Agent.receive = function() {
var p = this.incomingMessage().payload || {};
var body = p.body || {};
if (typeof body === 'string') { try { body = JSON.parse(body); } catch (e) { body = {}; } }
var token = body.access_token;
if (!token) { this.error('Kein Zugriffstoken: ' + String(p.status)); return; }
var ziel = {};
try { ziel = JSON.parse(String((p.rows && p.rows[0] && p.rows[0].ziel) || '{}').split('’').join("'")); } catch (e) { ziel = {}; }
if (!ziel.service_url || !/^https:\/\/smba\.trafficmanager\.net\//.test(ziel.service_url)) { return; }
var antwortAuf = String(p.antwort_auf || '');
var url = ziel.service_url + 'v3/conversations/' + encodeURIComponent(ziel.konversation) + '/activities' +
(antwortAuf ? '/' + encodeURIComponent(antwortAuf) : '');
this.createMessage({
teams_url: url,
teams_auth: 'Bearer ' + token,
bot_id: ziel.bot_id,
nutzer_id: ziel.nutzer_id,
konversation: ziel.konversation,
antwort_auf: antwortAuf,
text: String(p.antwort_text || '')
});
};
teams_senden — Post Agent:
| Option | Wert |
|---|---|
post_url | {{ teams_url }} |
method | post |
content_type | json |
headers | {"Authorization": "{{ teams_auth }}"} |
payload | {"type": "message", "from": {"id": "{{ bot_id }}"}, "conversation": {"id": "{{ konversation }}"}, "recipient": {"id": "{{ nutzer_id }}"}, "replyToId": "{{ antwort_auf }}", "textFormat": "markdown", "text": "{{ text }}"} |
disable_url_encoding | true |
emit_messages | true |
emit_on_error | true |
disable_url_encoding verhindert, dass die bereits kodierte Konversations-ID in der URL ein zweites Mal kodiert wird. Prüfe beim ersten Test im Log, ob die URL so ankommt, wie teams_aufbereiten sie gebaut hat.
Ein Zugriffstoken ist eine Stunde gültig. Für einen Assistenten mit wenigen Nachrichten pro Stunde ist es in Ordnung, für jede Antwort ein neues Token zu holen. Bei hohem Aufkommen speicherst du das Token mit Ablaufzeit in einer Tabelle und holst nur dann ein neues, wenn es abgelaufen ist.
Schritt 7 — Testen
- Öffne in Teams die App und starte einen Chat mit dem Bot.
- Schreib „Hallo“ und prüfe die Logs von
teams_eingangundteams_uebersetzen. - Verbinde testweise einen Echo-Agent mit
teams_ziel_lesen. Die Echo-Antwort erscheint als Antwort im Chat.
Antwort von teams_senden | häufige Ursache |
|---|---|
200 / 201 | gesendet |
401 | Token falsch oder für den falschen Mandanten geholt |
403 | Bot ist in dieser Unterhaltung nicht installiert oder wurde entfernt |
404 | Konversations-ID falsch kodiert oder serviceUrl falsch |
429 | zu viele Nachrichten — kurz warten und erneut senden |
Häufige Stolpersteine
- „App kann nicht hochgeladen werden“ — eigene Apps sind per Richtlinie gesperrt. Die Teams-Administration muss das Hochladen erlauben oder die App freigeben.
- Bot reagiert im Kanal nicht — in Kanälen erhält der Bot nur Nachrichten, in denen er erwähnt wird. Für alle Kanalnachrichten braucht die App eine zusätzliche, ressourcenspezifische Berechtigung im App-Manifest.
- Doppelte Nachrichten — antwortet der Webhook nicht innerhalb weniger Sekunden, stellt Teams erneut zu. Der Doppelschutz über
nachricht_id(Kurs 7) fängt das ab. - Antwort ohne Formatierung — Teams stellt Markdown dar, wenn
textFormat: markdowngesetzt ist. Für Tabellen und Schaltflächen nutzt du Adaptive Cards (siehe Teil B).
Teil B — Benachrichtigungen per Workflows-Webhook
Für Nachrichten, die nur in eine Richtung gehen — ein Bericht, eine Warnung, eine Erinnerung in einem Teamkanal — genügt ein Workflows-Webhook. Er ist in wenigen Minuten eingerichtet.
Einrichten
- In Teams am gewünschten Kanal ⋯ → Workflows.
- Die Vorlage „Webhookbenachrichtigungen an einen Kanal senden“ (englisch: Send webhook alerts to a channel) wählen.
- Namen vergeben, Team und Kanal bestätigen, Workflow hinzufügen.
- Die angezeigte URL kopieren und als Credential
teams_kanal_webhookhinterlegen.
Senden
Post Agent teams_kanal_melden:
| Option | Wert |
|---|---|
post_url | {% credential teams_kanal_webhook %} |
method | post |
content_type | json |
payload | siehe unten |
{
"type": "message",
"attachments": [{
"contentType": "application/vnd.microsoft.card.adaptive",
"contentUrl": null,
"content": {
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.2",
"body": [
{ "type": "TextBlock", "text": "{{ titel }}", "weight": "Bolder", "size": "Medium" },
{ "type": "TextBlock", "text": "{{ text }}", "wrap": true }
]
}
}]
}
Grenzen
- Ein Workflows-Webhook gehört einer Person. Verlässt sie das Unternehmen, kann der Flow verwaisen — lege ihn mit einem Funktionskonto an.
- Höchstens 28 KB pro Nachricht; mehr als vier Anfragen pro Sekunde werden gedrosselt.
- Bei der Trigger-Einstellung „Jeder“ darf kein
Authorization-Header mitgeschickt werden. Behandle die URL deshalb wie ein Passwort. - Nachrichten erscheinen im Namen von „Workflows“, nicht mit eigenem Namen und Bild.
Kurzer Selbst-Check
Warum prüft der Eingang, dass die serviceUrl zu Microsoft gehört?
Weil der Ausgang an diese Adresse ein gültiges Zugriffstoken des Bots schickt. Könnte jemand eine Nachricht mit einer eigenen serviceUrl einschleusen, würde er das Token erhalten und könnte im Namen des Bots schreiben.
Ein Bericht soll jeden Montag in einen Teamkanal. Azure Bot oder Workflows-Webhook?
Workflows-Webhook. Es wird nur gesendet, nicht empfangen — dafür ist der Bot überdimensioniert.