# Alerts Sender: Email, Slack, Discord & Teams (`cleanpull/alerts-sender`) Actor

Send an alert or a digest to email (your Resend, Postmark, SendGrid or SMTP account), Slack, Discord, Microsoft Teams or any webhook, with a table of results rendered for each. Run it after another actor or on a schedule. $1 per 1,000 messages delivered.

- **URL**: https://apify.com/cleanpull/alerts-sender.md
- **Developed by:** [CleanPull](https://apify.com/cleanpull) (community)
- **Categories:** Integrations, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.80 / 1,000 message delivereds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Alerts Sender: Email, Slack, Discord & Teams

Send an alert or a digest to **email** (through your own **Resend, Postmark, SendGrid or SMTP** account), **Slack**, **Discord**, **Microsoft Teams** or **any webhook** (Zapier, Make, n8n, your own). Write the message in light markdown and add a table of rows, from the input or straight from another run's dataset. Each channel gets its own rendering: an HTML and plain-text email, Slack blocks, a Discord message with embeds, a Teams Adaptive Card, and JSON for webhooks. **$1 per 1,000 messages delivered**, and nothing for a message that was not delivered.

### What you can do with it

- **"Email me when a competitor changes a price."** Run it after a monitor (as an integration on the monitor's run, or from a task) with Items from a dataset set to that run's dataset, and Skip when there are no items on: you get an email only when something changed.
- **"Post a daily digest to our Slack channel."** Schedule it with your message and a table of the day's rows.
- **"Send the same alert to email, Slack and Teams at once."** Every channel you fill in gets it; one failing never stops the others.
- **"Replace apify/send-mail in my integrations."** Its input keys (`to`, `cc`, `bcc`, `subject`, `text`, `html`, `replyTo`, `isMock`, `attachments`) are read here too. Add your email provider and key (send-mail sent from Apify's own address; this sends from yours).
- **"Preview what each channel would receive."** With no channel filled in, the run returns the rendered email, Slack, Discord and Teams messages without sending anything (free).

### What makes it dependable

- **Your accounts, your sender.** Email goes from your own domain through your own provider, so it lands like your other mail and the sending reputation is yours. Keys, SMTP passwords and webhook addresses are secret inputs: stored encrypted, never written to the results or the log (results show only the host and last 4 characters).
- **Each channel within its limits.** Slack sections stay under 3,000 characters and 50 blocks. Discord content stays under 2,000 characters, with at most 10 embeds and 6,000 characters across them, and mentions turned off, so a product name can never ping a channel. Long tables say how many more rows there were.
- **Never the same message twice.** Resend requests carry an idempotency key made from the run's ID, so a retried request cannot send again. Postmark, SendGrid and your own webhook are retried only when the request never left (or on 429), never after a server error or a dropped connection. Slack, Discord and Teams connectors are retried after a server error (which means not posted), and Teams workflows are not. If you resurrect a run within 24 hours after changing what it sends, Resend answers 409 for the keys it has already seen: start a new run instead.
- **Built against spam.** At most 50 recipients a run, and each "to" recipient gets their own email (nobody sees the other addresses), unless you add CCs or turn Send separately off. SMTP always uses TLS (STARTTLS on 587, TLS from the start on 465) with a login; port 25 is not used.
- **Retries where they help.** A 429 waits as the service asks (Retry-After), and a request that never reached the service is retried, three tries in all, with a backoff. Server errors are retried only where that cannot send twice (see above). A wrong address or key (4xx) is reported at once with the service's own message.
- **Only real addresses.** Chat webhooks must be on the service's own webhook host. No request follows a redirect or connects to a private network address, so a webhook cannot be used to reach inside a network.
- **A clear record.** One row per channel: sent (with the provider's message ID), failed (with the reason), or skipped. `RUN_SUMMARY` gives the outcome: `COMPLETE`, `PARTIAL` (some channels failed), `SKIPPED`, `PREVIEW` or `FAILED`.

### Input

| Field                                                                   | What it does                                                                                                            |
| ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `subject`, `message`, `html`                                            | The subject, the message in light markdown, and optional HTML of your own for the email                                 |
| `items`, `datasetId`, `itemFields`, `maxItemsShown`                     | Rows for the table: given directly, or from a dataset; the columns; how many rows to show                               |
| `skipIfNoItems`                                                         | Send nothing when the table would be empty                                                                              |
| `emailTo`, `emailCc`, `emailBcc`, `emailSeparately`                     | Recipients (at most 50 in all); each "to" recipient gets their own email unless you add CCs or turn Send separately off |
| `emailFrom`, `emailReplyTo`                                             | A sender on your own domain, and where replies go                                                                       |
| `attachments`                                                           | Files as `[{ "filename", "data" (base64), "contentType" }]`, at most 10 and 5 MB in all                                 |
| `preview`                                                               | Render for every channel and send nothing                                                                               |
| `emailProvider`, `emailApiKey`                                          | Resend, Postmark (server token) or SendGrid, with your key                                                              |
| `smtpHost`, `smtpPort`, `smtpSecure`, `smtpUser`, `smtpPassword`        | Or any SMTP server with a login (Gmail and Outlook with an app password, Mailgun, your own), over TLS                   |
| `slackWebhookUrl`, `discordWebhookUrl`, `teamsWebhookUrl`, `webhookUrl` | Chat webhooks and any other endpoint                                                                                    |

#### Example inputs

After a price monitor, email and Slack, only when it found changes:

```json
{
  "subject": "Competitor price changes",
  "message": "**Prices moved** since the last check.",
  "datasetId": "{{resource.defaultDatasetId}}",
  "itemFields": ["title", "price", "previous", "changes", "url"],
  "skipIfNoItems": true,
  "emailTo": ["me@yourdomain.com"],
  "emailFrom": "Alerts <alerts@yourdomain.com>",
  "emailProvider": "resend",
  "emailApiKey": "re_…",
  "slackWebhookUrl": "https://hooks.slack.com/services/…"
}
```

A Teams message with a table given directly:

```json
{
  "subject": "Daily stock check",
  "message": "2 products are out of stock.",
  "items": [{ "product": "Tree Runner 10", "store": "allbirds.com", "stock": "OutOfStock" }],
  "teamsWebhookUrl": "https://….logic.azure.com/workflows/…"
}
```

### Output

One row per channel:

```json
{
  "recordType": "delivery",
  "channel": "slack",
  "target": "hooks.slack.com/…XYZ1",
  "status": "sent",
  "messageId": null,
  "items": 2,
  "at": "2026-10-14T08:00:00.000Z"
}
```

A failed channel has `"status": "failed"` and the service's answer in `reason`, for example `Resend answered 422: domain is not verified`. With no channel, one `preview` row holds the rendered `text`, `html`, `slack`, `discord` and `teams` messages.

### Pricing

| Event                                                           | Price           |
| --------------------------------------------------------------- | --------------- |
| Message delivered (per email sent, or per chat or webhook post) | $1.00 per 1,000 |

Each run also has a $0.00005 start fee. Failed, skipped and preview runs are not charged beyond the start. Your email provider bills its own sending as usual (Resend, Postmark and SendGrid all have free tiers).

### Limits

- **Email needs your own provider account** and a sender address on a domain it lets you send from. The actor does not send email from shared addresses.
- **Example content is never sent.** With a channel set and nothing to send, the run stops with a clear message (or sends nothing, with Skip when there are no items on); the form's example is used only for previews.
- **Teams** accepts messages through a workflow webhook ("Post to a channel when a webhook request is received"). Teams' older connector webhooks are being retired by Microsoft.
- **Items from a dataset:** pick the dataset in the form (or pass a run's dataset in an integration), which gives this actor read access to that one dataset. The actor runs with limited permissions.
- **Nothing delivered**: when your settings or endpoint refuse a message (a wrong webhook, key, address or SMTP login, or a dataset that is not there), the run still succeeds, with a `delivery` row per channel giving the `reason` and `status: "failed"`; nothing is charged, and RUN_SUMMARY's `outcome` is `FAILED`. Only a failure of our own fails the run.

### Changelog

- **0.1.x, reliability** (2026-10-24): a message refused by your own settings or endpoint ends SUCCEEDED with a row saying why (RUN_SUMMARY still says FAILED), instead of a FAILED run; channels refused before sending now get their own row; input that cannot work is explained in a free row instead of failing. Prices unchanged.
- **0.1** (October 2026): email through Resend, Postmark, SendGrid or SMTP; Slack, Discord, Teams and any webhook; markdown with tables rendered per channel; items from a dataset; skip when empty; preview mode; apify/send-mail's keys accepted.

# Actor input Schema

## `subject` (type: `string`):

The email subject, and the title of the chat message.

## `message` (type: `string`):

The text, in light markdown: **bold**, *italics*, [links](https://…), # headings, - lists. Rendered for each channel.

## `html` (type: `string`):

Optional. Your own HTML for the email body, used instead of the rendered message.

## `items` (type: `array`):

Optional. Rows to show as a table under the message (JSON objects), for example the changes a monitor found.

## `datasetId` (type: `string`):

Optional. A dataset whose rows are added to the table (for example the run that triggered this alert). Pick it here so this actor is given read access to it.

## `itemFields` (type: `array`):

Optional. The item fields to show as columns, in order. Empty: the first item's first 6 fields.

## `maxItemsShown` (type: `integer`):

At most this many rows in the table; the message says how many more there were.

## `skipIfNoItems` (type: `boolean`):

Send nothing (and charge nothing) when the table would be empty: for alerts that should fire only when a monitor found something.

## `preview` (type: `boolean`):

Render the message for every channel and send nothing (apify/send-mail's isMock).

## `emailTo` (type: `array`):

Recipients, one per line (at most 50 in all). Each one gets the email separately, so recipients never see each other's addresses (see Send separately).

## `emailCc` (type: `array`):

Optional. Copied recipients (shown to everyone; with CCs the email goes out once to all).

## `emailBcc` (type: `array`):

Optional. Hidden recipients.

## `emailSeparately` (type: `boolean`):

Send each recipient their own email (no one sees the other addresses; each email is billed). Off: one email to all of them.

## `emailFrom` (type: `string`):

The sender, on a domain your provider lets you send from, for example alerts@yourdomain.com or "Alerts <alerts@yourdomain.com>".

## `emailReplyTo` (type: `string`):

Optional. Where replies go.

## `emailProvider` (type: `string`):

How the email is sent: with your account at Resend, Postmark or SendGrid (API key), or through any SMTP server (Gmail, Outlook, Mailgun, your own).

## `emailApiKey` (type: `string`):

Your Resend, Postmark (server token) or SendGrid API key. Stored encrypted; never shown in logs or results.

## `attachments` (type: `array`):

Optional. Files as \[{"filename": "report.csv", "data": "<base64>", "contentType": "text/csv"}], at most 10 files and 5 MB in all.

## `smtpHost` (type: `string`):

For SMTP: the server, for example smtp.gmail.com or smtp.office365.com.

## `smtpPort` (type: `integer`):

For SMTP: 587 (STARTTLS) or 465 (TLS, with Secure on).

## `smtpSecure` (type: `boolean`):

On for servers that use TLS from the start (usually port 465). Off: STARTTLS on 587.

## `smtpUser` (type: `string`):

For SMTP: the login, usually your email address.

## `smtpPassword` (type: `string`):

For SMTP: the password or app password. Stored encrypted; never shown in logs or results.

## `slackWebhookUrl` (type: `string`):

A Slack incoming webhook (https://hooks.slack.com/services/…).

## `discordWebhookUrl` (type: `string`):

A Discord channel webhook (https://discord.com/api/webhooks/…).

## `teamsWebhookUrl` (type: `string`):

A Teams workflow webhook ("Post to a channel when a webhook request is received"); the message arrives as an Adaptive Card.

## `webhookUrl` (type: `string`):

Any https endpoint (Zapier, Make, n8n, your own): receives the subject, text, markdown and items as JSON.

## Actor input object example

```json
{
  "subject": "Price drop: 2 products changed",
  "message": "**2 competitor products changed price** since yesterday.\n\n- Check the table below",
  "items": [
    {
      "product": "Tree Runner",
      "store": "allbirds.com",
      "price": 98,
      "previous": 110,
      "change": "-10.9%"
    }
  ],
  "maxItemsShown": 25,
  "skipIfNoItems": false,
  "preview": false,
  "emailSeparately": true,
  "emailProvider": "resend",
  "smtpPort": 587,
  "smtpSecure": false
}
```

# Actor output Schema

## `results` (type: `string`):

One row per channel: sent, failed (with the reason) or skipped; a preview when no channel was given.

## `summary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "subject": "Price drop: 2 products changed",
    "message": `**2 competitor products changed price** since yesterday.

- Check the table below`,
    "items": [
        {
            "product": "Tree Runner",
            "store": "allbirds.com",
            "price": 98,
            "previous": 110,
            "change": "-10.9%"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("cleanpull/alerts-sender").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "subject": "Price drop: 2 products changed",
    "message": """**2 competitor products changed price** since yesterday.

- Check the table below""",
    "items": [{
            "product": "Tree Runner",
            "store": "allbirds.com",
            "price": 98,
            "previous": 110,
            "change": "-10.9%",
        }],
}

# Run the Actor and wait for it to finish
run = client.actor("cleanpull/alerts-sender").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "subject": "Price drop: 2 products changed",
  "message": "**2 competitor products changed price** since yesterday.\\n\\n- Check the table below",
  "items": [
    {
      "product": "Tree Runner",
      "store": "allbirds.com",
      "price": 98,
      "previous": 110,
      "change": "-10.9%"
    }
  ]
}' |
apify call cleanpull/alerts-sender --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,cleanpull/alerts-sender"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/EicHwMxdxRuhrwZH3/builds/eM9g3tsU3rXWWXigu/openapi.json
