docs blog library
github

Chat platforms

Reach an agent from your phone or team channel. It can inspect files, work with git, research questions, and ask for approval before taking actions on the machine hosting it.

PlatformWhat you needSetup
TelegramBot token, allowlisted chat id, DockerTelegram guide
DiscordBot token, allowed users/channels/server, DockerDiscord guide
iMessage, hostedPhoton project and allowed handles; Linux or macOSHosted-line guide
iMessage, localAwake Mac on macOS 14+, signed into MessagesLocal-Mac guide
WhatsAppLinked account, allowed numbers, running bridgeWhatsApp guide
Slack, Google Chat, your own appA custom adapterCustom integrations

Each supported bridge keeps conversation history and accepts /model, /persona, /remind, /status, and /stop. Configure a model provider before starting one.

Try a useful request

Once connected, send a request appropriate to the files and integrations available on its host:

Review today's commits and tell me what needs attention.
Summarize the PDF I attached and list the decisions I need to make.
/remind 30m Check the deployment

Email, calendar, and web search need their own integration setup. A Docker bridge sees its container’s files and mounts; it does not automatically see your laptop’s checkout.

Sending more while it works

You don’t have to wait for an answer before adding to your request. A message you send while the agent is working goes into the run in progress, and the agent reads it before its next step. “Book a table for tomorrow” followed by “for 4, around 8pm” gets one answer that covers both, not two separate runs.

Photos, audio and other supported media sent while the agent works are attached to that follow-up, using the same model support and file size limits as a message sent while idle. If the model cannot receive a file’s media type, it receives an explanation instead.

Commands such as /model and messages from other people in a group wait until the current answer arrives. Several messages that waited are answered together, in one turn. /stop drops anything still waiting.


Telegram (shipped)

Follow the Telegram setup to create a bot, set its token and allowed chat ids, and start the container. Message the bot directly; it shows tool activity and reasoning while the run works.

You want toSend or choose
Change model/model for a picker, or /model provider/model after configuring that provider
Change behavior/persona
Stop a run/stop or the stop button
Approve a toolAccept or reject its approval message; batch buttons answer several at once
Set a reminder/remind 30m check the deploy

Each chat has its own agent settings and conversation. In the default Linux container setup, chat agents run under separate Unix users. Provider keys still bill the operator’s accounts. See per-chat isolation.


Discord (shipped)

Follow the Discord setup, including Message Content Intent and the allowlist. Mention the bot in an allowed channel to start a thread; follow-ups in that thread need no mention. DMs require your user id to be allowed.

When it asks for an answer or approval, the person who started the run can reply in the same channel without another mention. Ordinary mention requirements resume afterward.

Use /model for the current provider’s model picker. To change providers, send /model provider/model as a normal message after adding that provider’s credential.

The default Linux container isolates conversations under separate Unix users. See per-conversation isolation.


iMessage and WhatsApp (shipped)

jazz imessage             # hosted line through Photon; no Mac required
jazz imessage --local     # your Apple account; macOS 14+
jazz whatsapp     # links as a device, the way WhatsApp Web does

Setup walkthrough: Reaching your agent from a chat app. Full tables: packages/imessage-bot/README.md, packages/whatsapp-bot/README.md.

ConcernWhat changes
Where it runsLocal iMessage needs an awake, signed-in Mac. Photon can run anywhere but carries messages through a third party. WhatsApp needs its linked-device process alive.
No buttonsApprovals arrive as numbered options answered by replying 1 or 2. A reply matching nothing is treated as an ordinary message, so an ignored prompt never swallows the next question.
No live progressiMessage cannot edit a sent message: 🤔 Working…, a “still working” line at 45s and roughly once a minute after, then the answer. No status bubble to update.
Stopping a runSend /stop. It is answered at once, like /status, /help and /new, rather than waiting behind the run it would stop.
FormattingWhatsApp gets the model’s Markdown in its own dialect (*bold*, _italic_, ~strike~, code blocks); Messages gets plain text with the marks dropped. Math becomes Unicode on both (see below).
Whose accountiMessage answers as you, so a chat with yourself needs a trigger word to tell questions from the bridge’s own replies. Photon gives the agent a line of its own instead: packages/photon-bot/.
Allow-list shapeHandles (E.164 or Apple ID) and group rowids for iMessage; numbers and group JIDs for WhatsApp. Both deny-by-default; being allowed to DM never admits you to a group.
IsolationPer-conversation uid sandboxing is a Linux mechanism the containerised bridges use. On a Mac or a linked device every chat shares one JAZZ_HOME and runs as your user.
SanctioningWhatsApp publishes no API for personal accounts; the bridge speaks the WhatsApp Web protocol via Baileys. Unusual behaviour can get a number limited or banned. Use a dedicated one if it matters.

Secrets the agent asks for (ask_user_secret, a PDF password, say) are collected only in a one-to-one chat with the bot: your next message is the value, it goes to the run over its private pipe and is never forwarded as a turn, and the agent sees only [redacted:<name>]. Telegram deletes your message once it is read; Discord, WhatsApp and iMessage bots cannot, so the prompt asks you to delete it. In a group, a server channel or a Photon space the secret is not collected, and the chat is told to take it to a private chat. See Secrets the person types.


Math

Chat apps cannot typeset LaTeX, so a formula in a reply is rewritten as Unicode text before it is sent: $x_i^2 + \alpha$ reads xᵢ² + α. This applies to Telegram, Discord, WhatsApp and iMessage. A block that spans lines (a matrix, aligned equations) goes in a code block so its columns line up. Formulas inside code are left as written.

Slack, Google Chat

Jazz does not ship adapters for Slack or Google Chat. A custom adapter must handle the platform’s authentication, conversation IDs, formatting, and acknowledgement deadlines:

ConcernTelegramDiscordSlackGoogle Chat
InboundgetUpdates long-poll or webhookGateway websocketEvents API webhook (or Socket Mode)Chat app webhook
Conversation keychat_idDM channel id, or thread idchannel + thread_tsspace + thread name
Reply formattingMarkdown / HTMLMarkdown (close to standard)mrkdwn (*bold*, no # headings)app card or plain text
Live progressedit the status messageedit the status messagechat.update on a placeholderupdate the card
Ack deadlinenone3 s for interactions3 s: ack, then reply async30 s
Authorizationchat-id allowlistuser / channel / guild allowlist + mention-gatingverify signing secret, then allowlistverify bearer token

Two things to get right, both platform-side:

  1. Ack fast, answer later. Slack will retry a webhook you don’t ack within 3 seconds, and an agent run takes longer than that. Ack immediately, run jazz run in the background, and post the answer as a follow-up. Retries are also why you should de-duplicate on the platform’s event id: otherwise a slow run gets billed twice.
  2. Translate the markdown. jazz run without --json gives you raw markdown precisely so you can convert it. Slack’s mrkdwn in particular is not markdown.

Sending yourself a message

Use a bridge’s notify.sh script to send a message from a shell or scheduled job:

~/jazz/packages/telegram-bot/src/notify.sh "backup finished, 41 GB, no errors"
~/jazz/packages/discord-bot/src/notify.sh "nightly update rolled back, needs a look"

It reads the same .env the bridge runs on and posts to the first allowed chat (TELEGRAM_ALLOWED_CHAT_IDS, or DISCORD_ALLOWED_CHANNEL_IDS).

The script sends directly through the platform API without starting an agent run or calling a model.

To notify after a job:

./long-job.sh && notify.sh "long-job: done" || notify.sh "long-job: FAILED ($?)"

The script exits non-zero and explains itself if the credentials are missing, so a caller can note that without failing whatever it was doing:

notify.sh "..." || echo "(notify failed)"

Security for chat surfaces

Restrict who can reach the bot and what their runs may do.

  • Always use an allowlist. Both bridges and Jazz have one; use both.
  • Default to low-risk. At high-risk, a message (or a prompt injection inside a web page the agent fetched) can run arbitrary commands on the host.
  • Know what “yolo” costs. Every bridge’s /mode yolo is high-risk for that conversation, and it is sticky: it survives /new and bridge restarts until someone sets it back to safe. Only an operator can turn it on (TELEGRAM_OPERATOR_IDS, DISCORD_OPERATOR_IDS, WHATSAPP_OPERATOR_NUMBERS, IMESSAGE_OPERATOR_HANDLES, PHOTON_OPERATOR_HANDLES; the iMessage account owner always is one), and the same goes for Telegram’s “Always allow”. Anyone admitted can turn it back to safe. With no operators configured, nobody can turn approvals off from chat.
  • An approval belongs to whoever asked. In a group, only the person whose message started a run can approve, reject or answer its prompts; a tap or a typed 1 from anyone else does nothing. Cancelling is theirs too, or an operator’s.
  • A run gets an allowlisted environment. The bridge passes an agent run its PATH, locale and time zone, proxies, the provider keys and endpoints Jazz reads, and Jazz’s own switches. The bot token, webhook secret, WhatsApp and Photon credentials, the daemon token, and anything else in the bridge’s environment are left out, so an agent that runs env cannot hand the bot to whoever asked. A message and an incognito transcript reach the run on stdin, never on the command line, where every account on the host could read them.
  • Deny dangerous tools explicitly. tools is additive, so omitting execute_command does not remove it. Put exact names such as execute_command in the agent’s deniedTools; Jazz applies denials after every grant.
  • Treat the history volume as sensitive. Transcripts are plaintext JSON under ~/.jazz/history/.
  • Allowlisting is not isolation. Two people on the same allowlist share a host, and a Jazz agent has read_file and execute_command, so without an OS boundary either one’s agent can read the other’s transcripts, memory and stored credentials. Both bridges give each conversation its own uid and Jazz home for exactly this: Telegram, Discord. It matters most where the allowlist is a guild, since that admits everyone in it. Anything you build yourself needs the same, or a one-person allowlist.
  • A container is not a boundary against the host. Root, sudo, and the docker group all read a bridge’s volume whatever its uids and file modes say: the daemon runs as root, and the docker group is root-equivalent. On a machine other people administer, treat everything the bot has stored as readable by every admin on it.
  • Check health. Telegram and Discord expose /health for their container health checks. It returns 503 when the platform connection fails. For the other bridges, set JAZZ_BRIDGE_HEALTH_PORT. A restart stops active runs; waiting for your approval does not count against the run timeout.
  • Cap concurrency. JAZZ_BOT_MAX_CONCURRENT_RUNS (default 4) bounds how many agent runs one bridge has in flight across every chat, and JAZZ_BOT_MAX_QUEUED_MESSAGES (default 5) how many messages may wait behind one chat’s run. A requester’s own messages go into their run and don’t count.
  • Cap spend. Use costKnown as well as costUSD. The bridges pause subsequent requests after an unpriced run; no dollar cap can guarantee the cost of that first unpriced request.

See the security model.


Failed reminder deliveries remain queued and are retried.

machine-readable: /docs/surfaces/chat.md · /llms.txt · /llms-full.txt