> ## Documentation Index
> Fetch the complete documentation index at: https://praison.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Signal Bot

> Connect your AI agent to Signal via a local signal-cli-rest-api bridge — end-to-end encrypted, no cloud tokens

Connect your AI agent to Signal — end-to-end encrypted, linked through a local `signal-cli-rest-api` bridge, no cloud bot token needed.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonai_bot.bots import SignalBot

agent = Agent(name="assistant", instructions="Be helpful on Signal.", llm="gpt-4o-mini")
bot = SignalBot(account="+15551234567", bridge_url="http://localhost:8080", agent=agent)

import asyncio
asyncio.run(bot.start())
```

The user messages you on Signal; the bridge relays the chat to the agent and the agent replies in the same thread.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    User[📱 Signal User] --> Signal[🔒 Signal Network]
    Signal --> Bridge[📡 signal-cli-rest-api]
    Bridge --> Bot[🤖 SignalBot]
    Bot --> Agent[🧠 AI Agent]
    Agent --> Bot
    Bot --> Bridge

    classDef user fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef process fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff

    class User user
    class Signal,Bridge,Bot process
    class Agent agent
```

## Quick Start

**1. One command (CLI)**

The quickest way to run Signal — no Python needed:

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
praisonai bot run --platform signal \
    --account +15551234567 \
    --bridge-url http://bridge:8080 \
    --agent agents.yaml
```

The `--account` / `--bridge-url` flags are generated from Signal's `ChannelDescriptor.config_fields`. `bot run` starts any registered platform by name. See [`praisonai bot run`](https://praison.ai/docs/features/messaging-bots#registered-but-verb-less-platforms-praisonai-bot-run) for the full flag list.

**2. Run the bridge**

Signal links a device through the [signal-cli-rest-api](https://github.com/bbernhard/signal-cli-rest-api) bridge. Start it with Docker in `native` mode.

```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
docker run -d --name signal-api -p 8080:8080 \
  -v ./signal-data:/home/.local/share/signal-cli \
  -e MODE=native bbernhard/signal-cli-rest-api
```

**3. Link your device**

Open the bridge link URL, then scan the QR from Signal → **Settings → Linked Devices → Link New Device**.

```
http://localhost:8080/v1/qrcodelink?device_name=praisonai
```

**4. Start the bot**

Point the bot at your linked account and the bridge.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonai_bot.bots import SignalBot

agent = Agent(name="assistant", instructions="Be helpful on Signal.", llm="gpt-4o-mini")
bot = SignalBot(account="+15551234567", bridge_url="http://localhost:8080", agent=agent)

import asyncio
asyncio.run(bot.start())
```

Message your linked Signal number and the agent replies.

> [!NOTE]
>
> No token needed — Signal links a device via the local bridge. The `token` constructor argument exists only for adapter-wiring parity and is unused.

> [!NOTE]
>
> Channel-specific extras (`account`, `bridge_url`) declared under a `bot.yaml` channel are now forwarded correctly through `praisonai bot start` as well — previously the `start` path dropped them.

***

## How It Works

Signal is poll-based: the bot fetches inbound envelopes from the bridge, routes text to the agent, and sends the reply back through the bridge.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant User as 📱 Signal User
    participant Bridge as 📡 Bridge
    participant Bot as 🤖 SignalBot
    participant Agent as 🧠 Agent

    User->>Bridge: Send message
    loop Every poll_interval
        Bot->>Bridge: GET /v1/receive/{account}
        Bridge-->>Bot: Envelopes (DMs + groups)
    end
    Bot->>Agent: chat(content)
    Agent-->>Bot: Response
    Bot->>Bridge: POST /v2/send
    Bridge-->>User: Reply

    classDef user fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef process fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef agent fill:#10B981,stroke:#7C90A0,color:#fff

    class User user
    class Bridge,Bot process
    class Agent agent
```

The adapter talks HTTP/JSON to these bridge endpoints:

| Endpoint | Purpose |
| - | - |
| `GET /v1/receive/{account}` | Poll inbound envelopes (DMs + groups) |
| `POST /v2/send` | Send text |
| `PUT /v1/typing-indicator/{account}` | Typing indicator (best-effort) |
| `GET /v1/accounts` | Bridge reachability + linkage check (`probe()`) |

**Inbound liveness.** A delivered envelope refreshes the Signal channel's `last_activity` for the shared [Channel Supervision](https://praison.ai/docs/features/gateway-channel-supervision.md#proactive-health-monitoring) health monitor. Successfully-empty polls do **not** refresh it, so a silently half-open bridge (returns `[]` forever without raising) trips `stale-socket` after `stale_after` seconds and the supervisor reconnects — instead of the bot going quietly deaf.

***

## Configuration

Signal needs an account and a bridge URL. Configure it with YAML, environment variables, or the Python SDK.

> [tab] YAML
>
> ```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> # bot.yaml
> channels:
>   signal:
>     account: "+15551234567"
>     bridge_url: "http://localhost:8080"
>     allowed_users: ["+15557654321"]
> ```
>
> ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> from praisonai_bot.bots import BotOS
>
> BotOS(agent=agent, platforms=["telegram", "signal"]).start()
> ```

> [tab] Environment
>
> ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> export SIGNAL_ACCOUNT="+15551234567"
> export SIGNAL_BRIDGE_URL="http://localhost:8080"
> ```
>
> ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> from praisonaiagents import Agent
> from praisonai_bot.bots import SignalBot
>
> agent = Agent(name="assistant", instructions="Be helpful.", llm="gpt-4o-mini")
> bot = SignalBot(agent=agent)  # reads SIGNAL_ACCOUNT / SIGNAL_BRIDGE_URL
>
> import asyncio
> asyncio.run(bot.start())
> ```

> [tab] Python SDK
>
> ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> from praisonaiagents import Agent
> from praisonai_bot.bots import SignalBot
>
> agent = Agent(name="assistant", instructions="Be helpful.", llm="gpt-4o-mini")
>
> bot = SignalBot(
>     account="+15551234567",
>     bridge_url="http://localhost:8080",
>     agent=agent,
>     allowed_users=["+15557654321"],
>     poll_interval=1.0,
> )
>
> import asyncio
> asyncio.run(bot.start())
> ```

### Config keys

| Field | Env var | Required | Default | Description |
| - | - | :-: | - | - |
| `account` | `SIGNAL_ACCOUNT` | ✅ | — | Linked Signal phone number, e.g. `+15551234567` |
| `bridge_url` | `SIGNAL_BRIDGE_URL` | ❌ | `http://localhost:8080` | Base URL of the running `signal-cli-rest-api` bridge |

### Constructor parameters

| Parameter | Type | Default | Description |
| - | - | - | - |
| `token` | `str` | `""` | Accepted for adapter-wiring parity; **unused** (Signal is token-free) |
| `account` | `str` | `""` → `SIGNAL_ACCOUNT` | Linked Signal number. Required at `start()`; raises `ValueError` if empty |
| `bridge_url` | `str` | `""` → env → `http://localhost:8080` | Bridge base URL; trailing slash stripped |
| `agent` | `Optional[Agent]` | `None` | The PraisonAI agent to route messages to |
| `config` | `Optional[BotConfig]` | `BotConfig(token="", mode="polling")` | Shared bot config |
| `allowed_users` | `Optional[List[str]]` | `None` | DM allowlist. Numbers normalise to digits so `+1 555…` and `1555…` match |
| `poll_interval` | `float` | `1.0` | Seconds between `/v1/receive` polls; clamped to a minimum of `0.2` |

***

## Capabilities & Limits

Signal declares honest capabilities so shared engines degrade correctly.

| Capability | Value |
| - | :-: |
| `max_message_length` | `2000` |
| `supports_edit` | `False` (streaming falls back to chunked sends) |
| `supports_typing` | `True` |
| `needs_rate_limit` | `True` |
| `accepts_webhooks` | `False` (poll-based) |
| `supports_media` | `False` (text-only replies; inbound voice notes are transcribed — see [Inbound voice notes](#inbound-voice-notes)) |

***

## Built-in Commands

Every Signal channel ships these commands out of the box.

| Command | Description |
| - | - |
| `/status` | Show bot status and info |
| `/new` | Reset conversation session |
| `/help` | Show help + custom commands |
| `/stop` | Cancel the current agent run |

Register custom commands:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
@bot.on_command("ping")
async def ping(msg):
    return "Pong!"
```

***

## DM Allowlist

Restrict who can DM the bot with `allowed_users` — phone numbers are normalised to digits so formatting never blocks a match.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
bot = SignalBot(
    account="+15551234567",
    agent=agent,
    allowed_users=["+1 555 765 4321", "15559876543"],
)
```

Groups pass through the allowlist — **group membership is the gate there**, not the DM list.

***

## What's Not Supported Yet

This release ships text messaging plus inbound voice-note transcription. The following are intentionally not wired:

| Feature | Behaviour |
| - | - |
| `edit_message()` | Sends a new message (Signal has no edit) |
| `delete_message()` | Returns `False` (bridge doesn't expose it) |
| `add_reaction()` / `remove_reaction()` | Return `False` (not wired) |
| Media in/out | `supports_media=False` for outbound (text only). **Inbound audio is transcribed** via the bridge — see [Voice Notes](https://praison.ai/docs/features/gateway-voice-notes.md). |
| Webhooks | Poll-based; `accepts_webhooks=False` |

***

## Troubleshooting

> [accordion] ValueError: Signal account is required
>
> The account is empty. Set it via the constructor, `channels.signal.account`, or the `SIGNAL_ACCOUNT` environment variable before calling `start()`.

> [accordion] ImportError: aiohttp is required
>
> The Signal adapter uses `aiohttp` (lazy-imported). Install it:
>
> ```bash theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
> pip install aiohttp
> ```

> [accordion] No messages arrive
>
> Confirm the bridge is linked and reachable. Check `GET /v1/accounts` returns your number, and lower `poll_interval` (minimum `0.2s`) for faster pickup. Verify the bridge runs in `native` or `json-rpc` mode so `/v1/receive` returns envelopes.

> [accordion] Bot reports HEALTHY but hasn't received a message in hours
>
> The `signal-cli-rest-api` bridge can go silently half-open — the HTTP call succeeds but the bridge returns `[]` forever. The gateway health monitor treats time since the last **delivered** envelope as the liveness signal (empty polls don't count), so the channel will trip `stale-socket` after `stale_after` seconds (default 120s) and the supervisor will reconnect automatically. Confirm via `GET /health` that `last_activity` for the Signal channel is advancing on real inbound messages, and consider lowering `stale_after` on very-chatty accounts or raising it on quiet ones. See [Channel Supervision → Proactive Health Monitoring](https://praison.ai/docs/features/gateway-channel-supervision.md#proactive-health-monitoring).

> [accordion] Bridge unreachable on probe
>
> `probe()` calls `GET /v1/accounts`. A non-200 means the bridge is down or the URL is wrong. Ensure `bridge_url` matches the Docker port mapping (default `http://localhost:8080`).

***

## Best Practices

> [accordion] Keep the bridge private
>
> The `signal-cli-rest-api` bridge holds your linked device. Run it on localhost or a private network — never expose port 8080 to the public internet.

> [accordion] Use the allowlist for DMs
>
> Set `allowed_users` so only trusted numbers can start a conversation. Numbers normalise to digits, so any formatting works.

> [accordion] Tune the poll interval
>
> `poll_interval` trades latency for load. Keep the default `1.0s` for most bots; lower it toward `0.2s` for snappier replies, raise it to reduce bridge traffic.

> [accordion] Expect plain text
>
> Signal has no message editing and replies are text-only. Design replies as self-contained plain-text messages rather than relying on edits or rich media.

***

## Inbound voice notes

Signal voice notes are transcribed automatically — the bot fetches the audio attachment from the `signal-cli-rest-api` bridge and the agent receives the transcript as plain text. STT is on by default; tune or opt out with the shared `stt:` block.

```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
channels:
  signal:
    platform: signal
    account: ${SIGNAL_ACCOUNT}
    bridge_url: ${SIGNAL_BRIDGE_URL}
    stt:
      enabled: true
      language: en
```

The DM allowlist runs **before** the bridge download, so rejected senders never trigger a transcription request. See [Voice Notes](https://praison.ai/docs/features/gateway-voice-notes.md) for the full STT configuration.

***

## Standalone scheduled delivery (no gateway)

You do not need a running Signal Bot process to deliver a scheduled brief to Signal. Point `SIGNAL_ACCOUNT` at your linked sender number (and, if the bridge is not on `http://localhost:8080`, set `SIGNAL_BRIDGE_URL`). From OS cron / CI, `praisonai schedule tick` will POST to `{SIGNAL_BRIDGE_URL}/v2/send` using an explicit `deliver: signal:<recipient>` target or `SIGNAL_HOME_CHANNEL`. The bridge must be reachable from wherever the tick fires. See [Out-of-process delivery](https://praison.ai/docs/features/scheduler-delivery.md#out-of-process-delivery).

***

## Related

> [card] [WhatsApp Bot](https://praison.ai/docs/features/whatsapp-bot.md)
>
> The closest sibling — linked-device / local-bridge model

> [card] [Channels Gateway](https://praison.ai/docs/features/channels-gateway.md)
>
> Run Signal alongside Telegram, Discord, Slack, and more

> [card] [Messaging Bots](https://praison.ai/docs/features/messaging-bots.md)
>
> All supported messaging platforms

> [card] [Bot Gateway](https://praison.ai/docs/features/bot-gateway.md)
>
> Multi-channel gateway architecture

> [card] [Voice Notes](https://praison.ai/docs/features/gateway-voice-notes.md)
>
> Transcribe inbound Signal voice messages via the bridge

> [card]
>
> Self-healing gateway channels — how Signal integrates with the health monitor
>
> [Open](https://praison.ai/docs/features/gateway-channel-supervision.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.