-
Notifications
You must be signed in to change notification settings - Fork 15
Webhook Message Formats
Add any chat platform to FreeITSM without a code change. Google Chat, Mattermost, Rocket.Chat, Telegram, PagerDuty — if it accepts an incoming webhook, you can add it yourself from Workflows → Settings → Message formats.
When a workflow sends a webhook, the Format dropdown decides the shape of the JSON we POST. Slack, Teams and Discord all want different things, which is why picking the wrong one gets you an HTTP 400.
Now look closely at what a "format" actually is:
Slack → {"text": "{{message}}"}
Discord → {"content": "{{message}}"}
Teams → {"@type": "MessageCard", …, "text": "{{message}}"}
A chat format is nothing but a JSON body template with a {{message}} slot.
That's the whole thing. Which means the Custom (raw JSON) format already did everything a preset does — a preset is just a custom body somebody saved and gave a name to. They were never a separate mechanism; they were the same mechanism, frozen into PHP.
So they aren't code any more. They're rows in webhook_message_formats, and you can add your own.
Workflows → Settings → Message formats → Add. Five fields — only the first three are required:
| Field | What it does |
|---|---|
| Name | What appears in the workflow editor's Format dropdown. |
| Key | The identifier stored inside every workflow that uses this format. Lowercase, no spaces. Avoid changing it later — existing workflows reference it. |
| Body template | The exact JSON the platform expects, with {{message}} where your workflow's message should be dropped in. |
| URL pattern (optional) | A regular expression matching that platform's webhook URLs. |
| Formatting hint (optional) | One line about the platform's markdown, shown under the Message box in the editor. |
Then it appears in the Format dropdown for every webhook action, immediately.
Copy these straight in. Always check the platform's current docs — services change their webhook contracts, and the ones below are correct as written but not frozen in time.
{"text": "{{message}}"}-
URL pattern:
chat\.googleapis\.com - Hint: Google Chat: bold, italic, backtick-code.
Slack-compatible, so the same body works.
{"text": "{{message}}"}-
URL pattern:
/hooks/ - Hint: Mattermost markdown: bold, italic, backtick-code.
{"text": "{{message}}"}-
URL pattern:
/hooks/
{"topic": "freeitsm-alerts", "title": "FreeITSM", "message": "{{message}}", "priority": 4}-
URL pattern:
ntfy\.sh - Note the static fields —
topicandpriorityaren't variables, they're just part of the template. A format doesn't have to be only{{message}}.
{"chat_id": "-1001234567890", "text": "{{message}}", "parse_mode": "Markdown"}- The workflow's Webhook URL is
https://api.telegram.org/bot<YOUR_TOKEN>/sendMessage. -
URL pattern:
api\.telegram\.org
Where the "message" is one field inside a much bigger structure:
{
"routing_key": "YOUR_INTEGRATION_KEY",
"event_action": "trigger",
"payload": {
"summary": "{{message}}",
"source": "FreeITSM",
"severity": "critical"
}
}-
URL pattern:
events\.pagerduty\.com
Templates can use any merge code, not just {{message}}. So a format can be genuinely bespoke:
{
"embeds": [{
"title": "{{ticket.number}} — {{ticket.subject}}",
"description": "{{message}}",
"color": 15158332,
"fields": [
{"name": "Priority", "value": "{{ticket.priority_name}}", "inline": true},
{"name": "Requester", "value": "{{ticket.requester_email}}", "inline": true}
]
}]
}Give it the key discord-embed, and every ticket workflow can post a proper card instead of a line of text.
⚠️ A format using ticket codes like{{ticket.number}}only makes sense on ticket triggers. On aknowledge.publishedworkflow those fields don't exist and will render blank. See Workflows → merge codes.
This is the detail that would bite a naive implementation, so it's worth stating plainly.
We do not paste your message into the template as text. If we did, a message containing a double quote, a backslash or a newline would produce invalid JSON — and a maliciously-crafted message could inject structure into the payload.
Instead: the template is decoded into a real data structure, the substitution happens inside the string values, and json_encode puts it back together and does all the escaping.
A message containing this:
He said "hello" \ then
a newline, and "}, {"injected": "yes
...comes out as valid JSON with exactly one key, message intact:
{"content":"He said \"hello\" \\ then\na newline, and \"}, {\"injected\": \"yes"}You never have to think about quoting. Write the template the way the platform documents it.
Slack, Teams and Discord are also defined in the engine's code, not only in the table. If webhook_message_formats is missing, empty or unreadable, the engine uses those.
A mangled setting must never be able to stop your webhooks going out.
A workflow pointing at a deleted format is different — that fails loudly, with an error naming the format. Silently falling back to some other shape would post a payload the receiver rejects for no visible reason, which is exactly the kind of failure that costs an afternoon.
Slack, Teams and Discord ship seeded and cannot be edited or deleted.
Editing Slack's body template in place would change the payload for every Slack webhook on the install, instantly, with nothing pointing at the cause. So instead there's a Copy button: it clones the built-in into a new, editable format. Want a Slack variant with a custom username and icon_emoji? Copy Slack, edit the clone, point your workflow at it.
Same add-only model as the freemail domain list — consistent rather than a new convention to learn.
Deleting a format that workflows still use is refused, and the error names them. The format key lives inside each workflow's action args, so deleting one out from under a live workflow would break it at the next fire.
Both exist because of bugs that cost real time during development.
URL pattern. Paste a Discord webhook URL while the Format dropdown is still on Slack, and Discord returns an HTTP 400 with nothing useful in it. The app can simply notice: if the URL doesn't match the chosen format's pattern, the editor warns — and if it matches a different known format, it says so by name: "This looks like a Discord webhook URL, but the format is set to Slack."
Formatting hint. Discord treats *one asterisk* as italic. Slack treats it as bold. Write a Slack-flavoured alert, point it at Discord, and your urgent P1 banner comes out in gentle italics. The hint puts that fact under the box you're typing in, rather than in a help page you'd have to already know to read.
Custom (raw JSON) and Full record aren't listed on the settings page, and that's deliberate.
- Custom sends the exact JSON you write on the action itself.
- Full record sends the entire object — the same shape the REST API returns.
Neither of them wraps a message. They're structurally different, and forcing them into the "body template with a {{message}} slot" shape would be a lie about what they do. They stay in the engine.
| Thing | Where |
|---|---|
| Table | webhook_message_formats |
| Built-in definitions + fallback | WorkflowEngine::BUILTIN_WEBHOOK_FORMATS |
| Registry loader | WorkflowEngine::webhookFormats() |
| Template rendering (the escaping) | WorkflowEngine::renderFormatBody() |
| Settings tab |
workflow/settings/ → Message formats |
| Endpoints |
api/workflow/formats.php, save_format.php, delete_format.php
|
- Webhooks — the full outbound-webhook guide (payloads, signing, delivery engine, data protection).
-
Workflows — merge codes, including
{{ticket.number}}and the_nametwins you can use inside a template. - HTTPS Certificates & CA Bundles — if your first webhook fails with a certificate error, it's almost certainly this and not your format.
- Workflow & Webhook Pitfalls — the bugs behind the URL pattern and the formatting hint.
FreeITSM — an open-source IT Service Management platform · github.com/edmozley/freeitsm · MIT licence
- Installation
- ⏰ Scheduled tasks (cron jobs)
- Architecture
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- Theming & Dark Mode
- ⌨️ Command palette (⌘K)
- 🔍 Searching inside tickets
- 📄 Attached documents
- Mobile‑Friendly
-
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
- Single Sign-On (SSO)
- 🗂️ LDAP & Active Directory
- Browser Extension
- API Reference
-
🔌 REST API — how it works
- ↳ 🎫 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: Service Status
- ↳ ☀️ REST API: Morning Checks
- ↳ 📝 REST API: Forms
- ↳ ⚙️ REST API: Workflow
- ↳ 🗺️ REST API: Network Mapper
- ↳ 🧭 Using the API docs page
- ↳ 📐 OpenAPI specification
- ↳ ✅ OpenAPI: kept correct
- ↳ 🛠️ Maintaining the catalogue
- Watchtower
-
Tickets
- ↳ Mailbox Authentication
- ↳ 📤 Email send log
- ↳ Basic IMAP mailboxes
- ↳ Email rendering & images
- ↳ SLA Management
- ↳ WhatsApp channel
- ↳ 💬 Web chat channel
- ↳ 🟣 Slack channel
- ↳ 🔗 Linking tickets
- ↳ 🗒️ Canned responses
- ↳ ✉️ Limiting replies to particular senders
- ↳ ✍️ Email signatures
- ↳ 🌐 The public web address
- ↳ 🔢 Ticket numbering
- ↳ 🙋 Raising a ticket for someone else
- ↳ 🔀 Merging tickets
- ↳ ⑂ Splitting tickets
- ↳ ✅ Selecting several tickets
- ↳ 🗂️ The folder pane
- ↳ 🛠️ Snoozing tickets — Developer Guide
- ↳ 👥 Collision detection
- ↳ ⏱️ Time tracking
- Problem Management
- Tasks
- Assets
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
- Forms
- Contracts
- Service Status
- 🔔 Notifications
- 🚨 War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- ↳ 📊 Progress tracker
- ↳ Concepts & vocabulary
- ↳ Email routing & mailboxes
- ↳ Settings: global vs per-company
- ↳ Users & self-service
- ↳ Staff cross-company access
- ↳ 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)