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.
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.
Quick Start
- One command (CLI)
The quickest way to run Signal — no Python needed:
bash praisonai bot run --platform signal \ --account +15551234567 \ --bridge-url http://bridge:8080 \ --agent agents.yamlThe
--account/--bridge-urlflags are generated from Signal’sChannelDescriptor.config_fields.bot runstarts any registered platform by name. Seepraisonai bot runfor the full flag list. - Run the bridge
Signal links a device through the signal-cli-rest-api bridge. Start it with Docker in
nativemode.bash 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 - 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 - Start the bot
Point the bot at your linked account and the bridge.
python 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.
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.
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 agentThe 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 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.
YAML
# bot.yaml
channels:
signal:
account: "+15551234567"
bridge_url: "http://localhost:8080"
allowed_users: ["+15557654321"]from praisonai_bot.bots import BotOS
BotOS(agent=agent, platforms=["telegram", "signal"]).start()Environment
export SIGNAL_ACCOUNT="+15551234567"
export SIGNAL_BRIDGE_URL="http://localhost:8080"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())Python SDK
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) |
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:
@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.
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. |
| Webhooks | Poll-based; accepts_webhooks=False |
Troubleshooting
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().
ImportError: aiohttp is required
The Signal adapter uses aiohttp (lazy-imported). Install it:
pip install aiohttpNo 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.
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.
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
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.
Use the allowlist for DMs
Set allowed_users so only trusted numbers can start a conversation. Numbers normalise to digits, so any formatting works.
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.
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.
channels:
signal:
platform: signal
account: ${SIGNAL_ACCOUNT}
bridge_url: ${SIGNAL_BRIDGE_URL}
stt:
enabled: true
language: enThe DM allowlist runs before the bridge download, so rejected senders never trigger a transcription request. See Voice Notes 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.
Related
The closest sibling — linked-device / local-bridge model
Channels GatewayRun Signal alongside Telegram, Discord, Slack, and more
Messaging BotsAll supported messaging platforms
Bot GatewayMulti-channel gateway architecture
Voice NotesTranscribe inbound Signal voice messages via the bridge
CardSelf-healing gateway channels — how Signal integrates with the health monitor