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.

The factory and cafe bridged by Merge & Tell, with product updates reaching smiling customers

How it works

Merge. Draft. Publish.

Your team

Merges a PR

Connect GitHub once. From then on every pull request your team and its coding agents merge flows in.

Merge & Tell

Drafts the update

Claude reads the diff and writes for each channel, for the buyer you named, in the voice you chose.

You, at first

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.

Your accounts

It goes out

At the next free slot, to the accounts you connected. One switch pauses everything.

Launches

Launch copy that is already written.

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

Support

Fewer tickets about features that already exist.

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.

Under the hood

For whoever connected GitHub.

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.

One merged pull request in the app: its title and merge date, then a timeline of what happened to it, from merge to the drafts waiting for approval.
A merged pull request, and what Merge & Tell did with it.

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:

  1. A marketing:skip label, the manual override.
  2. Any skip label you configured for the repo.
  3. A path rule marked publish: never, or a PR that asked to be skipped in its description.
  4. Bot authors. Dependency bumps announce themselves to nobody.
  5. A diff that is only lockfiles, generated files, or formatting: counted as zero meaningful lines and dropped.
  6. A marketing:launch label, which routes the PR to launch framing instead of a routine update.
  7. 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:launch label still drafts on its own.
  8. 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

  1. The voice bound to the connected account itself. It always wins, and no repo config can override it.
  2. A per-channel voice in the repo config.
  3. The repo’s default voice (a PR hint can override this one).
  4. Your account’s default voice.
  5. 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.

ChannelLimitThe catch
X280Weighted: Latin and common punctuation cost 1, CJK and emoji cost 2. Links bill a flat 23.
Bluesky300Grapheme clusters, so an emoji with a skin-tone modifier is one. Links count literally.
Mastodon500Links bill a flat 23.
LinkedIn3,000Aims far under the cap; nobody reads 3,000 characters.
Threads500An 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.
Discord2,000Posted 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.
Slack3,000Posted 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)UncappedOne email per period covering every merge routed here, never one per merge. Your own provider sends it. No hashtags.
WordPress (WordPress.com)UncappedA 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)UncappedNo hashtags. An entry is not a social post.
RedditTitle 300, body 10,000Copy-out draft. No native publish yet. First line is the title. No hashtags.
Hacker NewsTitle 80, body 8,000Copy-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
  • LinkedIn
  • 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.

The publish queue as a board: columns for posts that need approval, are scheduled, failed, or went out, each card with its network, time and copy.
The publish queue. Every post, its account and its time.

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:

  1. 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.)
  2. 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.
  3. 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.

Repositories

Hiding one, completely

Every synced repo is listed on one screen, and each has a hide switch that makes three promises and enforces all of them:

  • Records nothing. The webhook acknowledges the merge and drops it before a job exists, and a job created before you hid it re-checks and stops too.
  • Publishes nothing. The gate sits directly in front of the queue, so approving, publishing now, scheduling and publishing a batch all refuse with a reason. Already-queued posts for that repo are cancelled, and the count is shown.
  • Appears nowhere. Not in the PR feed, the batch view, the preview tool, the ICP picker or the routing tree.

Unhiding restores everything from that moment. Merges that happened while it was hidden were never recorded and cancelled posts do not come back. The screen says that out loud rather than letting you discover it.

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.

The compose screen: a row of channel checkboxes, one text box for the copy, and a character count per selected network.
Compose. One post, counted against each network on its own.

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.