Skip to content

Webhook Message Formats

Ed Mozley edited this page Jul 12, 2026 · 1 revision

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.


The idea

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.


Adding a format

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.


Worked examples

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.

Google Chat

{"text": "{{message}}"}
  • URL pattern: chat\.googleapis\.com
  • Hint: Google Chat: bold, italic, backtick-code.

Mattermost

Slack-compatible, so the same body works.

{"text": "{{message}}"}
  • URL pattern: /hooks/
  • Hint: Mattermost markdown: bold, italic, backtick-code.

Rocket.Chat

{"text": "{{message}}"}
  • URL pattern: /hooks/

ntfy (push notifications to your phone)

{"topic": "freeitsm-alerts", "title": "FreeITSM", "message": "{{message}}", "priority": 4}
  • URL pattern: ntfy\.sh
  • Note the static fields β€” topic and priority aren't variables, they're just part of the template. A format doesn't have to be only {{message}}.

Telegram

{"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

PagerDuty (Events API v2)

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

Discord β€” a rich embed rather than plain text

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 a knowledge.published workflow those fields don't exist and will render blank. See Workflows β†’ merge codes.


The two rules that keep this safe

1. Escaping is handled for you

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.

2. The engine has a hardcoded fallback

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.


Built-ins are locked (and why)

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.


What the URL pattern and formatting hint are really for

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.


What is not a format

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.


Reference

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

⚠️ Adding this feature adds a table β€” run Database Verification after updating.


See also

  • Webhooks β€” the full outbound-webhook guide (payloads, signing, delivery engine, data protection).
  • Workflows β€” merge codes, including {{ticket.number}} and the _name twins 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

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally