Guida completa alla conversione dei vecchi Webhook di Microsoft Teams in flussi Power Automate: da MessageCard a Adaptive Card, con esempi reali, mapping pronto‑uso e best practice per evitare l’errore “attachments è Null”.
Perché i Webhook di Teams stanno cambiando
Se in passato bastava inviare una MessageCard a un Incoming Webhook di Microsoft Teams per pubblicare notifiche nei canali, oggi quello stesso approccio non è più affidabile. Il formato MessageCard è obsoleto dal 2021 e l’ecosistema Microsoft si è spostato in modo deciso su Adaptive Card (schema moderno, flessibile, accessibile e coerente con Power Automate, Bot Framework e molte altre superfici). Di conseguenza, numerosi connettori storici—tra cui gli Incoming Webhook classici—stanno venendo dismessi o deprecati rapidamente a favore di soluzioni più moderne, spesso con logiche di autenticazione e governance più robuste.
La conseguenza pratica per chi integra sistemi di ticketing, monitoring o applicazioni line‑of‑business è evidente: flussi che prima funzionavano ora falliscono, soprattutto se si tenta di usare l’azione “Post adaptive card” partendo da un payload legacy in formato MessageCard.
Scenario di partenza
Di seguito la fotografia dell’ambiente da migrare, con due integrazioni già presenti in produzione e impatti differenti.
Webhook | Payload originario | Stato attuale |
---|---|---|
1 | MessageCard in formato legacy† | Fallisce nel flusso pre‑definito “When a webhook request is received → Post adaptive card” con l’errore «attachments è Null» |
2 | Stringa semplice inviata con la libreria pymsteams | Già convertito con successo creando manualmente un payload Adaptive Card |
† Il tipo MessageCard è obsoleto dal 2021; Teams e Power Automate richiedono ormai Adaptive Card.
Obiettivo della migrazione
L’obiettivo è disaccoppiare la sorgente che emette il payload (ticketing, script, microservizio) dal canale di destinazione (Teams) usando Power Automate come strato d’integrazione. Così possiamo:
- accettare payload legacy (MessageCard) senza toccare i sistemi sorgente,
- trasformarli in Adaptive Card a valle,
- pubblicarli in chat o canali Teams con l’azione nativa “Post adaptive card”.
┌──────────────┐ HTTP (legacy MessageCard) ┌─────────────────────┐ Adaptive Card ┌─────────────┐
│ Sorgente A ├─────────────────────────────────>│ Power Automate ├────────────────────>│ Teams │
└──────────────┘ │ (HTTP trigger + │ └─────────────┘
┌──────────────┐ HTTP (testo/pymsteams) │ Compose/Parse + │
│ Sorgente B ├─────────────────────────────────>│ Post adaptive) │
└──────────────┘ └─────────────────────┘
Soluzione per Webhook 1 (MessageCard → Adaptive Card)
Panoramica del flusso
- Trigger: When an HTTP request is received (accetta il body legacy).
- Compose (o Parse JSON): intercetta il raw body così com’è per evitare che il flusso cerchi un array
attachments
inesistente. - Trasformazione: converte i campi principali di MessageCard in una Adaptive Card nello schema richiesto.
- Post adaptive card in a chat or channel: pubblica la card risultante in Teams.
Perché il Compose “sblocca” l’errore
L’azione “Post adaptive card” si aspetta un oggetto nel formato { type: "message", attachments: [...] }
. Un MessageCard legacy non ha attachments
e viene quindi interpretato come payload “vuoto”. Inserendo uno step di Compose subito dopo il trigger, prendiamo il controllo del body e costruiamo noi l’array attachments
con dentro il contenuto Adaptive Card.
Tre approcci di trasformazione
- Mappatura manuale in una Compose (consigliata quando i payload sono omogenei e prevedibili).
- Inline Code / Run Python (se disponibile nel tuo ambiente): utile quando servono condizioni, cicli o normalizzazioni più spinte (es. sections dinamiche).
- Azure Logic Apps con funzione
json()
e Data Operations: adatta a scenari enterprise con varianti multiple e governance centralizzata.
Esempio di mappatura minima (Compose)
Questa è la base più semplice per passare da un MessageCard con campi title
e text
a una Adaptive Card 1.4 compatibile con Teams e l’azione “Post adaptive card”.
{
"type": "message",
"attachments": [
{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": {
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.4",
"body": [
{
"type": "TextBlock",
"size": "Medium",
"weight": "Bolder",
"text": "@{triggerBody()?['title']}"
},
{
"type": "TextBlock",
"text": "@{triggerBody()?['text']}",
"wrap": true
}
]
}
}
]
}
Nota: usa le espressioni Power FX (sintassi @{...}
) per “pescare” i campi dal body originale. Dove serve, aggiungi coalesce()
per valori di default:
@{coalesce(triggerBody()?['title'], 'Notifica')}
@{coalesce(triggerBody()?['text'], 'Nessun dettaglio fornito.')}
Mappatura estesa: dai principali campi MessageCard alle Adaptive Card
Se nel MessageCard esistono campi aggiuntivi (ad es. themeColor
, sections
, potentialAction
), la tabella seguente propone un mapping pragmatco:
MessageCard (legacy) | Adaptive Card (target) | Note di mappatura |
---|---|---|
title | TextBlock con weight="Bolder" | Intestazione principale |
text | TextBlock con wrap=true | Testo descrittivo |
themeColor | RichTextBlock o Container con accent | Teams ignora i colori di sfondo arbitrari: preferire elementi testuali o icone |
sections[].facts[] | FactSet | Mappa coppie name/value |
sections[].images[] | Image o ColumnSet | Adatta le dimensioni e style in base al contesto |
potentialAction[] (OpenUri) | Action.OpenUrl | Bottone con title e url |
potentialAction[] (HttpPOST) | Action.Submit | Richiede un bot o logica a valle: il webhook da solo non riceve callback |
Esempio di mappatura “ricca” (Compose)
Qui combiniamo titolo, testo, facts e un bottone “Apri in Jira/Service Desk”. Adatta i campi ai nomi effettivi nel tuo payload.
{
"type": "message",
"attachments": [
{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": {
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.4",
"body": [
{
"type": "TextBlock",
"size": "Large",
"weight": "Bolder",
"text": "@{coalesce(triggerBody()?['title'], 'Notifica')}"
},
{
"type": "TextBlock",
"text": "@{coalesce(triggerBody()?['text'], 'Dettagli non disponibili.')}",
"wrap": true,
"spacing": "Small"
},
{
"type": "FactSet",
"facts": [
{
"title": "Severità",
"value": "@{triggerBody()?['severity']}"
},
{
"title": "Servizio",
"value": "@{triggerBody()?['service']}"
},
{
"title": "Ambiente",
"value": "@{triggerBody()?['env']}"
},
{
"title": "Ticket",
"value": "@{triggerBody()?['id']}"
}
]
}
],
"actions": [
{
"type": "Action.OpenUrl",
"title": "Apri il ticket",
"url": "@{triggerBody()?['url']}"
}
]
}
}
]
}
Post Adaptive Card: come cablare l’azione
Nell’azione “Post adaptive card in a chat or channel” imposta:
- Post as: “Flow bot” o “User”—scegli in base alla policy del tenant.
- Post in: “Channel” (se vuoi postare su un canale di un team specifico) o “Chat”.
- Team e Channel: seleziona la destinazione corretta.
- Message: punta allo output dello step Compose che hai creato.
Risultato: il flusso non cerca più attachments
nel body legacy, perché glieli stai fornendo tu già nel formato giusto.
Soluzione per Webhook 2 (pymsteams → Adaptive Card)
Nel secondo webhook la conversione era già stata avviata a monte, inviando una stringa che il flusso traduce in Adaptive Card oppure costruendo direttamente il JSON nel codice Python. Se stai usando pymsteams
, ricorda che la classe connectorcard
è pensata per scenari legacy; puoi comunque impostare manualmente il payload in formato Adaptive Card e inviarlo al nuovo endpoint di Teams. Un esempio generico (semplificato):
import json
import pymsteams
card = {
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.4",
"body": [
{"type": "TextBlock", "weight": "Bolder", "size": "Medium", "text": "Build fallita"},
{"type": "TextBlock", "wrap": True, "text": "Vedi log e riprova il deploy"}
],
"actions": [
{"type": "Action.OpenUrl", "title": "Apri pipeline", "url": "https://..."}
]
}
teams_msg = pymsteams.connectorcard("https://...yourTeamsWebhookUrl...")
teams_msg.payload = {
"type": "message",
"attachments": [
{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": card
}
]
}
teams_msg.send()
Questo approccio “porta” già in Teams un oggetto compatibile, riducendo il lavoro nel flusso. Valuta se centralizzare comunque la logica in Power Automate per avere monitoraggio, retry e governance più solide.
Pattern di trasformazione: quando scegliere Compose, Inline Code o Logic Apps
- Compose: payload stabili, due‑tre campi da mappare, tempi rapidi di consegna. È il modo più veloce per eliminare l’errore “attachments è Null”.
- Inline Code / Run Python: payload variabili o complessi (es. array facts da costruire dinamicamente in base alle sections presenti). Consente if/else, cicli e normalizzazioni.
- Logic Apps: serve la massima orchestrazione (branching, Data Operations, gestione varianti, integrazioni enterprise). Stesso paradigma dei flussi, ma con strumenti più orientati all’integrazione.
Gestire varianti del payload (MessageCard “real world”)
Nella pratica i MessageCard non sono sempre uniformi. Ecco un esempio di Compose “robusta” che adatta titolo, testo e facts quando i campi cambiano nome (ad es. description
al posto di text
, priority
al posto di severity
):
{
"type": "message",
"attachments": [
{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": {
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.4",
"body": [
{
"type": "TextBlock",
"size": "Medium",
"weight": "Bolder",
"text": "@{coalesce(triggerBody()?['title'], triggerBody()?['summary'], 'Notifica')}"
},
{
"type": "TextBlock",
"text": "@{coalesce(triggerBody()?['text'], triggerBody()?['description'], 'Nessun dettaglio disponibile')}",
"wrap": true
},
{
"type": "FactSet",
"facts": [
{
"title": "Severità",
"value": "@{coalesce(triggerBody()?['severity'], triggerBody()?['priority'], 'n/d')}"
},
{
"title": "Autore",
"value": "@{coalesce(triggerBody()?['creator'], triggerBody()?['author'], 'sistema')}"
},
{
"title": "Categoria",
"value": "@{coalesce(triggerBody()?['category'], triggerBody()?['type'], 'n/d')}"
}
]
}
],
"actions": [
{
"type": "Action.OpenUrl",
"title": "Apri dettagli",
"url": "@{coalesce(triggerBody()?['url'], triggerBody()?['link'])}"
}
]
}
}
]
}
Validazione e test rapidi
Prima di esporre il flusso agli endpoint esterni, testa la trasformazione con un payload di esempio dal designer di Power Automate (o inviando una richiesta HTTP con lo stesso schema). Un MessageCard di test potrebbe essere:
{
"@type": "MessageCard",
"@context": "http://schema.org/extensions",
"title": "Incidente P1",
"text": "Servizio Catalog down nella regione EU.",
"severity": "P1",
"service": "Catalog",
"env": "EU",
"id": "INC-12345",
"url": "https://.../tickets/INC-12345"
}
Invialo al trigger HTTP del flusso: dovresti ottenere una Adaptive Card con titolo, descrizione, facts e bottone “Apri il ticket”.
Hardening: sicurezza, governance e manutenibilità
- Endpoint HTTP: proteggi con chiave, IP allowlist, o sfrutta ambienti separati (Dev/Test/Prod) e variabili d’ambiente per le URL. Evita di lasciare pubblici endpoint non protetti.
- Sensibilità dei dati: applica policy di DLP e limita i connettori. Tutto ciò che esce dal flusso (compresi link e ID di ticket) deve rispettare le regole di classificazione.
- Secrets: non hardcodare ID di team, canali o URL interne. Usa Environment Variables o Key Vault dove possibile.
- Logging: aggiungi uno Scope “Audit” con Compose del payload in ingresso (sanitizzato) e dell’output Adaptive, così da poter analizzare rapidamente eventuali difformità.
- Versioning: se devi cambiare layout o versione dello schema Adaptive, versiona il template (es.
version:"1.4"
→1.5
quando supportato) e mantieni retro‑compatibilità.
Affidabilità e performance
- Retry & resilienza: se usi azioni HTTP a monte o a valle, imposta politiche di retry. Per l’azione “Post adaptive card”, prevedi un scope “On Error” con notifica alternativa (email/Teams amministrativo).
- Idempotenza: se il payload include un
id
univoco, memorizzalo (Dataverse/SharePoint) per evitare doppi post in caso di retry. - Dimensione dei messaggi: mantieni le card compatte. Riduci testo e immagini, preferisci FactSet e link a dettagli esterni. Se la card supera i limiti della piattaforma, suddividi in due messaggi.
- Concorrenza: per picchi elevati abilita il “Concurrency Control” sul trigger e regola il grado di parallelismo in base alle capacità del tenant.
Strumenti di diagnostica: errori tipici e rimedi
Errore / Sintomo | Causa probabile | Risoluzione |
---|---|---|
attachments is Null | Payload in ingresso è un MessageCard senza attachments | Aggiungi Compose (o Parse JSON) e costruisci manualmente attachments[0].content in formato Adaptive |
Card non visualizzata correttamente | Schema o versione non supportata | Usa "version":"1.4" , evita elementi non supportati in Teams |
Bottoni non fanno nulla | Uso di Action.Submit senza un bot o logica di callback | Per semplici link usa Action.OpenUrl ; per postback serve un bot o un endpoint ascoltatore |
Malformed JSON | Virgolette, virgole o escape errati in Compose | Valida il JSON, evita trailing comma e verifica i caratteri speciali con json() se necessario |
Accesso negato alla destinazione | Autorizzazioni al team/canale o policy del connettore | Verifica permessi e policy di DLP; eventualmente posta come Flow bot |
Blueprint del flusso consigliato
- Trigger: When an HTTP request is received
- Method: POST
- Request Body JSON Schema: opzionale (utile per IntelliSense)
- Action: Compose – Build Adaptive Envelope
- Contenuto: uno dei template Compose proposti sopra
- Action: Post adaptive card in a chat or channel
- Post as: Flow bot (consigliato)
- Post in: Channel
- Team/Channel: destinazione
- Message:
@{outputs('BuildAdaptiveEnvelope')}
- Scope (facoltativo): On Error → notifica fallback (email/altro canale)
Template “plug & play” per ambienti enterprise
Quando hai molte sorgenti diverse, estrai la costruzione della card in un’azione dedicata e parametrizzala con variabili:
{
"type": "message",
"attachments": [
{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": {
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.4",
"body": [
{"type":"TextBlock","weight":"Bolder","size":"Medium","text":"@{variables('title')}"},
{"type":"TextBlock","wrap":true,"text":"@{variables('description')}"},
{
"type":"FactSet",
"facts":"@{variables('facts')}"
}
],
"actions":"@{variables('actions')}"
}
}
]
}
Esempio di inizializzazione variabili prima del Compose:
// Initialize variable 'title' => @{coalesce(triggerBody()?['title'], 'Notifica')}
// Initialize variable 'description' => @{coalesce(triggerBody()?['text'], 'Dettagli non disponibili')}
// Initialize variable 'facts' => Costruisci con 'Select' da triggerBody()?['sections']?[0]?['facts']
// Initialize variable 'actions' => Costruisci da 'potentialAction'
Considerazioni pratiche dal campo
- Ticketing API non modificabile: se non puoi correggere il payload in uscita, la trasformazione “a valle” nel flusso resta la via più semplice e veloce.
- Libreria Python: chi converte via codice può usare
pymsteams.connectorcard
impostandopayload
con la struttura Adaptive (come per Webhook 2). - Documentazione scarna: riferimenti di mapping MessageCard → Adaptive Card sono preziosi; costruisci un tuo “catalogo interno” con i template approvati.
- Community di supporto: per domande su Power Automate è di solito più efficace la community dedicata rispetto a forum generici; se il pulsante “Start a topic” non è attivo, fai prima una ricerca e usa “Post your question”.
- Soluzione Atlassian (Jira): esiste un’integrazione aggiornata che supporta i nuovi webhook di Teams; potrebbe richiedere funzionalità premium. Valutala se i ticket provengono da Jira.
Accessibilità e UX delle card
- Gerarchia visiva: usa un TextBlock principale in grassetto per il titolo e testi brevi per il contenuto; i dettagli vanno nel FactSet.
- Wrap e troncamento: abilita sempre
wrap
per evitare tagli imprevisti dei testi. - Azioni chiare: un solo bottone primario (Action.OpenUrl). Eventuali azioni secondarie in una More actions area.
- Internazionalizzazione: se pubblichi in team multilingua, metti le etichette in inglese o duplica il flusso con variabili di localizzazione.
Checklist di migrazione
- Raccogli tutti i punti di emissione: dove nasce il MessageCard?
- Classifica i campi realmente necessari: titolo, testo, severità, link, autore…
- Progetta una Adaptive Card unica e coerente (versione 1.4 consigliata per compatibilità).
- Implementa il Compose di trasformazione subito dopo il trigger HTTP.
- Posta in un canale test e raccogli feedback di leggibilità.
- Aggiungi logging e gestione errori.
- Documenta il nuovo endpoint: schema accettato, campi richiesti/opzionali.
- Pianifica la dismissione dei vecchi endpoint/URL nei sistemi sorgente.
FAQ
Devo riscrivere tutte le integrazioni?
No: se non puoi toccare i sistemi sorgente, trasforma i payload “a valle” nel flusso (strategia consigliata per tempi rapidi).
Posso continuare a inviare testo semplice?
Sì, ma conviene normalizzare in una Adaptive Card per una UX coerente e mantenere compatibilità futura.
Posso avere pulsanti che “scrivono indietro” al sistema?
Con Action.OpenUrl puoi aprire la pagina del ticket. Se vuoi inviare dati indietro (postback), serve un bot o un endpoint di callback gestito da un’azione HTTP in uscita.
Che versione di Adaptive Card usare?
La 1.4 è un buon compromesso di compatibilità e feature. Aggiorna solo quando sei certo del supporto lato Teams.
Riepilogo rapido
- MessageCard non è più supportato ➜ occorre convertirlo in Adaptive Card.
- Webhook 2 dimostra che la conversione funziona se si prepara il payload corretto.
- Per Webhook 1 basta un Compose/Parse JSON dopo il trigger per trasformare il messaggio prima dell’azione “Post adaptive card”.
- Se la sorgente non può cambiare, la trasformazione nel flusso è la soluzione più semplice e future‑proof.
Appendice: pacchetto di esempio del flusso (strutturale)
Per facilitare l’import in ambienti diversi, struttura il tuo flusso con variabili d’ambiente:
ENVTEAMSTEAM_ID
ENVTEAMSCHANNEL_ID
ENVCARDVERSION
(es.1.4
)
Nel Compose sostituisci la versione della card con @{variables('ENVCARDVERSION')}
e referenzia team/canale nell’azione finale. Questo ti permette di promuovere lo stesso flusso da Dev a Prod senza modifiche manuali.
Appendice: modello “incident card” pronto
{
"type": "message",
"attachments": [
{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": {
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.4",
"body": [
{
"type": "TextBlock",
"text": "@{coalesce(triggerBody()?['title'], 'Incident')}",
"weight": "Bolder",
"size": "Large"
},
{
"type": "TextBlock",
"text": "@{coalesce(triggerBody()?['text'], '—')}",
"wrap": true
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"width": "stretch",
"items": [
{"type":"TextBlock","text":"Severità","isSubtle":true},
{"type":"TextBlock","text":"@{coalesce(triggerBody()?['severity'],'n/d')}"}
]
},
{
"type": "Column",
"width": "stretch",
"items": [
{"type":"TextBlock","text":"Servizio","isSubtle":true},
{"type":"TextBlock","text":"@{coalesce(triggerBody()?['service'],'n/d')}"}
]
},
{
"type": "Column",
"width": "stretch",
"items": [
{"type":"TextBlock","text":"Ambiente","isSubtle":true},
{"type":"TextBlock","text":"@{coalesce(triggerBody()?['env'],'n/d')}"}
]
}
]
},
{
"type": "FactSet",
"facts": [
{"title":"Ticket","value":"@{coalesce(triggerBody()?['id'],'—')}"}
]
}
],
"actions": [
{
"type": "Action.OpenUrl",
"title": "Apri ticket",
"url": "@{coalesce(triggerBody()?['url'],'https://example.local')}"
}
]
}
}
]
}
Conclusione
La migrazione da MessageCard a Adaptive Card non richiede una riscrittura massiva: in molti casi è sufficiente incastonare uno step di Compose per generare l’involucro atteso dall’azione “Post adaptive card”. La chiave è progettare una o due Adaptive Card standard per l’organizzazione (incidenti, cambi, notifiche di servizio), centralizzare la trasformazione in Power Automate, e aggiungere un minimo di osservabilità (logging, gestione errori) per mantenere la soluzione robusta nel tempo.
Con questo approccio la tua organizzazione preserva gli investimenti esistenti, elimina l’errore “attachments è Null” e si allinea agli standard moderni di Teams, gettando le basi per automazioni più ricche e sostenibili.