What it does, sorted by who hears about it.
One merge goes in. The launch post, the note for your next customer call and the link for a support reply each come out written. Your team keeps merging.

How it works
Merge. Draft. Publish.
Merges a PR
Connect GitHub once. From then on every pull request your team and its coding agents merge flows in.
Drafts the update
Claude reads the diff and writes for each channel, for the buyer you named, in the voice you chose.
Sign off
Each channel starts out waiting for a person, in the app or from Slack. Once its drafts keep going out as written, let it go out on its own.
It goes out
At the next free slot, to the accounts you connected. One switch pauses everything.
Launches
Launch copy that is already written.
Every channel you run, drafted from the diff, aimed at a named buyer, in a voice you chose. It goes out on its own, or waits for you to edit it first. Your PR title never gets reworded and called a post.
LinkedIn · X · Bluesky · Mastodon · Threads · Newsletter
- Drafts from the real diffOne pass for what shipped, one per channel for how to say it. Not your PR title reworded.
- Written for your buyerPoint it at your product site and it drafts the ideal-customer profile: who they are, what hurts, the words they use.
- Company voice and founder voiceOne merge can go out twice, in different words. Copy written as a person never auto-publishes.
- Small merges wait for a batchA ten-line fix is not an announcement. Small merges wait until you group them into one update for the set.
- Every channel, sized to fitEach draft is written to that network's real limit, counted the way that network counts it.
- Links tagged for attributionOpt-in UTM tagging, applied per network as each post is queued, so your analytics can tell a LinkedIn click from a Bluesky one.
- Post things that were not PRsThe composer writes an ad-hoc post through the same voices, sets, schedule and queue, with a live preview per network.
Customer calls
An answer to "what's new since we last talked".
What shipped, in words you can forward, before the call rather than after it.
Weekly digest · Exec summary · Newsletter · Slack · Daily activity email
- Weekly digest and exec summaryCompiled from every merged PR in the window. Opt in, and it goes out on its own or waits for you.
- A newsletter from your own providerAn issue compiled from what shipped, sent by your own Resend or Mailchimp account, on your own sending reputation.
- Updates in SlackEach update posted to the Slack channel you connect, from the same draft as everything else.
- A morning email of what went outOne day of what shipped and what was told, with a link to each change.
- Each repo posts where it shouldGroup connected accounts into sets and point a repo at one. A missing set publishes nothing, never everywhere.
Support
Fewer tickets about features that already exist.
A link to send instead of an answer to write, out the week the change ships.
Changelog · WordPress · Discord · Atom and JSON feeds
- Your own changelog feedA public HTML page with Atom and JSON feeds at your own URL, from the same drafts. If you already keep a changelog in Featurebase, Beamer or Canny, each entry can publish there too.
- Articles on your WordPressThe article goes out first, and the posts about the same merge link to it instead of to your site.
- Community announcementsDiscord and Slack webhooks, at the size each one really allows.
- Noise never becomes a postBot authors, lockfile churn and anything labelled marketing:skip are dropped before a draft is written.
Sign-off
Your team merges. You decide what goes out on its own.
Every channel starts out waiting for you. Let each one go out on its own once its drafts keep going out as written.
In the app · in Slack
- Approver teamsNewPut a channel under a team, and its copy waits for someone on that team instead of going out on its own.
- Two-person approvalNewWhoever merged the change can't approve the post about it.
- Comment and request changesNewLeave the note on the draft, not in a thread nobody can find later. A change request stays on it until the copy is edited.
- Approve from SlackThe card lands in your channel. After five approvals in a row sent as written, it offers to send that kind of post on its own.
- The Autopilot barThe share of posts your rules would send with nobody pressing a button, and the next button to remove.
- Proof it is worth itKept-rate: how much generated copy you publish, and how much you rewrote first. If the drafts are bad, the number says so.
Ask what's wrong
Your MCP server tells you what's going on.
You never open the app to find out why something didn't post. Ask your agent, and the answer comes back with the reason, the evidence and the fix.
Claude · Cursor · any MCP client
- Ask why something isn't postingNewName a repo, a product or a network. Every merged PR is traced to where it stopped, and the causes come back ranked, each with its fix.
- Catches merges that never arrivedNewFor a single repo it checks GitHub's own list of merged PRs, so a webhook that never came shows up instead of passing for a quiet week.
- One PR, explainedNewAsk about one pull request and get each channel's outcome: out, queued, refused with the network's own words, waiting on you, or skipped and why.
- A daily health checkNewWhat went out yesterday, what broke, what waits on you, and how close you are to hands-free. Put the prompt on a schedule and it arrives every morning.
Under the hood
For whoever connected GitHub.
The parts someone will ask about. They stay visible, because the people who own them need to find them.
GitHub App · .marketing.yml · webhooks · queue
- Your accounts, not a middlemanMerge & Tell posts as you. Tokens stay encrypted server-side and never reach the browser.
- Split by who changes itThe repo owns skip labels and path rules in .marketing.yml. The app owns voice, ICP and where it posts.
- A queue with a stop buttonRetries back off, an attempt it cannot confirm is never blindly re-sent, and one switch pauses the account.
- Hide a repo, completelyOne switch and a repo records nothing, publishes nothing, and leaves every list and picker.
In detail
Between the merge button and the post.
Every step, in the order it runs. Products, repos, ICPs, voices and sets are separate things for a reason; see how the pieces fit.
The pipeline
From merge to draft
You install a GitHub App on the repos you want covered. When a pull request is merged, GitHub delivers a webhook. The handler checks the signature before anything else happens, constant-time and length-guarded, then records the delivery id, so GitHub redelivering the same event does not produce a second post. The webhook enqueues a job and returns straight away; nothing slow happens inside the request.
A worker claims the job with a row-locked database call, so two workers cannot pick up the same merge at the same time (a job whose worker died is reclaimed later, on purpose). The worker writes the pull-request row before any fallible work. A merge shows up in your feed even if drafting later fails, rather than vanishing. Failures retry with a cap of five attempts; a reconciler sweeps every five minutes for jobs that died mid-flight.
Two passes, not one
Drafting runs in two passes on purpose. The first pass runs Claude Haiku 4.5 over the pull request and answers one question: what changed for the people who use the product. It reads the PR description as the primary source, because that is a human explaining intent, with the diff (trimmed to 12,000 characters) as supporting evidence, and it answers in fixed fields: who is affected, what they had to put up with before, what happens now, and why that matters. Naming a file, function or table is off-limits, because a summary written in code vocabulary produces copy written in code vocabulary.
The second pass runs Claude Sonnet 5.5 once per channel and voice, assembling that brief together with your ICP, your voice, your repo config, and the channel’s own format brief.
The draft keeps both artefacts: the stage-one brief and the exact stage-two prompt. When a post reads oddly, you can see which input caused it instead of guessing.

The filter ladder
What never becomes a post
Most merges are not announcements. Before a token is spent, every merged PR runs down a ladder and the first match wins:
- A
marketing:skiplabel, the manual override. - Any skip label you configured for the repo.
- A path rule marked
publish: never, or a PR that asked to be skipped in its description. - Bot authors. Dependency bumps announce themselves to nobody.
- A diff that is only lockfiles, generated files, or formatting: counted as zero meaningful lines and dropped.
- A
marketing:launchlabel, which routes the PR to launch framing instead of a routine update. - A merge that lands inside a cooldown window you set on Schedule, when the rules would have posted it, is held for a batch you build, so six merges in an hour become one update rather than six posts. Off until you turn it on. A
marketing:launchlabel still drafts on its own. - Everything else goes to your routing rules. They skip chores and refactors on Discord, and minor user-facing news on X, Bluesky and Mastodon, so those never post on their own. You can still put PRs in a batch by hand.
Fewer than ten meaningful changed lines is not a rung. It marks the diff trivial, which is a size reading. A pin or the brief can still call the same change major, and major user-facing news still gets a post.
That ladder is structural, built out of labels, authors and line counts, so it cannot tell an internal rewrite from a new capability. One more check does. After the first pass reports what changed for users, a pull request whose honest answer is nothing reaches them stops there, in the model’s own words. A change nobody can see has no post in it, and asking for one anyway is how you get copy that invents a benefit or narrates the plumbing. A marketing:launch label overrides this, because you already answered the question.
The decision is recorded on the pull-request row with its reason, so “why didn’t this one post?” is a question the app answers rather than a mystery.
Voices
Who is speaking
A voice is an identity, and it is separate from your audience. A brand voice speaks as the company. A person voice speaks in first-person singular, gets the PR description as story material, and carries its own story hooks, off-limits list and disclosure line. The last two are never model-written.
Because drafting fans out per (channel, voice) pair, one merge can produce two different posts: the company account announcing a feature, and you saying what it was like to build it. Grouping is by resolved voice rather than by account, so two accounts that share a voice get one draft rather than duplicates.
How a voice is chosen
- The voice bound to the connected account itself. It always wins, and no repo config can override it.
- A per-channel voice in the repo config.
- The repo’s default voice (a PR hint can override this one).
- Your account’s default voice.
- The built-in brand voice.
A named voice that does not exist holds that channel and says so. It never falls back to a different identity. And a person voice holds its copy for review: copy written as a human never auto-publishes, whatever your rules say.
ICPs
Who is being spoken to
Point the ICP generator at your product site and it fetches the homepage, pricing and about pages in parallel, then produces a structured ideal-customer profile: who they are, what hurts, what they want, the vocabulary they use, the objections they raise, and the tone that lands.
Fetching a URL somebody typed is a security problem, so the address is checked before every request and again after each redirect: public http(s) only, with loopback, private ranges and the cloud metadata endpoint refused, and IPv4-mapped IPv6 normalised before the check rather than after. An eight-second timeout and a streamed two-megabyte cap bound what one run can cost. If every page fails to load, the run throws rather than overwriting a good ICP with an empty one.
Repos name an ICP by string, and paths within a repo can name a different one. Infrastructure work and product work rarely address the same reader.
Channels
Sized the way each network counts
Every channel is written to that network’s real ceiling, counted the way that network counts, which is not the same as counting characters.
| Channel | Limit | The catch |
|---|---|---|
| X | 280 | Weighted: Latin and common punctuation cost 1, CJK and emoji cost 2. Links bill a flat 23. |
| Bluesky | 300 | Grapheme clusters, so an emoji with a skin-tone modifier is one. Links count literally. |
| Mastodon | 500 | Links bill a flat 23. |
| 3,000 | Aims far under the cap; nobody reads 3,000 characters. | |
| Threads | 500 | An emoji costs its UTF-8 bytes, so a rocket is 4 and a family emoji is 25. One hashtag per post, which becomes a topic, and at most 5 links. |
| Discord | 2,000 | Posted through a channel webhook, and a webhook is not a Nitro subscriber, so 2,000 is the number that binds. No hashtags. On Discord they read as spam. |
| Slack | 3,000 | Posted through an incoming webhook. 3,000 is one text block, so the copy stays in one. No hashtags. On Slack they read as a broken channel link. |
| Newsletter (Resend, Mailchimp) | Uncapped | One email per period covering every merge routed here, never one per merge. Your own provider sends it. No hashtags. |
| WordPress (WordPress.com) | Uncapped | A blog post of 400 to 800 words for a reader who arrived from search, published or left as a draft. Tags go on as terms, not hashtags. |
| Changelog (Featurebase, Beamer, Canny) | Uncapped | No hashtags. An entry is not a social post. |
| Title 300, body 10,000 | Copy-out draft. No native publish yet. First line is the title. No hashtags. | |
| Hacker News | Title 80, body 8,000 | Copy-out Show HN draft. No native publish yet. First line is the title. |
Copy is budgeted before it is written (the cap minus room for hashtags), measured after, and only if it is still over does it get one follow-up call asking for it shorter. A hard trim at a word boundary is the last resort, not the first. The same measuring code runs in the browser, so the character count you see while editing is the one the publisher will enforce. Reddit and Hacker News are in that queue as copy-out drafts: there is no native publish yet, so you paste them. They never auto-publish.
Connections
It posts as you, from your own accounts
There is no aggregator in the middle. You connect your own accounts and Merge & Tell publishes to them directly:
- Bluesky
- Mastodon
- X
- Discord
- Slack
- WordPress
- Bluesky. A handle and an app password. No app registration, nothing to approve; the fastest way to try it.
- Mastodon. OAuth against your own instance. Merge & Tell registers the per-instance client once and caches it.
- LinkedIn. OAuth, posting to your member feed.
- X. OAuth2 with PKCE. X bills per post, so each post is priced and the estimate recorded on its queue row.
- Discord. A channel webhook URL, which is the credential: no identity, no session, no expiry.
- Slack. An incoming webhook URL for one channel. The URL is the credential, so its secret part is stored encrypted and never shown again.
- WordPress. Your own site, with a username and an application password, or a WordPress.com site over OAuth. Each change worth an article becomes a post on your own domain, published or left as a draft.
Where the secrets live
Tokens, app passwords and client secrets are stored encrypted in Supabase Vault, reachable only by the server. The table your browser can read holds metadata and nothing else. Deleting a connection destroys its secrets through a database trigger rather than hoping application code remembers.
A connection is active, needs_reauth (fails fast instead of retrying into a wall) or revoked. Every connect, test, disconnect and token-going-stale event is both logged and written to an Activity feed you can read on the screen. A silent credential failure is the most expensive lie this app could tell. Tokens that renew themselves are not flagged as broken; only the ones that need you are.
Social sets
Which accounts a repo posts to
A social set is a named group of connected accounts: “company accounts”, “the side project”, “founder only”. A repo names one in its config; without one it uses your account default, and without that, every active connection.
The failure mode here matters more than the feature. A repo that names a set which no longer exists publishes nothing and tells you why. It never falls back to “everywhere”. A repo whose config could not be read is shown as unreadable, not as unconstrained. The routing tree on the accounts screen shows, per repo, exactly where a post would land before you approve one.
Membership is many-to-many, which is what lets one company account belong to several products’ sets, or to every set at once. Why sets overlap, and what filing an account does.
The queue
When it goes out, and the stop button
Approving a draft creates one queue row per target, snapshotting the exact text that will be sent. You choose now, a specific time, or the next free slot in the posting schedule you set. Those are real wall-clock times in your own time zone, so an 09:00 slot stays 09:00 across daylight saving. Quiet hours and a weekend skip on that same screen hold a post until the window opens, including Publish now. A merge cooldown holds a later same-repo merge for a batch instead of drafting it on its own.

A publisher runs every minute. Rows claimed by a worker that died are reclaimed after ten minutes. Failures back off on a fixed ladder of one minute, five, fifteen, an hour and three hours, up to five attempts, and a retry waits as long as the network asks it to.
Not sending the same post twice
Three layers, in order:
- Claiming a row is a conditional update, so two workers can never dispatch the same row at the same time. (A row whose worker died mid-send is reclaimed later, which is what the next layer is for.)
- Before a retry, the network is asked whether the earlier attempt already landed. Mastodon dedupes server-side on a key Merge & Tell supplies; X and Bluesky get a lookup. “I could not check” is treated as “do not send”. LinkedIn cannot be checked at all, since reading your posts back requires a permission only approved apps get.
- The remote post id is written in the same update that marks the row published, so a row already carrying one is adopted, never re-sent.
Only Mastodon is exactly-once in the strict sense. The rest is layered mitigation, and it is described that way here on purpose.
One switch on the settings screen pauses the entire account. It cancels nothing already published and does not burn the retry budget. It sits in front of the send. Individual rows can be cancelled, retried, or unpublished from the network, with the row kept as history.
Configuration
Split by who changes it
A repo’s config has two owners, because its two halves change for unrelated reasons. The repo owns its GitHub plumbing in a .marketing.yml on the default branch: the labels that mean “skip”, and per-path areas that reframe or silence whole directories. Those go stale the moment code moves, so they change in the pull request that moves it.
The app owns the marketing: product name and URL, routing rules, ICP, voice, hashtags, style rules, a glossary of terms to keep straight, and the social set to post as. Changing a campaign shouldn’t need a code review or a deploy.
Any pull request can override that for itself with a marketing block in its description: a different ICP, a different framing, extra hashtags. With one guard. A PR can never make itself auto-publish. Whether a network publishes on its own is set in the app’s routing rules, which is the point. On a public repo, anyone can open a pull request.
Neither half is required, and neither can overwrite the other: saving a hashtag never drops a skip label, and committing a skip label never changes where a repo posts. A setting the file has no business holding is ignored and named on the repo’s page, rather than silently doing nothing.
Batches
Several merges, one update
A batch is a named group of merged PRs that share one update, and you pick the group. Nothing files one for you. The rules skip chores and refactors on Discord, and minor user-facing news on the socials, so none of those post. You group the merges you want into a named batch, generate one update for the set, and publish it yourself. The batch is linked back to every PR it covers. A cooldown on Schedule holds a later merge in the same repo when the rules would have posted it, so a burst does not become a burst of drafts.
There is also an opt-in weekly digest: categorized release notes and a non-technical exec summary compiled from eligible merged PRs in a window you choose. It waits in the same review queue by default, because a post spanning a week is the one you want to read first. Turn on auto-publish for it on Batches and it goes out on its own.
Composer
Posts that were not pull requests
Sometimes the thing to say did not come from a merge. The composer writes one, with channel chips showing how many accounts each one reaches under the selected set, a live per-network preview, and one body or a different one per channel.

It goes through the same routing, sets, voices, schedule and queue as generated copy, and it rejects over-limit text before anything is written, for every channel at once. Hand-written posts are excluded from the kept-rate metric, so they cannot flatter the numbers, but they are used as voice samples: what you wrote yourself is the truest sample of how you sound.
Attribution
Tagged links, opt-in
Switch tagging on once for the whole account and every repository inherits it. Any one of them can use its own campaign settings instead, or refuse tagging entirely. Only links to your own product get tagged, so a repository needs a product URL for tagging to reach it; the source is the network the post is going to, and a link that already carries its own tags is left exactly as written.
There is also a marker, via=mergetel, for the question your analytics otherwise cannot answer: which of this traffic did the app produce? utm_source only names the network, which a post you typed yourself would say too. Off unless you ask for it, and it never takes a label you were already using. Neither via nor utm_content is overwritten when the link already carries one.
Tagging happens per target as the post is queued, not at the last moment, so the tagged text is what gets length-checked. That order matters: Bluesky counts link text literally, so a post that fits before tagging can fail after it.
Changelog
A feed you own, and a page crawlers can read
The changelog is a channel with no network behind it. The same drafts render as a public HTML page with JSON-LD, an Atom feed, and a JSON Feed at your own URL. The feed format is in the path rather than a header, so each is its own cache key and each entry keeps the same id across both. Switching format does not re-announce your history. The HTML lives on /changelog/<id>/html and one page per entry; the feeds point at those pages rather than inventing a third feed.
Entries come from what was published, in publish order: editing a draft afterwards does not rewrite history. PR-sourced entries link back to the pull request. Bodies are text, never HTML. The feed and the HTML index are the newest fifty entries. Each entry also has its own page, which stays up after it drops off that fifty.
The screen also generates a prompt you can paste into an agent that should watch your releases, carrying your own feed URL and the four things such an agent otherwise gets wrong.
Stats
Whether any of this is working
Two numbers, both about generated copy only. Kept-rate is the share of decided drafts you published rather than discarded. Undecided drafts stay out of the denominator, so the number does not drift upward just because a queue is backing up.
The second is how much you edited before publishing: an edit ratio: the edit distance between what was generated and what went out, over the length of the longer of the two. Merge & Tell measures it at publish time and shows it as an average percentage. Zero means it went out verbatim, and lower is better. If the drafts are bad, both numbers say so, which is the point of showing them.
The same screen lists likes, reposts and replies the networks report on published posts, per channel. Bluesky and Mastodon hand those counts over without a paid read. X bills for tweet reads, so those posts stay unmeasured rather than filled in. A zero is a real snapshot. An unmeasured channel says so.
Ask what's wrong
Your MCP server tells you what's going on
The point of Merge & Tell is that nobody opens it. So when something stops posting, the answer can't live only on a screen. Ask your agent “why aren't my acme/app PRs going out?” or “why is nothing posting to LinkedIn?”, and the MCP server traces every pull request merged in the last 30 days (up to 90) to the furthest point it reached.
Each PR gets one outcome, read from what actually happened to it. It went out, or it is queued. The network refused it, and you get the network's own error. It waits for an approval, or for someone to press Generate. Your rules skipped it, and the rule says why. The filter dropped it as a bot, a skip label or a lockfile bump. Drafting failed. It was never processed. For a single repo the server also reads GitHub's own list of merged PRs, so a merge whose webhook never arrived is counted too. That is the one failure nothing else in the app can see.
The causes come back ranked: what stops everything (a paused account, a hidden repo, an account that needs reconnecting), what is losing posts, what waits on a person, and what is only worth knowing. Each carries the numbers it rests on and the fix. A fix that removes a button names the Rules cell to change. Turning on Publish stays your call.
The daily health check is the same trace across the whole account: what went out in the last day, what broke, what waits on you, your Autopilot percentage and the next button to remove. Put the daily_health_check prompt in a scheduled task and the report arrives every morning. Anything the server could not read is named in the answer, so a failed lookup never reads as “all fine”.
That is the whole machine.
You were going to merge the PR anyway.
Wondering how this differs from what you already use: why it is not a scheduler, why it does not make AI video, and why it is not a general AI agent.
Shopping the ones that also start at GitHub: ShipCast, PushToPost and BuildCast. Each of those pages says when to buy the other one.
Every feature is on every plan. Seats are unlimited.