# appleboy/telegram-action: sending Telegram messages from a GitHub Actions workflow

> This GitHub Action posts a Telegram message from a workflow step, configured with a bot token and a chat ID held as repository secrets. It ships as a Docker container wrapping an existing CLI, supports photos, documents, forum topics and proxying, and is published under the MIT licence.

**appleboy/telegram-action** — GitHub Action that sends a Telegram message.

- Repository: https://github.com/appleboy/telegram-action
- Website: https://github.com/marketplace/actions/telegram-message-notify
- Stars: 1,016 · Forks: 129
- Language: Dockerfile
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/appleboy-telegram-action

## A notification step for teams that read Telegram

The problem is small but real. A CI pipeline finishes, and the people who care about it are not watching the Actions tab. Somebody wants a ping on their phone when a deploy goes out or a release is cut.

appleboy/telegram-action is a GitHub Action that sends a Telegram notification message from a workflow step. The intended user is a maintainer or a small team that has already chosen Telegram as its chat tool and wants CI events to arrive in a private chat, a group, a supergroup or a channel, without writing glue code in the workflow.

The configuration surface is deliberately narrow. Two inputs are required, a chat ID and a bot token, and the documentation states that the message input is optional, in which case the action falls back to a default message that reads like `appleboy/telegram-action/telegram message triggered by appleboy (push)`. That default alone is enough to make the action useful, which is the sign of a tool aimed at getting a first notification working in minutes rather than at covering every conceivable reporting case.

The action also does something most alternatives leave to the user. It can send a photo, document, sticker, audio, voice message or video alongside the text, upload a location or a venue, target a specific topic inside a forum supergroup, and send silently with `disable_notification`. Media inputs accept comma-separated lists and glob patterns, and they require `actions/checkout` in the job so the files exist in the workspace.

## The action is a thin Docker wrapper around a CLI

The repository layout explains the design. The tree is small: `action.yml`, a `Dockerfile`, an `entrypoint.sh`, `tests/`, `images/`, a `.goreleaser.yaml` and the README files.

The Dockerfile is the clearest description of the whole thing:

```dockerfile
FROM ghcr.io/appleboy/drone-telegram:1.5.1

COPY entrypoint.sh /entrypoint.sh

WORKDIR /github/workspace

ENTRYPOINT ["/entrypoint.sh"]
```

In other words, the action is a container image built on top of `ghcr.io/appleboy/drone-telegram:1.5.1`, a prebuilt image published separately, with the repository's own entrypoint script copied over it and the working directory set to the workspace where a checkout would place files. The base image is pulled from a registry, so the first run on a cold runner pays a pull, and the base image version is pinned in the Dockerfile, which is a useful thing to know when a behaviour change surprises you.

The practical consequence is that the behaviour of the action is the behaviour of that upstream image plus this entrypoint script. The `socks5` input exists because the underlying client supports a custom proxy URL, accepting `http`, `https` or `socks5`, and that is a feature of the base image rather than something the action adds. The `debug` input, which defaults to `false`, is likewise a client level switch.

The `tests/` directory is where the project's own verification lives, and the README points at a workflow badge for the telegram message workflow, so the action is exercised by CI rather than shipped untested.

## Getting a bot token and a numeric chat ID

The setup path has three steps, and the middle one is where people get stuck.

First, create the bot. You talk to @BotFather in Telegram, send `/newbot` and follow the prompts. BotFather replies with the bot token, and that token is the value you will store as `TELEGRAM_TOKEN`.

Second, find the chat ID. You must send a message to the bot first, or add the bot as a member of a group or channel and post there. Then you call the Bot API and read the ID out of the response:

```bash
curl https://api.telegram.org/bot<token>/getUpdates
```

The chat ID sits in `result[].message.chat.id`. The documentation spells out the formats, because they differ in a way that catches people out. A private chat ID is a positive number such as `65382999`. A group, supergroup or channel ID is negative and usually starts with `-100`, for example `-1001234567890`. You must use the numeric ID, because usernames like `@channelname` are not supported. If `getUpdates` returns an empty result, you send a fresh message in the chat and call it again.

Third, add both values to the repository. In `Settings → Secrets and variables → Actions` you add `TELEGRAM_TOKEN` and `TELEGRAM_TO`, and you reference them in the workflow as `${{ secrets.TELEGRAM_TO }}` and `${{ secrets.TELEGRAM_TOKEN }}` so the values never appear in the workflow file or in the logs.

One diagnostic is documented with a specific meaning. The error "Chat not found" means the chat ID is wrong or the bot has never been added to that chat. That is a precise failure, not a network problem, and it is worth internalising before you start guessing at tokens.

## A first workflow that messages on every push

The usage example in the README is the canonical first step. It sends a custom message on every push, and it uses the GitHub Actions expression syntax to pull the actor, the commit message, the repository and the commit URL into the message body:

```yml
name: telegram message
on: [push]
jobs:

  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      - name: send telegram message on push
        uses: appleboy/telegram-action@v1
        with:
          to: ${{ secrets.TELEGRAM_TO }}
          token: ${{ secrets.TELEGRAM_TOKEN }}
          message: |
            ${{ github.actor }} created commit:
            Commit message: ${{ github.event.commits[0].message }}

            Repository: ${{ github.repository }}

            See changes: https://github.com/${{ github.repository }}/commit/${{github.sha}}
```

The message is a YAML block scalar, so newlines are preserved, and the action posts it to the chat named by `to`. The `to` input accepts a comma-separated list, for example `65382999,-1001234567890`, so one step can notify a private chat and a channel at the same time.

Two details are easy to overlook. The `message` input is optional, so removing it gives you the default text and nothing else, which is a reasonable way to start before you commit to a message format. And the `format` input, which can be `markdown` or `html`, is what makes the Telegram specific markup render; leave it empty and the message is sent as plain text.

The file example in the documentation shows the media path, which always needs a checkout first:

```yml
- uses: actions/checkout@v7
- name: send photo message
  uses: appleboy/telegram-action@v1
  with:
    to: ${{ secrets.TELEGRAM_TO }}
    token: ${{ secrets.TELEGRAM_TOKEN }}
    message: send photo message
    photo: tests/github.png
```

The `actions/checkout@v7` step is not optional decoration. The `photo`, `document`, `sticker`, `audio`, `voice` and `video` inputs are file paths in the workspace, and `message_file` reads a file from disk, so without a checkout those paths do not exist when the action runs.

## Forum topics, formatting and the rest of the input surface

Beyond the basic message, the input list covers a set of Telegram features that each map to a Bot API call. It is worth reading the list as a capability map rather than a configuration reference.

`message_thread_id` targets a topic inside a forum supergroup, and the documentation is explicit that it only applies to forum supergroups. `location` takes a latitude and longitude pair, `24.9163213 121.1424972` in the given example, and `venue` takes a latitude, a longitude, a title and an address, so you can report a job running on a specific machine. `disable_web_page_preview` suppresses link previews, which is what you want when a URL is a build log rather than something worth expanding, and `disable_notification` sends the message silently.

`message_file` is the one input that changes where the text comes from. It overwrites the default message template with the contents of a file you point at, which is how you push a diff summary, a changelog section or a generated report into the chat without interpolating it into the workflow YAML.

`format` selects `markdown` or `html` for the body. This is the input people most often get wrong, because Telegram markup is not Markdown and not HTML in the general case, and the rendering differences show up as literal asterisks or unescaped entities in the delivered message.

The remaining inputs are infrastructural rather than cosmetic. `socks5` points the client at a custom proxy, which matters on networks where Telegram is unreachable, and `debug` turns on verbose client logging when a delivery fails and you need to see the raw API exchange.

## Pinning @v1 or pinning an exact version

The README states the version policy plainly: `@v1` follows the latest `v1.x` release, and the documentation recommends pinning an exact version such as `appleboy/telegram-action@v1.1.1` if you want fully reproducible builds.

The recent releases show a small, deliberate-looking cadence. v1.1.1 was published on 2026-08-16, v1.1.0 on the same day, and v1.0.2 on 2026-08-14. The last push to the default branch `master` was on 2026-08-16, so the code and the newest tag moved together recently, and the action is not archived.

For a notification step this matters less than for a build step, and the trade-off is worth stating plainly. Floating on `@v1` means you get a fix to a formatting or upload bug without touching your workflow, which is convenient and is the reason the recommendation exists. Pinning means a broken or unwanted change cannot reach you without a deliberate pull request, which is the right default for an organisation that reviews workflow changes but is overkill for a personal repository with one notification step.

The licence is MIT, which is permissive, allows use in commercial and internal tooling, and requires only that the copyright notice and the licence text travel with copies of the software. Since the action runs inside your own runner rather than shipping a binary to your users, the practical licence question is mostly about redistribution, and nothing in the terms raises a specific issue for a CI notification. That is a note on permissions, not legal advice.

## Where this approach stops working

There are three honest limits to this approach, and none of them are bugs in the action.

The first is that the notification travels over Telegram's infrastructure to a chat you identify by number. There is no delivery receipt beyond the action's exit status, and there is no retry with backoff documented for a transient Telegram outage. If the API is unreachable during a brief window, the message is simply not delivered, and a workflow step that is not retried leaves you with a silent gap.

The second is that the action is a container pull. Every job that uses it needs the image, which means registry access from your runner. On a self-hosted runner with a restrictive network policy, or in an air-gapped environment, the base image `ghcr.io/appleboy/drone-telegram:1.5.1` may not be reachable at all. The same environment also blocks `api.telegram.org`, which the `socks5` proxy input is there to work around.

The third is the alternative worth naming. You can call the Bot API directly from a workflow step with `curl`, and for a plain text message that is one line and no third party dependency at all. The difference in approach is that the raw call handles exactly what the API exposes and nothing more: no glob patterns for attachments, no `message_file` substitution, no forum topic selection helper, no `format` handling, no `debug` output to inspect when something goes wrong. The action earns its place when you send more than plain text, and it is overhead when you do not.

There is also a quieter consideration. The bot token is a credential for a bot that can read and post in the chats it is a member of, and it lives in repository secrets where anyone with write access to workflows can reference it. Keeping it in `TELEGRAM_TOKEN` rather than in a workflow file is the whole of the security practice here, and the action does nothing extra to protect it.

## Conclusion

This action is worth adopting if your team already lives in Telegram and you want build and deploy status to land in a chat, since the setup cost is a bot token, a numeric chat ID and a short `with:` block, and the media, topic and formatting inputs cover most notifications without extra shell steps. It is the wrong choice if you need delivery guarantees, if your organisation blocks Telegram at the network level, or if you want the notification logic to be plain workflow YAML that any reviewer can read. Verify three things first: that `getUpdates` returns a non-empty result before you store the chat ID, that the workflow includes `actions/checkout` whenever you pass `photo`, `document` or `message_file`, and that you pin `appleboy/telegram-action@v1.1.1` rather than floating on `@v1`.

## FAQ

### How do I get the chat ID for appleboy/telegram-action?

Send a message to your bot first, or add the bot to the group or channel and post there, then call `curl https://api.telegram.org/bot<token>/getUpdates` and read `result[].message.chat.id`. Private chat IDs are positive numbers, group and channel IDs are negative and usually start with `-100`, and usernames are not supported.

### What does the Error: Chat not found message mean?

According to the documentation it means the chat ID is wrong or the bot has never been added to that chat. It is a configuration error rather than a network failure, so check the numeric ID and the bot's membership in the chat.

### Do I need actions/checkout to send a photo with appleboy/telegram-action?

Yes. The photo, document, sticker, audio, voice and video inputs are file paths, and `message_file` reads from disk, so the workspace must contain the files. The README example puts an `actions/checkout@v7` step before the action.

### Can appleboy/telegram-action post to a forum topic?

Yes, through the `message_thread_id` input, which takes the unique identifier of a message thread or topic. The documentation notes that this applies to forum supergroups only.

### Should I use appleboy/telegram-action@v1 or pin a version?

The README states that `@v1` follows the latest `v1.x` release, and recommends pinning an exact version such as `appleboy/telegram-action@v1.1.1` for reproducible builds. The most recent tags are v1.1.1 and v1.1.0, both dated 2026-08-16.

### How does appleboy/telegram-action handle a blocked Telegram network?

There is an optional `socks5` input that takes a custom proxy URL and accepts `http`, `https` or `socks5`. The action also has a `debug` input, disabled by default, that turns on verbose client output when you need to see what happened during a failed delivery.

## Sources

- [appleboy/telegram-action on GitHub](https://github.com/appleboy/telegram-action)
- [License: MIT](https://github.com/appleboy/telegram-action/blob/master/LICENSE)
- [Project website](https://github.com/marketplace/actions/telegram-message-notify)
- [README](https://github.com/appleboy/telegram-action/blob/master/README.md)
- [Releases](https://github.com/appleboy/telegram-action/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/appleboy-telegram-action
