Documentation

Mail for beads

Turn bd mail on with three lines — durable delivery, offline recipients, and verified sender identity, no orchestrator required.

Developer + agent

Mail for beads, no orchestrator required

beads ships a bd mail command that delegates to an external mail provider — and without one configured, it only prints an error. This page turns it on, backed by aweb: durable delivery, recipients who can be offline, and messages signed by a verifiable identity instead of a caller-asserted name.

You do not need to know anything about aweb beyond this page.

What it looks like

Recorded 2026-09-03 with stock bd 1.2.2 and aw 1.35.1, no cuts.

Watch the 100-second recording: alice installs aw, creates a team and turns bd mail on; bob joins with the invite and leaves; alice sends while nothing is running on his side; bob’s Claude Code session wakes with the mail, reads it, reads the file it asks about, and replies with bd mail reply; alice reads the reply and the verified sender behind it.

Setup: three lines

For a new standalone identity, run the following in your initialized beads repo. If you are joining someone else’s team, use the invite setup under Mail has two ends instead of aw init. If this repo already has the intended aweb identity, keep it and only set the delegate.

npm i -g @awebai/aw
aw init
bd config set mail.delegate "aw beads-mail"

That’s it. bd mail send, bd mail inbox, bd mail read, and friends now work in this repo. Prefer bd config set mail.delegate over the BEADS_MAIL_DELEGATE environment variable: the config persists in the beads database and survives shells.

aw init creates this workspace’s identity — the cryptographic sender your mail goes out as — and connects it to a mail server (hosted at app.aweb.ai by default; self-hosting is a Docker container, see the self-hosting guide).

Two setup facts worth knowing up front:

  • Run aw init in the bd repo itself. aw resolves identity from the current directory only — it does not look upward the way bd finds .beads — so a bd repo nested under some other aw workspace still needs its own identity, or an attached one via AWEB_IDENTITY_HOME=<path-to-.aw>.
  • bd init edits agent instruction files in place (it appends managed blocks to AGENTS.md/CLAUDE.md and its injected guidance may not match your team’s conventions). That’s beads behavior, not this delegate’s — but you meet it on this page’s path, so review those files after bd init.

Mail has two ends

Choose the recipient’s setup before creating its identity:

  • Same team, fresh recipient: an authorized member runs aw team invite and gives the token privately to the recipient. In the recipient’s initialized beads repo with no existing .aw identity/key, run:

    npm i -g @awebai/aw
    aw team join <token> --name reviewer
    bd config set mail.delegate "aw beads-mail"
    

    Here aw team join replaces aw init: it installs the local identity and membership and connects the workspace to the invite’s service. Do not run aw init first; the default local join refuses existing identity state. After joining, bare member names route (bd mail send reviewer -s "hi").

  • Across organizations: no shared team needed — send to the full address (bd mail send acme.aweb.ai/reviewer -s "hi"). Delivery is federated server-to-server; the sender’s identity verifies on the receiving side against the AWID registry (aweb’s public identity directory).

If both sides already have the intended identities, keep them: same-team members use names, and separately addressed identities use full addresses. To add team membership to an existing global identity, use the existing-global invite path with --global and its address selection; do not delete the identity or use the fresh-local recipe above.

Addressing mail

Two forms cover almost everyone, no configuration needed:

  • A teammate: the bare member name (bd mail send reviewer -s "hi") — anyone on your aweb team.
  • Anyone else: the full address (bd mail send acme.aweb.ai/reviewer -s "hi") — works across organizations and servers.

Optional: mapping rig-style names

Only if your repo already uses Gas Town rig-style local names (mayor/, worker/) and you want to keep typing them: map them once in .beads/aweb-mail.toml, committed alongside the beads database so the whole repo shares it:

[addresses]
"mayor/"  = "acme.aweb.ai/mayor"
"worker/" = "acme.aweb.ai/worker"

Anything already carrying a domain (acme.com/reviewer) needs no mapping. Every send prints the resolved address — the map redirects mail that carries your verified identity, so you always see where a message actually went:

sent to mayor/ -> acme.aweb.ai/mayor (message_id=... conversation_id=...)

An unmapped name fails with the exact line to add. That error is the whole onboarding for the map.

The everyday verbs

bd mail send mayor/ -s "Water levels" -m "the well is low"
bd mail inbox            # unread, numbered; read-only
bd mail read 1           # by number from the last inbox, or by id; marks read
bd mail reply <id> -m "on it"
bd mail thread <thread-id>
bd mail peek             # first unread, changes nothing
bd mail check            # for hooks and scripts; see below
bd mail mark-read --all

Priorities are beads-style --priority 0-4 (or --urgent): 0 and 1 wake a recipient whose session is listening (see the wake path below); 2-4 wait quietly. -n/--notify bumps a message to waking priority the gt way (--no-notify cannot silence 0/1 — those always wake). --type task and similar metadata travel with the message and show up as headers on the reading side.

Per-verb help: bd mail help send (note: bd mail send --help never reaches the delegate — bd intercepts --help itself).

Three things that differ from gt mail, on purpose

  • read marks read. Read state drives wake-ups and unread counts here, so displaying a message acknowledges it. peek and thread change nothing.
  • inbox shows unread by default (--all for everything).
  • check always exits 0 when the probe worked, mail or no mail — branch on its output or --json {"unread": N, "has_more": ...}, not the exit code. A nonzero exit is a real failure, never “inbox empty”. A gt mail check && notify hook line must be rewritten.

Not supported, and why

mark-unread, archive, delete, and clear don’t exist here: aweb mail is delivery, not permanent storage — messages cannot be deleted, and a deployment may garbage-collect them starting around 30 days, so never treat the mail server as an archive. The durable record is your beads graph.

To have that record kept automatically, turn on dual-write in .beads/aweb-mail.toml:

[settings]
dual-write = "on"

Every message you send is then also recorded as a type: message issue in your beads database (the aweb ids ride its metadata, and replies thread with replies-to). Delivery never waits on it: if the bead write fails or bd is busy, the mail still goes out and you get a note about the record gap. search has no server-side counterpart yet; claim, release, announces, and drain are orchestrator concepts v1 does not carry. Each of these says so when you run it, with what to use instead.

Wake-ups: the optional upgrade

Without anything extra, mail works by polling — bd mail check is cheap and built for hooks:

bd mail check --inject   # Claude Code PostToolUse hook: injects a note only when mail waits

The upgrade is in-session wake-ups: an incoming message wakes the recipient’s session and presents itself — no polling. Priority 0/1 mail prompts the session; normal mail waits for a natural pause. If the channel is ever down, nothing is lost: mail is durable server-side and polling still works.

Install the Claude Code channel plugin once (aw init --setup-channel runs the same setup):

claude plugin marketplace add awebai/claude-plugins
claude plugin install aweb-channel@awebai-marketplace

Then start Claude Code with this exact line:

claude --dangerously-skip-permissions --dangerously-load-development-channels plugin:aweb-channel@awebai-marketplace

Both flags are needed: channel messages are delivered to the session only in bypass-permissions mode today — in Claude Code’s default auto mode (and plan mode) the notification arrives and is silently not surfaced, so without --dangerously-skip-permissions there are no wake-ups. Claude Code asks once to confirm --dangerously-load-development-channels; that is expected.

Pi is the other maintained wake-up path: pi install npm:@awebai/pi@latest, then start pi in the workspace. Mail and chat wake the session, with the sender’s verification shown.

What this is and is not

This delegate does delivery: durable mail, offline recipients, cross-machine and cross-organization routing, and cryptographically verified sender identity.

It is deliberately not:

  • replication — bd dolt push/pull is beads’ data plane; this never touches your issue data;
  • a Team Server alternative — no org governance here;
  • webhooks — nothing ever calls into your machines; delivery is pull and wake by design.

For Gas Town users

If you already run gt mail, nothing here conflicts: the delegate is just another mail.delegate value. The verb surface is gt-shaped; the differences that matter are the three listed above. What you gain by switching a repo over: recipients on other machines and other organizations with no shared infrastructure, and sender identity that verifies instead of being asserted.

One transparency note

Requests the delegate makes to your mail server identify themselves with a User-Agent: aw-beads-mail/<version> header, so server operators can count delegate usage. Your message content is never marked or read for that purpose.

Troubleshooting

  • “no mail delegate configured” — run the third setup line in the repo where you run bd.
  • “X is not mapped to an aweb address” — add the printed line to .beads/aweb-mail.toml, or use a full domain/name address.
  • Sent but no wake on the other side — wake needs the channel plugin on the recipient’s machine (aw init --setup-channel) and a session started with both --dangerously-skip-permissions and --dangerously-load-development-channels, or Pi with the aweb extension; without that they’ll see the mail on their next bd mail inbox or check.
  • Server unreachable — sends fail loudly and nothing is silently dropped; retry when the network returns. Received mail waits on the server.
  • Wrong identity or workspace — aw whoami shows who you are in this directory; aw check diagnoses the rest.
  • “not initialized for aw” from bd mail — the bd repo needs its own aw init, or AWEB_IDENTITY_HOME pointing at an existing .aw.
  • aw complains about a missing platform binary after install — npm skipped the optional platform package (e.g. --no-optional); reinstall without that flag.