Guide
Slack notifications
Connect a channel and every run posts its result there — the verdict in the headline, the numbers underneath, and a link back to the audit. No app to install and no permissions to grant beyond the one webhook.
Last reviewed: 17 August 2026
What gets posted, and when
Slack receives a message when a monitor finishes a scheduled run — as often as daily or as seldom as fortnightly, on a schedule you set and at a time Mend picks inside the due day. It is a report of the run, not an alert: a clean run posts too, because a channel that only ever speaks up when something is broken gives you no way to tell “everything passed” from “the monitor stopped running a fortnight ago”.
Every run posts, including ones you start yourself with Run now. The channel is a record of every check the page has had, which is what makes a gap in it mean a run did not happen rather than a run that was filtered out. A manual run is labelled Run manually. in the footer, so a message arriving at 3pm reads as a colleague rather than as the schedule having drifted.
This is separate from email alerts, and the two are designed for different jobs. Email is an exception report: it waits out a transient failure and only writes when something actually needs you. Slack posts every run. Turning one off does not affect the other.
Connecting a channel
In Slack, add an Incoming Webhook to the channel you want the results in — Slack's own instructions are at api.slack.com/messaging/webhooks. Slack gives you a URL. Paste it into Post results to Slack on your Monitors page and save.
That is the common setup, and the rest of this section assumes it. Mend can also post to a Workflow Builder trigger instead, which needs a different body and a different setting — see Sending to a Workflow Builder trigger below.
The URL must be an https address on hooks.slack.com exactly. Nothing else is accepted: the webhook is a bearer credential sitting in a URL path, so anyone holding it can post into your channel, and a lookalike host is the obvious way to be handed one.
Once saved, Mend never shows the URL back to you. The settings panel displays only the workspace id from the path — enough to tell two workspaces apart, not enough to post anything. If you need the URL again, get it from Slack, or make a new webhook and replace the stored one.
Send a test message posts to the channel immediately, so you can confirm it lands in the right place without waiting for the next scheduled run.
One webhook covers the whole account. Every monitor you have posts to the same channel, and connecting a second webhook replaces the first rather than adding to it. If you want different pages reporting into different channels, that is not something Mend can do today.
Every field in the message
The headline carries the verdict and links to the audit. Below it is a two-column grid of fields. There are ten fields in total, and a given message carries between five and ten of them — five are always present, and five more appear only when a successful run actually found something.
Each entry below gives the exact label as it appears in Slack, followed by the value it is built from.
Always present, in this order:
Status— one of✅ Passedwhen the run found nothing,⚠️ N issueswhen it did, or🚨 Failedwhen the scan itself could not complete. Built fromokand the total ofpages[].counts.Pages audited—pages.length, the number of pages this run covered.Checked—finishedAt, when the run finished.Next check—nextRunAt, when the next one is due. This is the same value stored on the monitor row, not a fresh guess, so the channel and the site can never disagree about it.Sign-in flow—stepCount, rendered asNonewhen it is zero and otherwise as a step count. See monitoring a page behind a login.
Present only when the run succeeded and found issues, inserted after Pages audited:
Critical,Serious,ModerateandMinor— the run-wide total at each impact level, summed acrosspages[].counts.critical,.serious,.moderateand.minor. Those four keys are the complete set of impact levels.Rules failed—ruleCount, the number of distinct accessibility rules broken anywhere in the run.
Four more values are carried by the message without being grid fields: url is the monitored page, and is the link in the headline; error is the failure text, shown in a code block on a failed run and truncated at 400 characters; monitorsUrl is the “Fix it in Mend” link on a failure; and each entry in pages[] carries its own url, counts, ruleCount and auditUrl, which is what the per-page list and the “See the results” link are built from.
These five are omitted from a clean run on purpose. Four zeroes and a zero is five cells of noise on the night the answer is “nothing is wrong”, and the headline has already said so. When there are issues all four impact counts appear even where the count is zero, because the shape of the breakdown is itself the information.
Rules failed is worth one note if you are reconciling the numbers by hand: it cannot be derived from the per-page counts. Adding them up overstates a rule that is broken on four pages, and taking the largest understates it, so the run-wide figure is counted separately and passed through.
Under the grid, a failed run adds the error text, and a multi-page run lists the pages it covered — up to eight, with a note of how many more there were. The footer carries Run manually. when somebody started the run by hand, and on a clean scheduled run it names the cadence instead — Daily check passed., Weekly check passed. or Fortnightly check passed. The every-3-days interval reads Scheduled check passed., English having no adjective for it.
The trigger is a footer line rather than an eleventh field on purpose. The grid is already at Slack's ten-field ceiling, so one more would spill into a second section holding a single lonely cell — and it is not a metric anyone scans the grid for. In the flat payload, where no such ceiling applies, it is a proper trigger key.
The payload Mend posts
What arrives at your webhook is a Slack Block Kit message with exactly two top-level keys, text and blocks. text is the notification and fallback line; blocks is what renders in the channel.
The grid is a section block whose fields array holds one mrkdwn object per cell, each formatted as the bold label, a newline, then the value:
{
"text": ":warning: 3 issues on example.com",
"blocks": [
{
"type": "section",
"text": { "type": "mrkdwn", "text": ":warning: *3 issues on <https://example.com|example.com>*" }
},
{
"type": "section",
"fields": [
{ "type": "mrkdwn", "text": "*Status*\n:warning: 3 issues" },
{ "type": "mrkdwn", "text": "*Pages audited*\n1" },
{ "type": "mrkdwn", "text": "*Critical*\n0" },
{ "type": "mrkdwn", "text": "*Serious*\n2" },
{ "type": "mrkdwn", "text": "*Moderate*\n1" },
{ "type": "mrkdwn", "text": "*Minor*\n0" },
{ "type": "mrkdwn", "text": "*Rules failed*\n2" },
{ "type": "mrkdwn", "text": "*Checked*\n<!date^1786179600^{date_short_pretty} at {time}|2026-08-08 07:00 UTC>" },
{ "type": "mrkdwn", "text": "*Next check*\n<!date^1786266000^{date_short_pretty} at {time}|2026-08-09 07:00 UTC>" },
{ "type": "mrkdwn", "text": "*Sign-in flow*\nNone" }
]
},
{
"type": "context",
"elements": [ { "type": "mrkdwn", "text": "<https://mend-a11y.com/…|See the results>" } ]
}
]
}The only block types used are section and context, and every text object is mrkdwn. Timestamps use Slack's <!date^…> token so each reader sees the moment in their own timezone, with the UTC string after the pipe as the fallback for notifications and older clients.
A section renders at most ten fields and silently drops the rest, so the grid is chunked into additional section blocks rather than one longer array. With ten fields as the current maximum, one section is all that has ever been produced.
Sending to a Workflow Builder trigger
If you want the run to drive something — open a ticket, page whoever is on call, branch on how bad it is — a channel message is the wrong end of the tool. Slack's Workflow Builder can be triggered from a webhook, and Mend can post to it directly.
It needs a different body. A workflow trigger declares typed variables up front and expects a flat object whose keys match them, so the Block Kit payload above — with its nested blocks array — cannot be used: an array is not a variable a workflow can declare.
So when you save the URL, tell Mend which kind it is. The choice sits under the URL field on the Monitors page: an incoming webhook (the default, and what most people want) or a Workflow Builder trigger.
Mend cannot detect this for you, which is the only genuinely awkward part of the feature. Both kinds of URL live on hooks.slack.com and differ only in a path segment Slack does not document as stable. Picking the wrong option saves successfully and then posts a body the other end rejects — so if a destination is failing immediately after setup, this is the first thing to check.
Declare the variables you want under Set up variables on the webhook step, giving every one the Text data type. Text is the only type a webhook variable offers for carrying data — the other choices (channel ID, user ID, user email) are for routing a message, not holding a value — and Mend matches it: every value in the body is a string, the counts included. A JSON number arriving at a text variable is dropped silently rather than converted, which is why the counts come as bare decimal strings — "3", never “3 issues” — that a workflow condition can still compare. Declare only the keys your workflow actually uses: Mend always sends the full set, and Slack ignores any key you have not declared.
Every key in the body, and what it holds:
status—passed,issuesorfailed. The branch point for a workflow that treats a broken scan differently from a bad page.text— the same headline the channel message carries, e.g.:warning: 3 issues on app.example.com/pricing.url— the monitored page.auditUrl— the stored results; see below.issueCount,critical,serious,moderate,minor,ruleCount,pageCount,stepCount— the same numbers the channel message's grid shows, as decimal strings.topIssues— the run's worst findings, ready to paste into a message; see below.finishedAtandnextRunAt— ISO 8601 timestamps.trigger—scheduledormanual. Use it to skip the noisy parts of a workflow for a run somebody is sitting and watching — not paging on-call for a Run now is the obvious one.error— the failure text. Empty on every successful run.
Values that do not apply are empty strings, never null and never absent — a declared variable fails on a null, so a workflow should test for empty, not for missing.
A real run's body looks like this:
{
"status": "issues",
"url": "https://app.example.com/dashboard",
"text": ":warning: 4 issues across 3 pages on app.example.com/dashboard",
"pageCount": "3",
"issueCount": "4",
"critical": "1",
"serious": "0",
"moderate": "3",
"minor": "0",
"ruleCount": "3",
"topIssues": "critical — Images must have alternative text (1 element)\nmoderate — Document should have one main landmark (2 elements)\nmoderate — Page should contain a level-one heading (1 element)",
"finishedAt": "2026-08-16T20:17:33.256Z",
"nextRunAt": "2026-08-23T02:13:13.916Z",
"stepCount": "7",
"trigger": "scheduled",
"error": "",
"auditUrl": "https://mend-a11y.com/audits/7e70dad9-7584-4941-880b-54350ff5c6e2"
}Linking to the audit from your workflow's message
Declare auditUrl as a text variable and insert it into the message step wherever you want the link — the {} Insert a variable button in Workflow Builder's message editor. It points at the stored results for the monitored page, the same audit the channel message's See the results link opens.
It is an empty string when there is nothing to link to: a failed run stores no audit, and a rerun that exactly duplicates one already stored links nothing new. Give it a line your message can afford to leave blank, or branch on it being empty.
The worst findings, ready to paste
topIssues carries the run's five worst rules, one per line, worst first — ordered by impact, then by how many elements fail the rule:
critical — Images must have alternative text (1 element)
moderate — Document should have one main landmark (2 elements)
moderate — Page should contain a level-one heading (1 element)It is one pre-formatted string rather than five variables because a webhook variable cannot hold a list, and five declared slots would be four empty lines on the night one rule breaks. Insert it into a message as-is; it is empty on a clean run and on a failed one, so it adds nothing when there is nothing to say. A run can break more than five rules — ruleCount says how many there really were, and the audit has the rest.
Send a test message posts a flat body to a flat destination, so the button still answers the question it exists to answer. It carries the full set of keys, with the counts at "0" and topIssues empty, so your declared variables all resolve rather than erroring on the first message you send.
When a post fails
There is no retry loop, by design. A monitor runs on a schedule, and the next run posts the next message. Queuing and backing off would be real machinery whose entire benefit is delivering a “3 issues on /pricing” notice a minute sooner.
A failed post never affects the run. The scan result is saved before Slack is contacted, so a Slack outage cannot turn a monitor that worked into a monitor that errored, and nothing you can do in Slack will cost you audit history.
The one case handled specially is a destination that has stopped existing — the webhook deleted, the channel archived, the app uninstalled, the workspace disabled. Slack names these in its response, and Mend disconnects the destination rather than posting into the void every night forever. Everything else — a timeout, a rate limit, a bad gateway — is treated as temporary and kept, because a destination wrongly discarded costs you your configuration with no explanation, while one wrongly kept costs a single failed POST a day.
Either way the failure is visible on the Monitors page: the panel states the last error, alongside when a post last succeeded. That second date is deliberately not reset by a failure, so “last posted 9 days ago” sitting next to an error is what tells you how long the channel has actually been silent. The error itself carries no separate timestamp. That matters because a webhook that has quietly stopped working looks identical to a quiet week from inside Slack. Nothing arrives in both cases. The site is the only place that can tell you which one you are in.
Disconnecting
Disconnect on the Monitors page removes the stored webhook, and runs stop posting immediately — for every monitor, since the destination is account-wide. It does not touch the webhook in Slack itself, so revoke it there too if you want it gone for good. Deleting your account removes the stored destination along with everything else; deleting a single monitor does not, because the other monitors are still using it.