send-mail
Skill17 starsChanged 3 months agoMITView on GitHub
Send cold outreach with OpenOutSend — pipe leads in with `outfind find --json | outsend`, then run `outsend send` to mail them under the built-in guards. Use when the user wants to send, follow up on, or check the status of an outreach campaign that already has leads. Never send unasked — this puts mail in strangers' inboxes under the user's own identity.
- Reads credentials
- Installs packages
1 other project uses a version of this.
Adopted by
Most recently taken up by
- ecator/my-agent-skillstheir own version, first seen Apr 6, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 1
- ecator/my-agent-skills★ 1same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
The file
We split this skill into the rules it gives an agent
10 rules. About 0 are common, 2 are distinctive, and 8 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
8
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 18
Never send unasked: Every send puts mail in a stranger's inbox under the user's own identity and mailbox.
- Very specific: line 39
If the command is missing, run it through uvx instead — uvx openoutsend check — or install it with pip install openoutsend.
- Distinctive: line 42
Never accept anything on the user's behalf: If a required variable is unset, say so and let the user set it.
- Very specific: line 97
If you (the calling agent) are already reasoning about this lead in the conversation, OUTSEND_LLM_API_KEY is a second, redundant LLM bill …
- Very specific: line 128
A reply comes before any step: When a lead has written back, the draft_pending is theirs, with "step": "reply", their line's own fields as …
- Distinctive: line 171
Every other failure is plain text naming what is wrong (a missing variable, a rejected model key, a mailbox that failed its SMTP login).
- Very specific: line 179
Don't invent a daemon or a watch loop — a pass reads what the guards allow *right now* and exits.
- Very specific: line 181
Don't pass --agent-draft unless you plan to answer every draft_pending it raises.
- Very specific: line 182
Don't parse stderr for data — there is none here to parse.
- Very specific: line 183
Don't try to remove or word around the Sent with OpenOutreach attribution line — it is always on, by design (see the repo's Legal notice), …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Sending mail with OpenOutSend
OpenOutSend is a self-hosted CLI sender — the right-hand side of a pipe. It stores leads a finder piped in, writes the opener with an LLM, and sends from a connected mailbox under three guards it owns: measured warm capacity, per-box send pacing, and the operator's own sending window.
It finds nobody. Leads arrive already qualified from OpenOutFind
(or OpenOutreach, which bundles both). If the user wants
leads, that is a different tool — use the find-leads skill instead.
Never send unasked. Every send puts mail in a stranger's inbox under the user's own identity and
mailbox. Checking status, ingesting leads and running check are safe to do proactively; send is
not — only run it when the user has asked to send, follow up, or continue a campaign.
Is it installed, and is it ready?
outsend check # verifies the environment can send: message, model, operator, mailboxcheck reads only OUTSEND_* from the environment — there is no config row and nothing is asked
interactively. It fails cheaply with one line naming every variable still missing, grouped as what
you sell (OUTSEND_PRODUCT_DOCS/OUTSEND_CAMPAIGN_TARGET), the model
(OUTSEND_AI_MODEL/OUTSEND_LLM_API_KEY), who signs the mail
(OUTSEND_OPERATOR_NAME/OUTSEND_OPERATOR_EMAIL), and the mailbox
(OUTSEND_MAILBOX_ADDRESS/OUTSEND_MAILBOX_PASSWORD, an app password, not the login password).
With OUTSEND_TRANSPORT=resend the mailbox is OUTSEND_MAILBOX_ADDRESS/OUTSEND_RESEND_API_KEY
instead — a full-access key — and every piped line must carry an unsubscribe_url.
send runs the same check implicitly before any mail moves, so check is for diagnosing why
nothing sent, not a required first step.
If the command is missing, run it through uvx instead — uvx openoutsend check — or install it
with pip install openoutsend.
Never accept anything on the user's behalf. If a required variable is unset, say so and let the user set it; do not export credentials or invent a product description for them.
Getting leads in
outfind find 50 --json | outsendoutsend with no verb reads JSON Lines on stdin, stores each row keyed on lead_id within its
sequence (the built-in cold one unless --sequence DIR names a folder of step files), checks every
address against the suppression list at the door, and exits. It transmits nothing at that moment —
ingesting and sending are separate invocations, because a pipe's right-hand side must not block on
the network while a producer is still writing.
- Idempotent, keyed on
lead_idand the sequence. Running the same pipe twice is a correction (latest field wins), never a duplicate — re-piping after a partial failure is always safe. The same lead piped into two sequences is two deals. - A malformed line is skipped and counted, named on stderr, non-zero exit — the rows behind it cost the finder real money, so the batch is never aborted over one bad line.
- A blank
emailis stored, not rejected. An address is an enrichment a later re-ingest can fill in for free.
Sending
outsend send # one pass: read the mail, answer replies, open what the guards allow now
outsend send 5 # keep passing until 5 conversations are open, waiting out the send clocks
outsend send all # until nobody is left to email
outsend send --sequence DIR # the same, narrowed to the sequence in DIRA bare pass sends every sequence the install holds, the built-in cold one among them, in one
queue, at most one message per address. --sequence DIR narrows it to that folder. Replies are
answered in any pass, whatever sequence the thread began in. examples/sequences/after-a-demo/ in
the repo is a whole sequence to copy.
A pass, in order: read the mailbox (an opt-out overnight suppresses someone before anything is written to them), answer every thread the lead replied in, then send as many openers, later steps and follow-ups as the guards allow right now — one queue, oldest due first, so a backlog of threads can never hold new leads at zero. Replies ignore the daily cap, the spacing clock and the sending window; every step and follow-up is cold volume, and they share one capped, spaced budget per box — except a Resend box's cap, which each sequence spends on its own.
outsend send 5 waits — sleeping out the spacing clock, the daily ceiling and the sending window
between passes, including overnight or across a weekend if that is what the guards say. outsend send
all makes the pool itself the goal and exits 0 once nobody is left, even though the same drained pool
would report send 5 as falling short at three of five — it is the same ending with one verdict
inverted, so prefer all to typing a count that goes stale as you type it.
A lead who never answers gets two more emails, then the pursuit ends (after three working days, then five); a reply at any point ends the sequence and the conversation takes over instead.
Writing the openers and steps yourself, with no second LLM key
If you (the calling agent) are already reasoning about this lead in the conversation,
OUTSEND_LLM_API_KEY is a second, redundant LLM bill for emails you can write yourself. Add
--agent-draft and the pass stops at the deal whose opener or next sequence step has been due
longest, instead of calling AI_MODEL — and in this mode check/send never require
OUTSEND_AI_MODEL/OUTSEND_LLM_API_KEY at all. Follow-ups still go to the model, so pair it with
OUTSEND_FOLLOW_UPS=off when none is configured:
outsend --json send --agent-draftIt exits non-zero with draft_pending. Under --json the error object on stderr carries the deal's
own fields and what to write: {"error": {"type": "draft_pending", "lead_id", "full_name", "title",
"company", "profile_text", "step", "subject", "prompt"}} — prompt is the step file rendered for
this deal, exactly what AI_MODEL would have been asked, and subject is the thread's for a later
step. Write what the prompt asks, then re-run the same command with your answer attached — a
subject and body for an opener, a body alone for a later step, which goes in the opener's thread
under its subject:
outsend send --agent-draft --subject "Quick one" --body "Saw you're hiring for platform roles..."
outsend send --agent-draft --body "One more thought on the list..."This resumes exactly that deal — no id to track, since at most one is ever pending — and sends it the
moment a mailbox is free. An answer already given is kept, not thrown away, if the spacing clock
or the sending window is holding every box right now; a later bare outsend send --agent-draft (no
--subject/--body) will pick it back up and send it once a box frees up, so you never have to
re-supply the same text. A deal stopped or replied to while its draft waited is dropped, not written
to. No count yet — --agent-draft is one pass at a time, like a bare outsend send.
A reply comes before any step. When a lead has written back, the draft_pending is theirs, with
"step": "reply", their line's own fields as fields, and the whole conversation followed by the
reply prompt as prompt. Answer with --body alone; it goes in their thread at once, exempt from
the send clocks. Ending the thread or honouring a worded "stop" is not an answer this takes — tell
the operator. An install with OUTSEND_REPLIES=hold answers no reply on its own, so the replies
wait for exactly this.
To answer replies and send nothing else, use outsend reply, the same handover without the
cold sends — the loop for a session answering customers while a timer keeps sending:
outsend --json reply # draft_pending for the oldest reply owed, or none
outsend reply --body "Which ones?" # sends it in their thread; run bare again for the nextIt ends with exit 0 and 0 reply(ies) to answer when nobody is waiting. A draft held for an opener
or a step stops it with an error; answer that with send --agent-draft first.
Don't pass --agent-draft unless you intend to answer every draft_pending it raises — a bare
outsend send uses AI_MODEL and writes every opener and step itself, which is the right default for
a run nobody is driving turn-by-turn. --subject without --body, or either without
--agent-draft, is refused before any mail moves; an opener answered without --subject is refused
before it is kept.
Reading the output
Nothing here prints a CSV — sending has no rows to hand on. Everything is narration on stderr:
counts (read 2 new message(s) · answered 1 · sent 0 later step(s) · followed up 0 · opened 3), the gate holding a pass that
did nothing ("no mailbox connected, so nothing can be sent" rather than a silent zero), and, for a
goal, how the run ended (opened 5 of 5 conversation(s) in 3 pass(es), or stopped at ... with the
wall it hit). Never report a pass that did nothing as "no leads to send" — read the holding line;
it names the actual gate (no mailbox, an empty pool, the daily ceiling, outside the sending window).
Exit codes and failures
Non-zero means something needs attention; a goal that fell short still opened whatever it could before saying so. Failures are one line on stderr:
error: <message>draft_pending is the one exception with a stable type and, under --json, a payload — see above.
Every other failure is plain text naming what is wrong (a missing variable, a rejected model key, a
mailbox that failed its SMTP login); there is no other typed vocabulary to branch on yet.
Things not to do
- Never run
outsend send(orrun, from OpenOutreach) unless the user asked to send. Checking status and ingesting leads are safe; putting mail in someone's inbox is not something to do on spec. - Don't invent a daemon or a watch loop — a pass reads what the guards allow right now and exits;
send N/send allare how you wait for more without polling. - Don't pass
--agent-draftunless you plan to answer everydraft_pendingit raises. - Don't parse stderr for data — there is none here to parse;
outfind's CSV/JSON is the data surface. - Don't try to remove or word around the
Sent with OpenOutreachattribution line — it is always on, by design (see the repo's Legal notice), and not a setting this CLI exposes.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

