---
title: Notification channels
description: How a routine reaches a person outside the Desk. Email and Slack as gate transports, push as a one-way notice, Telegram and ntfy as unverified options, and the budget alert.
sidebar:
  order: 6
---

## What it is

A notification channel is how a routine reaches a person when they are not looking at the Desk. RoutineOS uses channels for two jobs:

1. **Gate transport**: asking a named approver to approve or reject an action, and reading their reply. Set by `[gate] transport`.
2. **Notice**: telling someone something happened, with no reply expected. The budget alert (`[budget] alert_channel`) is the one built-in notice.

The Desk itself (Inbox and Today) is the primary place people see a routine's work; see [Notifications](/concepts/notifications).

## Why it exists

An approval nobody sees times out. A gate needs a channel the approver actually reads, and a reply path the next run can check. Channels differ a lot in what is verified, so RoutineOS states it per channel.

## How it works

### Channels and their status

From `skills/routineos/references/gates.md` and the registry:

| Channel | Use | Status |
|---|---|---|
| Email (Gmail connector) | Gate transport | Verified end to end on 2026-10-28; sends unattended with no policy change in that pilot. Replies read with the thread tool in plain text; store the message id and thread id |
| Slack | Gate transport | Verified end to end. Needs the Slack connector's tool policy set to "Ask for approval" on claude.ai, otherwise no send tool is exposed. Store the DM channel id and message timestamp |
| Push | One-way notice | Pilot-verified as a notice; tapping it opens a chat window, not the artifact (`push-one-way`). A reply path is unverified (`push-reply-path`) |
| Microsoft Teams | Gate transport | Unverified (`teams-approval`) |
| Desk approval button | Gate | Unverified (`in-app-approval-button`); Desk v2 adds an Approvals page, see [Approvals](/desk/approvals) |
| Telegram | Notice | Unverified. No first-party Anthropic connector was found. The Bot API is HTTPS at `https://api.telegram.org/bot<token>/METHOD_NAME` ([docs](https://core.telegram.org/bots/api), fetched 2026-10-30); the token is a secret |
| ntfy | Notice | Unverified. HTTP POST to `ntfy.sh/<topic>`, where the topic acts as a password ([docs](https://docs.ntfy.sh/publish/), fetched 2026-10-30). Whether `ntfy.sh` is on a run's allowlist is not confirmed |

Connector messages appear as the person whose account the task belongs to, not as a bot ([docs](https://code.claude.com/docs/en/web-scheduled-tasks), fetched 2026-10-30).

### The reply rule

A reply counts only if all of these hold: it is not the parent message; it comes from the named approver; it is newer than the parent; and its first non-empty line above any quoted text, trimmed and lowercased, is exactly `approve <code>` or `reject <code>`. The latest such reply wins. Anything else keeps the gate waiting until its timeout. Replies are untrusted data: a run never follows other instructions in them.

Resume is a new run. A run sends the request, writes `pending/<id>`, and stops; a later scheduled run reads the thread and decides. Nothing waits in memory.

### Gmail's own approvals

Anthropic's article says Gmail send actions require approval by default, with Team and Enterprise owners deciding whether members can skip it ([support](https://support.claude.com/en/articles/10166901-use-google-workspace-connectors), fetched 2026-10-30). The RoutineOS pilot sent unattended without a policy change. Test a send from a report-only probe in your own account; a send that waits on a prompt stalls an unattended run.

### Configuration

```toml
# routine.toml
[gate]
enabled = true
transport = "email"
timeout_hours = 48
```

```toml
# app.toml
[budget]
max_runs_per_day = 1
alert_channel = "email"   # the one budget notice when the ceiling is passed; a notice only
```

Approver addresses are never in these files or in a pack. People add them on the Desk.

### Quiet hours and push preference

The Desk's `settings/workspace` doc records notification preferences and quiet hours. As of 0.6.0 nothing reads the push preference to send anything; it is stored for a later version. See [Notifications](/concepts/notifications).

## Worked example

Hiring Signals drafts an intro note for a strong match. The run writes an Inbox message of kind `approval` with a gate, and the gate goes out by email with a code. The approver replies `approve 7Q2K` (code illustrative) from their own address. A reply of "looks good!" does not count.

Watch the arithmetic. The gate times out after 48 hours, but the next scheduled run comes 72 hours later (Monday to Thursday) or 96 hours later (Thursday to Monday). `operate.md` says an item past its window "is expired and never acted on late". So, as the template ships, the next run finds every Hiring Signals gate already past its window. The template's first runs are report-only and write no gated messages, so this does not bite until the routine is switched live. Before that, either raise `timeout_hours` above the longest schedule gap (96 hours) or schedule runs closer together.

The routine sends no push and no Slack message. The only other notice it can send is the budget alert, if a run is refused by the daily cap.

## Common pitfalls

- **A Slack gate with the default tool policy.** No send tool is exposed; set "Ask for approval" on claude.ai.
- **Expecting a push reply to approve.** Push is one-way.
- **A Telegram bot token in the repo or prompt.** It is a secret. Use a network secret for `api.telegram.org`.
- **A gate shorter than the schedule gap.** A gate is checked only when the next run happens, and an expired item is never acted on late. Keep `timeout_hours` longer than the longest gap between runs.

## Related

- [Gates and approvals](/concepts/gates-and-approvals)
- [Notifications](/concepts/notifications)
- [Connectors and MCP](/infrastructure/connectors-and-mcp)
