Review pending outreach proposals
Every outbound email in Munin ships through a human-approved gate: curators file drafts as pending proposals (skill://outreach/draft-initial-email, skill://outreach/draft-reply-email, skill://outreach/draft-followup-email), and nothing leaves the org until an operator — or an admin agent acting on their explicit instruction — decides each one. This skill is that decision pass.
Approving sends. outreach_approve_proposal is not a status flip: for an initial proposal it creates the outbound conversation and sends the first email through the campaign's channel (appending the CTA link and unsubscribe footer per campaign settings); for a reply or followup it sends the draft verbatim on the existing conversation. There is no undo. Never approve in bulk without reading each draft.
Dismissing a follow-up stops the sequence. A dismissed followup permanently ends the campaign's follow-up sequence for that contact — no later step will be drafted. That makes dismiss the right call for "stop chasing this person" and the wrong call for "reword this". If the operator just dislikes the wording, edit the draft in the dashboard review drawer and approve the edited version instead.
In an MCP App host (Claude, Claude Desktop, …)
Call outreach_list_proposals({ "status": "pending" }). Hosts that support MCP Apps render the Munin Inspector panel (ui://munin/inspector) inline: one card per proposal with the contact, campaign, draft subject/body, and the curator's evidence, plus Approve & send and Dismiss buttons that call the decision tools directly. In these hosts the decision tools are panel-only (_meta.ui.visibility: ["app"]): they are hidden from you and only the operator's click can invoke them. Render the list and stop — the send decision is physically the human's.
Without a panel
In hosts without MCP Apps support the decision tools appear normally, and the same flow works as plain tool calls:
- List —
outreach_list_proposals({ "status": "pending" }). Each row carriesid,kind,draftSubject,draftBody,evidence,proposedSendAt, and nestedcontact/campaignsummaries. - Present each draft to the operator: who it goes to, which campaign, the subject, the full body, and anything notable in
evidence. Don't paraphrase the body — the operator is approving the literal text. - Decide one at a time on the operator's word:
outreach_approve_proposal({ "id": "..." })— sends immediately; the result carriesstatus: "sent",conversationId,sentMessageId.outreach_dismiss_proposal({ "id": "...", "reason": "..." })— no send; the reason lands on the proposal for the curator's next pass.
- Handle refusals cleanly. Both tools reject non-
pendingproposals (someone else may have decided it since listing — refresh rather than retry). Approval also rejects when the campaign was disabled or the contact became suppressed since drafting, and — forfollowupproposals — when the prospect replied after the draft was filed (dismiss it; the reply flow owns the conversation now). That is the suppression floor and stop-on-reply working, not an error to route around.
What not to do
- Never approve on your own initiative. A pending queue is not permission. The invariant that makes propose-only outreach safe is that a human read each draft.
- Don't edit-and-approve in one breath. If a draft needs changes, dismiss with a reason (or have the operator edit it in the dashboard) and let a fresh proposal be filed. Exception:
followupdrafts — dismissing one ends the sequence for that contact, so wording fixes go through the dashboard edit, never dismiss. - Don't loop approve over the whole list ("approve all") unless the operator explicitly reviewed every draft and said exactly that.