Repository navigation
Microsoft Teams Alerts Developer Guide
How the Teams channel alerts work, where they live, and the traps. The user page is Microsoft Teams alerts. The Teams bot (a messaging channel, 3.1.0) is a different subsystem - Teams and Mattermost β Developer Guide - and nothing here touches it.
Contributed by Carl Steynvaard (Open Resilience), October 2026. The design is his: the page is a friendly front over Workflows rather than a second engine - every ticked alert is a workflow - plus per-channel team routing for an L1 / L2 / L3 desk, and an Adaptive Card in place of the retired connector card. The code was rebuilt; what changed and why is below.
System β Integrations β Microsoft Teams (system/integrations/msteams.php)
β api/system/msteams_settings.php (administrators)
βΌ
msteamsSaveChannel() βββΆ msteams_channels (address encrypted)
β
ββ msteamsSyncWorkflows() βββΆ workflows (one per ticked alert: trigger + conditions +
β send_webhook, format 'teams_workflows')
ββ msteams_channel_workflows (channel_id, alert_key) β workflow_id
an event βββΆ WorkflowEngine (the same run, queue, retries and delivery log as any workflow)
βββΆ webhook_deliveries βββΆ cron/webhook_deliveries.php βββΆ Teams
It is an alerts-kind provider in includes/integrations/integrations.php; the Integrations card counts channels with an address.
| File | What it is |
|---|---|
includes/integrations/msteams.php |
MSTEAMS_ALERTS (the alert catalogue), msteamsSaveChannel(), msteamsSyncWorkflows(), msteamsAlertRule(), msteamsPrioritiesAtOrAbove(), msteamsDeleteChannel(), msteamsSendTest()
|
api/system/msteams_settings.php |
get (never returns an address), save, delete, test. Administrators only; writes are POST + CSRF |
system/integrations/msteams.php |
The page: one card per channel |
system/integrations/help_msteams.php |
The in-app guide, included by help.php
|
workflow/includes/engine.php |
The teams_workflows built-in format, enrichWithLinks() ({{link}}, {{ticket.url}}, {{app.url}}), ticket.team_assigned / ticket.created field lists |
includes/ticket_events.php |
ticketDispatchCreated() - now sends ticket.assigned_team_id
|
msteams_channels, msteams_channel_workflows (tables) |
The channels, and which workflow each (channel, alert) owns |
tests/msteams-alerts.php |
39 checks |
teams_workflows in WorkflowEngine::BUILTIN_WEBHOOK_FORMATS (and seeded into webhook_message_formats by Database Verification, display order 40):
{"type": "message", "attachments": [{"contentType": "application/vnd.microsoft.card.adaptive", "contentUrl": null,
"content": {"$schema": "http://adaptivecards.io/schemas/adaptive-card.json", "type": "AdaptiveCard", "version": "1.4",
"msteams": {"width": "Full"},
"body": [{"type": "TextBlock", "text": "{{message}}", "wrap": true}],
"actions": [{"type": "Action.OpenUrl", "title": "Open in FreeITSM", "url": "{{link}}"}]}}]}That envelope is what Teams' Post to a channel when a webhook request is received workflow expects. It goes through renderFormatBody() like every format: the template is decoded, merge codes substituted inside string values, re-encoded - so a message with quotes, backslashes or newlines cannot break the JSON.
The older teams format (a MessageCard, made for the Office 365 connectors Microsoft switched off in May 2026) is left exactly as it was, so a workflow already using it sends what it always sent.
enrichWithLinks(), called from enrichPayloadForTemplates() - so the run and the editor's Send test agree. Full addresses from publicBaseUrl(). RECORD_LINKS maps an object to its page (ticket β tickets/?ticket_id=); {{link}} is the first record's page, else the front page.
{{link}} is never empty, on purpose. Teams refuses a card whose Action.OpenUrl has no URL, so a format that pointed its button at {{ticket.url}} would fail outright on every non-ticket trigger (a status page incident, an approval).
msteamsSyncWorkflows() makes the channel's workflows match its settings:
-
Wanted = each ticked alert the settings can make (
msteamsAlertRule()returns null for a priority alert with no priority, a team alert with no teams). No address = nothing wanted. - An existing mapping whose workflow still exists β UPDATE in place (same id, so its history stays together).
- A mapping whose workflow somebody deleted β a new workflow, new mapping.
- A mapping no longer wanted β the workflow is deleted (
workflow_executionsandwebhook_deliverieskeep their rows:ON DELETE SET NULL). - Anything without a mapping is not the page's - never touched, however much it looks like one of its own.
The webhook address goes through webhookEncryptActions(), as every workflow save does.
| Alert | Trigger | Conditions |
|---|---|---|
priority_new |
ticket.created |
ticket.priority_id in the band |
priority_raised |
ticket.priority_changed |
new_priority_id in the band and old_priority_id not in it |
team_new |
ticket.created |
ticket.assigned_team_id in the teams |
team_handover |
ticket.team_assigned |
team_id in the teams |
every_new, sla_warning, sla_breached, status_incident, approval_waiting
|
their trigger | none |
The band - msteamsPrioritiesAtOrAbove(): the chosen priority and every active one with a higher display_order (higher = more urgent, as Watchtower and the Zabbix map read it). No priority name is assumed.
-
TRAP: filter a ticket's team on what each event carries.
ticket.createdsendsticket.assigned_team_id- only since this change; before it, the payload had no team at all.ticket.team_assignedsends the team as a top-levelteam_id. A condition on the other one never matches - and says nothing: the alert is simply never sent. The contributed version filtered both onticket.assigned_team_id, so its team routing never fired. Both triggers' field lists inavailableFields()now say what they carry, so the editor offers the right one. -
Never offer a field a trigger does not send.
ticket.assigned_team_idwas added toticket.created's list only, not to$fullTicket, because the other ticket events do not carry it. -
The address is a credential. Anyone with it can post to the channel. Encrypted in
msteams_channels.webhook_urland in the workflow's action;getreturnshas_urlonly. - The page owns its workflows. An edit made in Workflows is replaced the next time the channel is saved. The workflow's description says so.
-
Confidential tickets leave as their number only -
ticketRedactForOutbound(), the rule every webhook follows. -
dispatch()is not a test tool. It also creates in-app notifications and updates the search index. The tests run one workflow at a time withmanualFire(), which applies the conditions and queues the card exactly as a real event does.
| The contribution | Here | Why |
|---|---|---|
Priorities found by name (P1, P2), falling back to ids 4 and 3 |
A priority chosen per channel, "at or above" by order | Stock installs have Lowβ¦Urgent; ids differ between installs |
Team routing on ticket.assigned_team_id for both events |
The field each event really carries (and the team added to ticket.created) |
It never matched - see the trap |
Messages using {{ticket.assigned_team_name}} on hand-over |
{{team_name}} |
The hand-over event's team is team_id
|
Workflows found by a [fitsm-teams:β¦] marker in their description |
msteams_channel_workflows |
An edited description lost the workflow; a hand-made one with the marker would have been taken over |
| Unticked alerts left as inactive workflows | Removed (history kept) | No pile of dead workflows |
| Formats inserted on save with the site address baked into the template | A built-in format with {{link}}
|
One format for every install, every trigger |
| Six fixed channel slots | Any number of channels | |
"FITSM Cloud" text; SQLite settings; its own admin check and CSRF; register-patch.php
|
Plain text; tables; admin_api_guard.php, the app's CSRF; a registry entry |
One implementation of each; nothing patched |
php tests/msteams-alerts.php
39 checks: the card (valid JSON whatever the message holds, the ticket's button, the front page when there is no ticket), the merge codes offered, the workflows a save makes (count, triggers, conditions, encrypted address), updating in place, removing, recreating, never touching one it did not make, validation, the alerts firing or not through the real engine and queue, Test against a local stand-in for Teams (accepted, and refused with Teams' words), and the API (administrators only, no address returned, delete removes the workflows).
What it cannot prove: that Microsoft's own endpoint accepts the card. The envelope is the one Teams documents for Workflows webhooks, and the one the contribution ran in production; Test on the page is the proof for a real channel.
Related: Microsoft Teams alerts Β· Workflows Β· Webhook Message Formats Β· Teams and Mattermost β Developer Guide Β· Developer Tests
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
-
Architecture
- β³ π§Ή Root folder tidy (URL routing)
- β³ π Database Integrity (FKs & constraints)
- β³ π οΈ Database Verification β Developer Guide
- β³ π§© Service Layer (UI + API share code)
- β³ πͺ€ Code traps (the TRAP: comments)
- β³ π Commenting for the Engineering wiki
- β³ π οΈ Engineering wiki scanner - Developer Guide
- β³ π Service-Layer progress
- π€ Contributing (read before a pull request)
- π§ͺ Developer tests
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- π Date & Time Formats
- π± Currencies
- π§ The sidebar (optional module rail)
- π§© Preference templates
- π Display order (modules, Watchtower cards) β Developer Guide
- Theming & Dark Mode
- ποΈ Recent β getting back to what you were doing
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
-
MobileβFriendly
- β³ π« Mobile: Tickets
- β³ π» Mobile: Assets
- β³ π Mobile: Calendar
- β³ π Mobile: Knowledge
- β³ π¦ Mobile: Service Status
- β³ πΌ Mobile: Watchtower
- β³ π§© Mobile: Problem Management
- β³ π Mobile: Change Management
- β³ πΏ Mobile: Software
- β³ β Mobile: Tasks
- β³ π Mobile: Forms
- β³ π Mobile: Contracts
- β³ π Mobile: Domains
- β³ π Mobile: People
- β³ π Mobile: Projects
- β³ ποΈ Mobile: Files
- β³ π Mobile: LMS
- β³ πΊοΈ Mobile: CMDB
- β³ πΊοΈ Mobile: Network Mapper
- β³ π§ Mobile: Process Mapper
- β³ βοΈ Mobile: Workflow
- β³ π₯οΈ Mobile: System
- β³ π Mobile: Reporting
- β³ π Mobile: System Wiki
- β³ π Mobile: Self-Service Portal
- β³ π§° Mobile: Techniques & Tricks
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- β³ π‘οΈ CSRF protection (S4) β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- π CardDAV contact sync
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π Proposal: custom fields, inventory import and change evidence (#194)
- β³ π οΈ Custom fields, evidence + inventory - review (#195-#197)
- β³ π οΈ Long forms in a task's Work view - review (#198)
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π REST API: Domains
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ π·οΈ REST API: Cost centres
- β³ π§βπΌ REST API: Analysts
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ π οΈ Inbox.js map β Developer Guide
- β³ π οΈ Tickets settings β Developer Guide
- β³ π Rota copy and paste β Developer Deep Dive
- β³ β Checklists & SOPs
- β³ βοΈ Mandatory fields
- β³ π§Ύ Custom ticket fields
- β³ π·οΈ Ticket categories
- β³ π₯ Assigning tickets to a team, and escalation
- β³ π’ One board across every company
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ π Attachment size limits and large emails
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
-
β³
βοΈ Telegram channel - β³ β CSAT company scope and filters β Developer Guide
- β³ π₯ Microsoft Teams channel
- β³ π¨οΈ Mattermost channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ β Record previews
- β³ π Ticket notes: internal or shared
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ π¨ Telling the analyst a ticket is theirs
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ π Confidential tickets
- β³ π₯ Portal managers
- β³ π Who has seen a ticket
- β³ π Reading long tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ ποΈ The folder pane
- β³ π½ Just my tickets, or no closed ones
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- β³ π Case reports - a ticket and its work as one PDF
- β³ π Scheduled work in your own calendar
- Problem Management
- Tasks
- π Projects
-
Assets
- β³ π’ Moving an asset between companies
- β³ π Shared asset locations
- β³ π§βπΌ Assigning assets to analysts
- β³ π Warranty and lease alerts
- β³ π Leases
- β³ π Saved table views
- β³ π¨οΈ Recording anything, and importing it
- β³ π§© Asset type layouts
- β³ π Inventory freshness
- β³ π Connected assets
- β³ π Asset review
- β³ π·οΈ QR asset labels
- β³ π Who holds what, and handover documents
- β³ π₯οΈ The inventory agent (PowerShell)
- β³ ποΈ Proxmox VE servers
- β³ βοΈ VMware Cloud Director servers
- β³ π Linking equipment to tickets
- β³ βοΈ Follow-up tasks on a ticket
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
-
Forms
- β³ π¨ The form designer β Developer Guide
- β³ π Layout & the grid β Developer Guide
- β³ ποΈ Collections β grouping submissions
- β³ π Submissions as PDFs
- β³ β‘ What happens next β a form's own actions
- β³ π οΈ Sections & conditional logic β Developer Guide
- β³ π οΈ Lookup fields β Developer Guide
- β³ π‘οΈ Catalogue request approvals
- People
- Domains
- ποΈ Files
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- π‘ Zabbix (monitoring)
- π¬ Microsoft Teams alerts
- π 3CX (phone system)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ π’ One board across every company
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)
- What this is
-
π Bugs resolved
- β³ π³ The Docker image was missing PHP extensions
- β³ π A failed DNS lookup read as a missing record
- β³ π A link could change data
- β³ π Access outlived its removal
- β³ π Knowledge articles could run scripts
- β³ π Knowledge writes ignored permissions
- β³ π Company boundaries (Oct 2026)
- β³ π Smaller gaps (Oct 2026)
- β³ π Escaped text could leave its attribute
- β³ π Task descriptions were not cleaned
- β³ π Task documents ignored company scope
- β³ π Repeating tasks ignored company scope
- β³ π¬ #183 Mailbox checks overlapped
- β³ π #188 Transfer lock shown off when unknown
- β³ β #184 Subtask names and Overdue
- β³ π§© #186 Teams: the buttons did nothing
- β³ π Text in a change could run script
- β³ π AI settings changed without the AI permission
- β³ πͺ #178 Deleting an analyst failed
- β³ π« A team with no departments showed 0 tickets
- β³ π Projects: adding a phase did nothing on Save
- β³ π Leavers could still sign in
- β³ πΌοΈ Logo and courses broke on Apache with PHP-FPM
- β³ π’ Chat tickets ignored your ticket numbering
- β³ π Dates shown as a dash, or in server time
- β³ π Assets β Users showed people from other companies
- β³ π Restricted analysts could read other modules' data
- β³ πΌοΈ Replies with a picture in the thread failed to send
- β³ π Reply attachments never reached the customer
- β³ π οΈ Outbound email attachments β Developer Guide
- β³ π A global SSO provider was missing from the portal
- β³ π Behind a proxy, the SSO redirect said http
- β³ βοΈ The portal tagline moved when you saved it
- β³ π¨ The portal settings screen forgot what you saved
- β³ π‘οΈ The approvals inbox said "Error" and nothing else
- β³ π A table's answers were missing from the PDF
- β³ β A single-select column let you tick every option
- β³ π The portal ignored a form's field widths
- β³ π The tasks board stopped taking clicks
- β³ ποΈ #121 The index list is out of date after upgrading
- β³ π #133 The calendar subscription was empty
- β³ π #131 Tasks always reopened on the board
- β³ π₯ #129 Every page returned HTTP 500 after upgrading
- β³ π³ #127 A PHP warning above the System page
- β³ π #126 Notes stamped with the server's clock
- β³ π Storing every date in UTC
- β³ πͺ The portal was down for everyone signed in
- β³ βοΈ #120 Workflow notes could never be written
- β³ βοΈ #123 Three errors when running Database Verification
- β³ π #122 The description box was a stub in the corner
- β³ π£ Demo data deleted real accounts
- β³ π #117 Sign-in redirected to the wrong address
- β³ π¨ #108 The priority dot was invisible
- β³ β±οΈ #116 Time logged from the right-click menu
- β³ π #114 API keys refused by our own guard
- β³ ποΈ #110 Assigning a task told nobody
- β³ πͺ #107 Signed out while still working
- β³ π #103 "Share with Requester" reached nobody
- β³ π #102 Search found nothing for hyphens
- β³ πͺ #101 Source code editor opened behind
- β³ βοΈ #88 Subtasks could not be ticked off
- β³ π» #84 Asset deep link selected nothing
- β³ π« #79 A new ticket arrived with no status
- β³ π§ #79 A ticket from email did not say so
- β³ π #78 Bell opened to nothing
- β³ π¬ #77 Mail only collected from Inbox
- β³ π #74 The default password could not be changed
- β³ π¦ #70 Renaming an impact level
- β³ π€ #67 App-only mailboxes could not send
- β³ π #45 Verify only ever worked for Microsoft
- β³ π #45 IMAP reported as not authenticated
- β³ βοΈ An email template stopped escaping itself
- β³ π The portal dashboard showed the wrong time
- β³ π’ The folder said 99 and the list showed 96