# Blowhorn > Blowhorn is a social media console that takes one message and broadcasts, reposts and amplifies it across every profile and platform your community runs, on autopilot. Made by Layer5 for macOS. -------------------------------------------------------------------------------- title: "Blowhorn: one message, many ears" url: https://blowhorn.ai/index.md description: Stop pasting the same post into seven tabs. One queue. Every profile. Posted as you, from your own Chrome. Made by Layer5 for macOS. -------------------------------------------------------------------------------- Stop pasting the same post into seven tabs. One queue. Every profile. Posted as you, from your own Chrome. Made by Layer5 for macOS. -------------------------------------------------------------------------------- title: "Trust Center" url: https://blowhorn.ai/legal/index.md description: How Blowhorn handles your data, sessions and accounts, in plain language, and every policy that covers the Blowhorn website, the Blowhorn app for macOS and the Blowhorn Chrome extension. -------------------------------------------------------------------------------- ## Report a security issue If you think you have found a security vulnerability in Blowhorn, send the details privately to [security@blowhorn.ai](mailto:security@blowhorn.ai) rather than opening a public issue. Layer5 acknowledges and analyzes each report within 10 working days and keeps the reporter updated while it is addressed. The [security policy](https://github.com/layer5io/blowhorn-site/blob/master/SECURITY.md) explains what to report and how fixes are disclosed. ## Questions - About privacy: open an issue on the [site repository](https://github.com/layer5io/blowhorn-site/issues), or contact Layer5 through the channels on its [privacy policy](https://layer5.io/company/legal/privacy/). - About the terms: email [legal@layer5.io](mailto:legal@layer5.io). ## Changes to these policies Each policy shows the date it last changed at the top of its page, and every edit to these pages is recorded in the [site repository's history](https://github.com/layer5io/blowhorn-site/commits/master/content/en/legal). -------------------------------------------------------------------------------- title: "Privacy" url: https://blowhorn.ai/legal/privacy/index.md date: "2026-10-09" description: What the Blowhorn website and the Blowhorn app store, where they store it, and what the app sends. -------------------------------------------------------------------------------- ## This website blowhorn.ai is a static site served by GitHub Pages. It sets no cookies, runs no analytics and loads no fonts, images, styles or scripts from third parties: they are all served from this site. Its one request to another host is the Download section's release lookup, described below. The newsletter form sends nothing unless you submit it. - GitHub serves the pages and keeps its own server logs. Those are covered by the [GitHub privacy statement](https://docs.github.com/site-policy/privacy-policies/github-privacy-statement). - The Download section asks GitHub's public API for the newest release from your browser. That request reaches GitHub like any other request your browser makes, with your IP address and the usual request headers. With JavaScript off, no request is made and the page links to the releases page instead. - The site has one form, the newsletter signup in the footer. It is the same Layer5 newsletter signup as on layer5.io. When you submit it, your browser sends the email address you typed to Layer5's newsletter list on Mailchimp and opens Mailchimp's confirmation page. [Layer5's privacy policy](https://layer5.io/company/legal/privacy/) covers that list. The site collects nothing else you type. ## The Blowhorn app ### On your Mac - Browser sessions for LinkedIn, X, Reddit and Hacker News live in Chrome profiles on your machine. They never leave it. - Configuration lives in `.blowhorn.yaml` inside the installation. The store connection, including its password, lives in `~/.config/blowhorn/store.yaml`, readable only by your user account. - For LinkedIn, X, Reddit and Bluesky, the secret a run signs in with is read from `profiles//config.yaml` inside the installation, and `blowhorn profile set` writes it there. - Run logs are written to the `logs/` directory of the installation. The menu-bar app keeps its preferences and state under `~/Library/Application Support/Blowhorn/`. ### In Chrome, through the Blowhorn extension The extension works only for the Blowhorn app on your Mac, over Chrome's native messaging. It acts only on the sites Blowhorn supports, only in the Chrome profiles you install it in, and only when the app asks. Everything it reads goes to that app and nowhere else: it reports to no server of its own, and it keeps nothing itself. - Sign-in information: a session cookie or token on a supported site, read when the app needs to reuse a sign-in you already have. Today that is your Slack workspace session, which the app keeps in your organization's store (below). - Your communications: the posts, comments and messages the app asks it to enter and send as you. - Page content: what a page shows, read to tell when an action has finished; a screenshot when an action needs a person to check it; and an export you asked a site for, such as an analytics or connections export, which it catches and hands to the app. ### In your organization's store on Layer5 Cloud - The content queue, schedule, profiles, run ledger and analytics. Every row is scoped to your organization's id. - Platform credentials, such as API tokens and app passwords, in the credentials table of your Layer5 Cloud organization. Blowhorn reads them when it checks which profile may act on which platform. GitHub, Hacker News and Slack runs authenticate with the stored value; for Slack that is the workspace session captured from your Chrome, or a Slack app token you enter. LinkedIn, X, Reddit and Bluesky runs read theirs from the local `config.yaml` above. - Layer5 Cloud's handling of that data is governed by the [Layer5 privacy policy](https://layer5.io/company/legal/privacy/). ### What the app sends - Posts, comments, reactions and invitations to the platforms a profile is configured for, when an approved row is due or when you run a command. Nothing is published from a dry run. - A version check that lists the releases on Layer5's Blowhorn release channel, hosted on GitHub. The check needs GitHub credentials: when a GitHub token is already on your Mac (`GH_TOKEN`, `GITHUB_TOKEN` or `gh auth token`), the app sends it with that request, and updating downloads the disk image published with the newest release, installs the Blowhorn app from it into `/Applications` and relaunches. Without a token, the update button opens that release page in your browser and the app downloads nothing itself. - No usage analytics and no crash reports. The app carries no analytics or crash-reporting library. ## Questions Open an issue on the [site repository](https://github.com/layer5io/blowhorn-site/issues) for anything about this page, or contact Layer5 through the channels on its [privacy policy](https://layer5.io/company/legal/privacy/). -------------------------------------------------------------------------------- title: "Terms" url: https://blowhorn.ai/legal/terms/index.md date: "2026-10-09" description: The terms between you and Layer5, Inc. for the Blowhorn website, the Blowhorn app for macOS and the Blowhorn Chrome extension. -------------------------------------------------------------------------------- **Draft, pending legal review.** A lawyer has not yet reviewed these terms, and they may change before they are final. ## What these terms cover Blowhorn is a social media console made by Layer5, Inc. ("Layer5", "we" or "us"). These terms cover all of it: - the website at blowhorn.ai; - the Blowhorn app for macOS: the menu-bar app, the `blowhorn` command-line tool and the background service that runs scheduled work; - the Blowhorn extension for Google Chrome, published on the Chrome Web Store as item [`fcflpiagmpcfifgknledeknedfopnapf`](https://chromewebstore.google.com/detail/fcflpiagmpcfifgknledeknedfopnapf). By downloading, installing or using any of them you agree to these terms. If you use Blowhorn for an organization, you agree to them for that organization and you confirm you are allowed to. Blowhorn relies on Layer5 Cloud, which has its own [terms of service](https://layer5.io/company/legal/terms-of-service/) and [privacy policy](https://layer5.io/company/legal/privacy/). Those govern your Layer5 Cloud account and organization. These terms do not change them. ## Early access Blowhorn is in early access. Expect features to change, move or be withdrawn, and expect bugs. Rehearse a run with its dry-run form before you let it publish. During early access Blowhorn is free. There are no plans or tiers to choose between: everyone gets the same features. If Layer5 introduces paid plans, we will announce them on this site and update these terms before we charge anything, and you will not be charged unless you choose a paid plan. ## What Blowhorn needs - **A Layer5 Cloud organization.** Blowhorn keeps your content queue, schedule, profiles, run ledger and platform credentials in your organization's store on Layer5 Cloud, and you sign in with your Layer5 Cloud account. If you lose access to that organization, Blowhorn stops running work that depends on its store. - **A Mac with macOS 13 or newer** for the app. - **Google Chrome** for the platforms Blowhorn drives in your browser, with the Blowhorn extension installed in each Chrome profile you map to a Blowhorn profile. ## Downloads - The official Blowhorn app downloads are the signed disk images linked from blowhorn.ai, published as assets on the [releases page](https://github.com/layer5io/blowhorn-site/releases) of this site's repository, and the updates the Blowhorn app installs itself from Layer5's release channel, as the [privacy page](/legal/privacy/) describes. Nothing else is an official copy. Verify each disk image you download from blowhorn.ai against the `SHA256SUMS.txt` published with it before you open it. - The only official Blowhorn extension is the Chrome Web Store item named above. - The Blowhorn software, meaning the app for macOS, the `blowhorn` command-line tool, the background service and the Chrome extension, is licensed under the [GNU Affero General Public License, version 3](https://www.gnu.org/licenses/agpl-3.0.html) (AGPL-3.0). Each release ships a copy of that licence. Read it before you install. Where the licence and these terms differ about the software itself, the licence decides. - Releases are never replaced in place. A fix ships as a new version. ## Your accounts - Blowhorn acts on the third-party platform accounts you configure, such as LinkedIn, X, Reddit, Hacker News, Slack, Bluesky and GitHub. It acts either in your own signed-in Chrome, through the extension, or through the platform's own API with credentials you provide. - You are responsible for those accounts and for everything published from them. Use only accounts you have the right to use, including any account you run for someone else. - You are responsible for following each platform's own terms, rules and limits. Blowhorn gives you the controls; it does not give you permission to break a platform's rules. - The platforms are independent of Layer5 and are not part of Blowhorn. A platform can change its site or API, restrict a feature, or suspend an account at any time, and that can stop a Blowhorn action from working. Layer5 is not responsible for what a platform does to your account. - Blowhorn amplifies what you approve: the rows you approve in the queue and the commands you run. A dry run publishes nothing. On Hacker News it submits and comments only; it never votes. ## Your content The posts, comments, messages, profiles and other content you put into Blowhorn stay yours. You give Layer5 permission to store, copy and send that content only as needed to run Blowhorn for you, which includes keeping it in your organization's store on Layer5 Cloud and publishing it to the platforms you choose. ## The Chrome extension - The extension is a companion to the Blowhorn app on your Mac. It is not a standalone social client and does nothing without that app. - It talks only to the Blowhorn app on your Mac, over Chrome's native messaging. It acts only on the sites Blowhorn supports, only in the Chrome profile you install it in, and only when the app asks. - On those sites it opens or finds tabs, sends the clicks and keystrokes a queued action needs, reads page state, takes a screenshot when an action needs a person to check it, catches an export you asked for, and reads a sign-in cookie when the app needs to reuse a session you already have. - It hands what it reads to the Blowhorn app and to nothing else. What the app keeps and sends is described on the [privacy page](/legal/privacy/). - Because the extension acts in your own signed-in session, an action it takes at your instruction is taken by you, on your account. ## Acceptable use You may use Blowhorn only for lawful purposes and in line with these terms. In particular, you will not: - use Blowhorn to send spam, to impersonate anyone, to harass anyone, or to manipulate a platform's votes, rankings or reach in breach of that platform's rules; - run Blowhorn against an account you do not have the right to use; - use the site or Blowhorn in a way that could damage, disable, overburden or impair it, or interfere with anyone else's use of it; - try to get materials or information through the site or Blowhorn by any means not intentionally made available. If you use Blowhorn from outside the United States, you are responsible for following your local laws. ## Trademarks and brand assets Blowhorn, the Crowhorn mark, Major Blowhorn and Layer5 are trademarks of Layer5, Inc. The brand assets this site serves are a copy of the Blowhorn brand kit, published for the site's pages and as the kit's public reference; they are not licensed for reuse. The fonts are licensed under the SIL Open Font License and are credited in the site's [licence note](https://github.com/layer5io/blowhorn-site/blob/master/static/assets/brand/LICENSES.md). The site's text, graphics, logos and images belong to Layer5 or its suppliers and are protected by copyright and other laws. Except for the AGPL-3.0 licence that covers the Blowhorn software, these terms give you no licence to Layer5's intellectual property. ## No warranty **Blowhorn, this site and everything available through them are provided "as is", without warranty or condition of any kind, to the maximum extent permitted by applicable law.** Layer5 and its suppliers disclaim all warranties and conditions, including the implied warranties or conditions of merchantability, fitness for a particular purpose, title and non-infringement. Layer5 and its suppliers make no promise that Blowhorn is suitable for any purpose, or that it is reliable, available, timely or accurate. That includes any promise that an action will reach a platform, that a platform will accept it, or that a platform will leave your account alone. The site and Blowhorn may contain errors, and Layer5 may change them at any time. ## Limitation of liability **To the maximum extent permitted by applicable law, Layer5 and its suppliers are not liable for any direct, indirect, punitive, incidental, special or consequential damages, or any damages at all, arising out of or connected with Blowhorn or this site.** That includes damages for loss of use, data or profits; for anything published, or not published, from your accounts; and for anything a platform does to your accounts. It applies whether the claim is based on contract, tort, negligence, strict liability or anything else, even if Layer5 was told such damages were possible. Some jurisdictions do not allow the exclusion or limitation of incidental or consequential damages, so this limitation may not apply to you. If you are dissatisfied with Blowhorn, this site or these terms, your sole and exclusive remedy is to stop using them. ## Indemnification You agree to indemnify, defend and hold harmless Layer5, its officers, directors, employees and agents, and third parties, against any losses, costs, liabilities and expenses (including reasonable attorneys' fees) arising out of your use of Blowhorn or this site, the content you publish through Blowhorn, your breach of these terms, your breach of a platform's terms, your violation of anyone else's rights, or your violation of any law. Layer5 may, at its own cost, take over the defence of any such matter, and if it does you will cooperate with it. ## Ending your use - You can stop using Blowhorn at any time: quit and delete the app, and remove the extension from Chrome. Your data in your organization's store on Layer5 Cloud is handled under Layer5's terms and privacy policy. - Layer5 may, in its sole discretion, end or restrict your access to the site and to Blowhorn, or to any part of them, at any time and without notice. Layer5 may also end early access or stop offering Blowhorn. - The sections on your content, trademarks, no warranty, limitation of liability, indemnification and governing law continue to apply after your use ends. ## Governing law To the maximum extent permitted by law, these terms are governed by the laws of the State of California. Use of Blowhorn and this site is not authorized in any jurisdiction that does not give effect to every provision of these terms, including this section. You and Layer5 may bring claims against each other only individually, and not as a plaintiff or class member in any class, collective or representative proceeding. ## General - You consent to receive communications from Layer5 electronically, and you agree that agreements, notices and other communications we provide electronically, including on this site, satisfy any legal requirement that they be in writing. - Layer5 does not knowingly collect personal information from anyone under 18. If you are under 18, you may use Blowhorn only with the permission of a parent or guardian. - These terms create no joint venture, partnership, employment or agency relationship between you and Layer5. - Layer5's performance of these terms is subject to existing laws and legal process, and nothing in them limits Layer5's right to comply with government, court or law enforcement requests. - If any part of these terms is found invalid or unenforceable, it is replaced by a valid provision that most closely matches its intent, and the rest of the terms stay in effect. - These terms, together with the AGPL-3.0 licence that covers the Blowhorn software and Layer5's terms for Layer5 Cloud, are the entire agreement between you and Layer5 about Blowhorn and this site. - These terms are written in English, and the English version governs. ## Changes Layer5 may change these terms. The current version replaces every earlier one, and the date at the top of this page says when it last changed. Every edit to this page is recorded in the [site repository's history](https://github.com/layer5io/blowhorn-site/commits/master/content/en/legal/terms.md). ## Contact Questions about these terms go to Layer5: Layer5, Inc.\ 1000 Congress Avenue\ Austin, Texas 78735\ Email: [legal@layer5.io](mailto:legal@layer5.io)\ Telephone: 512-810-8200 -------------------------------------------------------------------------------- title: "Blowhorn docs" url: https://blowhorn.ai/docs/index.md description: Tutorials, how-to guides, reference, and explanation for the Layer5 Blowhorn app for macOS. --------------------------------------------------------------------------------

Docs

# Blowhorn docs One queue. Every profile. Posted as you, from your own Chrome.
**Early access.** Blowhorn is in early access. Install and sign-in pages appear here as each step opens to everyone.
## Start here {#start}
Install Blowhorn Get the app running on your Mac. Your first post Queue a post and preview it. Nothing goes out until you say so. What Blowhorn can do on each platform Actions, limits and sign-in, platform by platform.

Worried about bans, mistakes or double posts? You're in control gathers every safety control in one place.

## Find your kind of page {#quadrants}
Tutorials Learn by doing Short lessons you follow end to end. How-to guides Reach one goal Steps first, for when you know the basics. Reference Look it up Commands, settings, messages and limits. Explanation Understand why How Blowhorn works, and why.
## Platforms {#platforms}
LinkedIn X Reddit Hacker News Slack Bluesky GitHub

Something not working? Start with Troubleshooting and Messages and exit codes.

-------------------------------------------------------------------------------- title: "Get started" url: https://blowhorn.ai/docs/tutorials/index.md description: Short lessons that walk you through Blowhorn end to end. -------------------------------------------------------------------------------- Tutorials are lessons you follow from start to finish: no choices, no skipped steps, and nothing published before you mean it. The first lessons, *Your first post with the Blowhorn app*, *Your first run from the command line*, *Put a post on a schedule*, and *Amplify a post from every profile*, arrive here as installing, signing in, and the public build open to everyone during early access. -------------------------------------------------------------------------------- title: "How-to guides" url: https://blowhorn.ai/docs/how-to/index.md description: Reach one goal: steps first, for a reader who knows the basics. -------------------------------------------------------------------------------- Each guide solves one real problem. Pick the group that matches what you want to do: - [Set up](set-up/): install, sign in, Chrome, profiles, and platform accounts. - [Publish](publish/): queue a post, preview it, publish it, reply, and amplify. - [Grow](grow/): follow, find conversations, and build an audience. - [Schedule](schedule/): jobs, pausing, and the background service. - [Measure](measure/): analytics and run history. - [Settings](settings/): defaults and how Blowhorn reaches Chrome. - [Troubleshoot](troubleshoot/): fix what stopped working. - [Manage](manage/): update and uninstall. -------------------------------------------------------------------------------- title: "Set up" url: https://blowhorn.ai/docs/how-to/set-up/index.md description: Install Blowhorn, sign in, connect Chrome, and add profiles. -------------------------------------------------------------------------------- Install the app on your Mac, sign in to Layer5 Cloud, add the Chrome extension to each Chrome profile you post from, connect your Blowhorn profiles to those Chrome profiles, and sign each profile in to each platform. Install and sign-in guides appear here as each step opens to everyone during early access. -------------------------------------------------------------------------------- title: "Publish" url: https://blowhorn.ai/docs/how-to/publish/index.md description: Queue a post, preview it, publish it, reply, and amplify. -------------------------------------------------------------------------------- Add posts to the queue, preview them with a dry run, publish them to several profiles at once, and amplify what worked: reposts, retweets, quotes, upvotes, and GitHub reactions. Reply and comment from the queue too. -------------------------------------------------------------------------------- title: "Grow your audience" url: https://blowhorn.ai/docs/how-to/grow/index.md description: Follow, find conversations, and build an audience. -------------------------------------------------------------------------------- Follow and unfollow accounts on X, Bluesky, and GitHub, find conversations on Bluesky, build a GitHub audience from a repository, and handle LinkedIn invitations: accept the welcome ones, withdraw the stale ones. -------------------------------------------------------------------------------- title: "Schedule and automate" url: https://blowhorn.ai/docs/how-to/schedule/index.md description: Jobs, pausing, and the background service. -------------------------------------------------------------------------------- Schedule a job from the Jobs screen, pause and resume posting for one row, one Mac, or every Mac, find out why a job did not run, and keep posting with the app closed through the background service. -------------------------------------------------------------------------------- title: "Measure" url: https://blowhorn.ai/docs/how-to/measure/index.md description: Analytics and run history. -------------------------------------------------------------------------------- Collect and read your analytics from the Analytics screen, and review what ran, re-run a failure, and read the report from the Runs screen. -------------------------------------------------------------------------------- title: "Settings" url: https://blowhorn.ai/docs/how-to/settings/index.md description: Defaults and how Blowhorn reaches Chrome. -------------------------------------------------------------------------------- Change your defaults, pace, excluded profiles, dry-run behaviour, notifications, and logs from Settings or the command line. Advanced readers can choose how Blowhorn reaches Chrome: when to leave the extension default, and when to attach or launch instead. -------------------------------------------------------------------------------- title: "Troubleshooting" url: https://blowhorn.ai/docs/how-to/troubleshoot/index.md description: Fix what stopped working, symptom first. -------------------------------------------------------------------------------- Start from the symptom: a sign-in that stopped working, a post that would not confirm, an extension that says it is not connected, or an organization Blowhorn cannot reach. Each fix page names the cause, the fix, and where to read more. -------------------------------------------------------------------------------- title: "Manage" url: https://blowhorn.ai/docs/how-to/manage/index.md description: Update and uninstall. -------------------------------------------------------------------------------- Update Blowhorn and the Chrome extension, and remove the app, the background service, the extension, and local data when you leave. -------------------------------------------------------------------------------- title: "Reference" url: https://blowhorn.ai/docs/reference/index.md description: Look it up: the app, the platforms, the commands, and every message. -------------------------------------------------------------------------------- Reference pages describe the machinery as it is: system requirements, the app screen by screen, what Blowhorn does on each platform, the content queue, jobs, the command reference, profiles, settings, messages and exit codes, extension permissions, plans and limits, release notes, and where to get help. The [command reference](cli/) is generated from the app itself, so it always matches the build you run. -------------------------------------------------------------------------------- title: "CLI reference" url: https://blowhorn.ai/docs/reference/cli/index.md description: Every command, the shared flags, and profile resolution. -------------------------------------------------------------------------------- # CLI reference Every command, the flags they share, and how a profile is resolved. {{< major >}}Your agent gives the order. I carry it out. Once.{{< /major >}} ## CLI Usage (Subcommand UX) Blowhorn now uses a command-first UX: ``` blowhorn [flags] ``` Legacy `--action ...` mode is removed. Use `blowhorn -h` (or `blowhorn --help`) to view help and command examples. **Every command's `--help` carries a description specific to that command and at least one worked example**, and commands with genuinely distinct modes carry one example per mode - a source, a platform, a sentinel value, or the preview-then-`--apply` pair. Where a command has a `--dry-run` or a preview posture, at least one example leads with it, so nothing in the help text sends, posts, or connects for real without saying so. Sentinel values are spelled out rather than left to be discovered: `--limit 0` means target-only mode on `follow` and "no limit" on `schedule tick`. This is a standing rule, not a one-off pass. `tests/test_cli_help_coverage.py` enforces it structurally - every registered command must expose a non-empty description and an examples epilog containing at least one example that runs that very command; no two commands may share a description or an examples block; and every example in every epilog is parsed by the real argument parser, so an example naming an option that does not exist fails the suite. A new command therefore cannot ship without contextual help and examples. CLI framework: Python `Typer` (inspired by modern CLIs like `docker`, `kubectl`, and `gh`). If `typer`/`click` are missing, `blowhorn` attempts a one-time bootstrap install via `python -m pip install typer click` using the active interpreter. Should that bootstrap fail, `blowhorn` falls back to an argparse parser built from the same help and example constants as the Typer app, and the same test file asserts the two surfaces expose the same commands with identical prose - so the fallback can never drift into a second, staler help text. For a fresh virtualenv, install the full project runtime with `pip install -r requirements.txt`; the CLI depends on `PyYAML`, `requests`, `playwright`, `tweepy`, `psycopg`, `pandas`, and `xlrd` in addition to `typer` and `click` (`gspread` and `oauth2client` are still listed only for the migration script's last step and go with it). Global flags (available on all commands): ``` --profile --platform --exclude (alias: --e) --log-dir --tee-stdout --headless --headed --chrome-launch --dry-run --pace -v, --verbose --config ``` `all` is a reserved alias, not a real profile name. When you use `--profile all`, Blowhorn expands to all configured profiles and then removes any names passed via `--exclude` / `--e`. Use `--exclude none` when you want to clear configured default exclusions for a single run. When you pass explicit profile names with `--profile your-profile,second-profile`, those named profiles are processed even if they appear in the configured exclusion list. Chrome is never run headless. With `--headless`, Chrome puts `HeadlessChrome/` in the `User-Agent` header of every request while its client hints still say Google Chrome, which tells every site the run is automated. So `--headless` now means *unattended*: the browser opens as a background window (minimized, kept out of the foreground, the same window a flagless run opens), and no one is waited for at a sign-in or challenge. Use `--headed` when you want the window visible and in front so you can finish a sign-in yourself. `blowhorn schedule tick` runs its jobs unattended but never forces visibility onto them - each job resolves its own from its params, then the environment, then Settings - so scheduled jobs never wait for someone who is not there whatever the window does. `trigger-now` runs its one row attended, so it may wait for you. `--headed` and `--headless` are one choice with two spellings, so exactly one of them survives resolution. `--headed` wins a tie, but only from an equal or higher-precedence layer: `defaults.headed: true` in `.blowhorn.yaml` does **not** cancel a `--headless` typed on the command line or set through `BLOWHORN_HEADLESS`. A run that passes `--headless` opens the background window whatever the file says. Blowhorn drives only the installed Google Chrome: `/Applications/Google Chrome.app` on macOS, `google-chrome` or `google-chrome-stable` on `PATH` or `/opt/google/chrome/chrome` on Linux ([Linux](/docs/reference/chrome/#linux); Chromium is not supported yet). When it is missing, every browser-backed command refuses with one sentence instead of starting Playwright's bundled Chromium, whose user agent and client-hint brands both read `HeadlessChrome`. The refusal comes before the browser driver is started and ends the command with exit 1: a `--profile all` run stops there rather than repeating the sentence for every profile and platform and finishing as if it had run. `--chrome-launch ` says how this run reaches that Chrome, for one run: `extension` (the default) drives Chrome through the [Blowhorn Chrome extension](/docs/reference/chrome/#the-extension-default) and starts no Playwright Chrome; `persistent` launches Blowhorn's own Chrome from `chrome.automation_data_dir` with the mapped Chrome profile directory inside it, `cdp` attaches to a Chrome that is already open (it must be `ready` per `blowhorn chrome status`; your daily Chrome asks you to Allow the first connection of the run, a [dedicated attach Chrome](/docs/reference/chrome/#a-dedicated-attach-chrome-no-allow-dialog) does not, and the run works in its own visible window), and `auto` attaches when Chrome is ready and launches otherwise, saying `using blowhorn chrome (persistent): ` once on stderr when it falls back. Resolved as the flag, then `BLOWHORN_CHROME_LAUNCH`, then `BLOWHORN_BROWSER_DRIVER`, then `defaults.chrome.launch_mode` in `.blowhorn.yaml`, then the built-in `extension`; `BLOWHORN_EXTENSION_ROLLBACK=1` selects `persistent` in place of `BLOWHORN_BROWSER_DRIVER=extension`, the file's value or the built-in; any other value is refused before anything runs, naming the four. Every browser run needs the profile mapped with `blowhorn chrome map` first, in either mode: an unmapped profile you named is `ERROR` and exit 1, one reached through `--profile all` is one `WARNING` line and the walk continues. A job `blowhorn schedule tick` or `trigger-now` runs resolves its own driver: the job's driver mode, then `BLOWHORN_BROWSER_DRIVER`, then `defaults.chrome.launch_mode`, then the built-in `extension`, with `BLOWHORN_EXTENSION_ROLLBACK=1` beating a job's `extension` as it beats `BLOWHORN_BROWSER_DRIVER=extension`, the file's value and the built-in; `blowhorn schedule tick` and `trigger-now` hand their `--chrome-launch` to every job they run, and a `tick` with none of its own leaves the launch mode unset so each job resolves its own; an explicit `--chrome-launch` on the tick still wins. `trigger-now` keeps a launch mode only when `--chrome-launch` or `BLOWHORN_CHROME_LAUNCH` chose it; otherwise its job resolves its own mode the same way. A forced `cdp` against a Chrome that is not ready is refused with `chrome status`'s next-step sentence; the run tells you to click Allow before it connects and waits up to 120 seconds for it; a connection still unanswered then is refused by name. A job `schedule tick` runs is unattended: it is told nothing and waits 20 seconds, so it fails by name rather than hangs. `trigger-now` is attended and gets the 120-second window and the notice. `blowhorn chrome status` shows the mode a run would use right now; see [Launch modes](/docs/reference/chrome/#launch-modes) and [Choose how Blowhorn reaches Chrome](/docs/how-to/settings/launch-mode/). `--pace` controls the speed of randomised delays between actions. Use a preset name (`fast` = 0.5×, `normal` = 1.0×, `slow` = 2.0×) or any positive number as a multiplier (e.g. `--pace 0.25` for quarter-speed delays, `--pace 3` for triple). The default is `normal`. Can also be set via the `BLOWHORN_PACE` env var or the `pace` key in `.blowhorn.yaml` defaults; the precedence is always the flag, then the env var, then the config key. `--pace` is a global flag: every command and subcommand accepts it, and so does the root, so `blowhorn --pace slow post --profile your-profile`, `blowhorn schedule --pace slow tick` and `blowhorn post --profile your-profile --pace slow` all mean the same thing (the flag nearest the subcommand wins when more than one is given). Commands that never sleep - `auth`, `config`, `desktop`, `service`, `store`, `uninstall`, the read-only `schedule` and `profile` subcommands, `report`, `analytics history` / `operations` - still accept it, still resolve it through the same chain and still reject an unrecognised value, but it changes nothing about how they run beyond the `Pace` column of the `-v` summary; each of them says exactly that in its own `--help` rather than implying a delay it never takes. For commands that support multi-profile execution (`post`, `accept`, `follow`, `unfollow`, `comment`, `analytics`, `report`), you can pass `--profile all` or a comma-delimited subset such as `--profile profile-a,profile-b` to run only that set of profiles. All commands accept `--dry-run`, but they do not all preview the same amount. `post`, `report`, `find`, `withdraw`, and `accept` run their real pipeline with every write suppressed, so they print what *would* happen. (`withdraw` goes further: preview is its default and it acts only when you pass `--apply` - for it `--dry-run` simply forces the preview it would have produced anyway. `accept` does the same on the received-invitations page: it opens the page, resolves the same Accept controls a real run would click, names everyone it found, and clicks nobody - see [Accepting incoming invitations](/docs/how-to/grow/accept-invitations/).) The remaining action commands - `follow`, `unfollow`, `comment`, and `analytics` - stop immediately and report `dry-run: no changes made` without doing any work. (`source --platform github` lists the audience and, on `--dry-run`, writes nothing; a real run stores one row per login and prints no email address.) (`follow --platform github` is the one exception inside that list: it has no browser to stop before, so its preview performs the real follower walk and names exactly who would be followed - see [Previewing a GitHub follow run](/docs/how-to/grow/follow-on-github/#previewing-a-github-follow-run).) (`unfollow` runs on Bluesky only, with `--platform bluesky --target `, and its `--limit` is accepted and not read. On X, or with no platform, the platform is refused by name before a browser opens and the run exits 1, dry run included: X unfollow does not run until a follow-back badge and a confirmed landing are both read. `unfollow` cannot be scheduled.) `schedule`, `desktop`, `service`, `store` and `uninstall` are not covered by that sentence: they implement their own dry-run output (for example `schedule tick --dry-run` reports the rows it would run, `service start --dry-run` prints the command it would run, `store --import --dry-run` prints the import report without writing the connection file, and `uninstall --dry-run` lists every item it would remove or keep). Either way nothing is published. Because the `COMMAND SUMMARY` footer is shown only when `-v` / `--verbose` is used, a dry run of one of the stop-early commands prints nothing at all without `-v` - except `source --platform github`, which lists the audience it would store. That silence is expected, not a failure. Top-level commands: ``` post accept withdraw follow source unfollow comment analytics report find profile config schedule desktop service store chrome auth slack uninstall ``` `blowhorn auth` is a group of three: `auth status` reports whether a Layer5 Cloud token and organization id are configured (never the token value), the api base, enforcement mode, a summary of the last entitlement attestation, and why the last read of the plan failed when it did; `auth login` is not available yet (set `BLOWHORN_CLOUD_TOKEN` instead); `auth logout` clears the local attestation cache. During early access your Blowhorn administrator sets up the token and organization id. `blowhorn store` is a single command, not a group: it reports whether this machine can reach the Blowhorn database and why not, opens the bastion tunnel when nothing is listening, and with `--import` fills the connection once from your Layer5 Cloud configuration. It exits 3, a code nothing else uses, when the store is unavailable. During early access the connection is set up by your Blowhorn administrator. `blowhorn config get ` reads one option and says which document holds it. `blowhorn uninstall` is a single command: it removes Blowhorn from this machine, in order - the background service (`ai.blowhorn.service` and the Outbox-era `io.layer5.outbox.service` / `com.outbox.scheduler`, booted out and their LaunchAgent files removed), the Chrome native host (the `com.blowhorn.chrome_extension` manifest and the Outbox-era `com.outbox.chrome_extension` / `com.outbox.actuator`, their launchers under `~/Library/Application Support/Blowhorn` and `Outbox`, and the every-profile install entries `chrome extension-install --all-profiles` wrote), the `~/.local/bin/blowhorn` launcher and the `outbox` shim (only when they carry the installer's marker), `/Applications/Blowhorn.app` and `Outbox.app` (the service label they registered is booted out with the service, and each bundle first unregisters its own Login Items entries - open at login and the background service - by running `--unregister-login-items`; a bundle whose `Info.plist` does not advertise that flag, or a call that fails, is reported `not unregistered` with System Settings > General > Login Items named as the step left to you), and the checkout's virtualenv (`--venv-dir`, else `BLOWHORN_VENV_DIR`, else `venv/`, removed only when it holds `pyvenv.cfg`; the filesystem root, the home directory, the checkout and a directory holding either are refused), `node_modules/`, `desktop/node_modules/` and `logs/` - reporting each item as `removed`, `not present`, `not checked`, `kept`, `not unregistered` or `failed`. A failed item does not stop the run; the command exits 1 at the end, as it does when a real run leaves a login item not unregistered. Local data is kept and named (the store connection file, the desktop app's state, Blowhorn's own Chrome trees under `~/.blowhorn`, the app's per-user Library state, the checkout's `cache/` and each profile's `browser/` sessions); `--purge` removes it after you type `purge` at the prompt, or with `--yes` when there is no terminal (`--json --purge` needs `--yes` outside a dry run, and `--yes` without `--purge` or `--discard-unsynced-runs` is refused), and the shared store is never touched. Nothing under a Chrome profile is read or written. It is refused by name, in a dry run too, while a scheduler tick, a service pass, an extension run, `Blowhorn.app`, a desktop app started from this checkout or, with `--purge`, a Chrome on `~/.blowhorn` or `~/.outbox` is running, and in every mode while a write-back spool (`runs/pending-store.ndjson`) this run removes - under the checkout's `logs/`, or the app's state under `--purge` - holds run records that have not reached the shared store - run `blowhorn store --replay-spool` first, or pass `--discard-unsynced-runs` when the store will never be reachable again, which names the count it deletes and asks you to type `discard` (`--yes` with no terminal or under `--json`). A spool in any other log directory is left in place and reported as kept, with its count. A probe that could not run is a refusal too, and an unreadable `engine.json` is listed as not checked. `--dry-run` lists every item and removes nothing; `--json` emits one object (`ok`, `error`, `dry_run`, `purge`, `platform`, `refusals`, `not_checked`, `items` with a `result` each, `kept`, `unsynced_runs_given_up`, `unsynced_runs_kept`, `discard_unsynced_runs`, `store_touched: false`); `--bin-dir` and `--venv-dir` mirror `install.sh`'s. `./uninstall.sh` at the checkout root (and `make cli-uninstall`) runs the same code with the system Python once the venv is gone, reading `defaults.chrome.data_dir` and `defaults.logs.directory` from `.blowhorn.yaml` itself. macOS only; Linux and any other platform are refused by name. `blowhorn chrome` is a group of eight: `chrome profiles` lists the profiles the installed Google Chrome knows (directory, display name, signed-in account, the last-used one marked, and the Blowhorn profile each is mapped to on this machine), `chrome status` reports whether Chrome is installed, running and open to remote debugging with one next-step line when something is off, `chrome suggest` prints a name-matched mapping proposal and applies nothing, `chrome map ` / `chrome unmap ` are the mapping's only writers, `chrome attach-chrome [status|start]` reports or starts a dedicated second Chrome that is open to remote debugging with no Allow dialog (`~/.blowhorn/attach-chrome`, on a port Chrome picks), writing no configuration of its own, and `chrome extension-install` writes the native-messaging host manifest Chrome launches (`com.blowhorn.chrome_extension.json`) and the executable launcher it names, removes the older `com.outbox.actuator` host manifest and launcher when those are the files it wrote, keeps and names the pre-rename Outbox host (`com.outbox.chrome_extension.json`) unless `--remove-legacy` is given, and prints the unpacked `extension/` directory to load. If you already installed the host, run the command again and reload the extension once. `--profile ` (through this machine's mapping) or `--chrome-profile ''` names one Chrome profile and prints the steps to load the extension in that profile alone. `--all-profiles` also asks Chrome to install the extension in every Chrome profile on this machine (the policy by default, or `--external` for the offer in each profile). `--dry-run` prints the paths, the host manifest, and the policy: on macOS only the `ExtensionInstallForcelist` fragment of the shared plist (other keys in that file are preserved and not printed), on Linux the exact JSON Blowhorn's own `blowhorn.json` must contain, preserved keys included. It writes nothing. It does not launch Chrome. `chrome extension-status` reports whether the extension is installed, which install route is on disk, and whether the Chrome extension answers hello. It reads the manifest, the launcher, the policy and the socket, stats the external-extensions file and does not read it, and writes only the bridge lock, exiting 0 whatever it reports. All but `chrome unmap`, `chrome extension-install` and `chrome extension-status` read two files under a Chrome data directory (`Local State`, `DevToolsActivePort`) and nothing else - `attach-chrome` reads the dedicated tree's pair rather than the daily one's; `chrome unmap` reads only the store, so a mapping can be removed when those files are gone; `chrome extension-install --all-profiles` also reads `Local State` in the configured Chrome data directory, only to name each Chrome profile there, and does not list profiles outside that directory. The policy route applies the extension to every Chrome profile on this machine. It does not read `DevToolsActivePort`; `chrome extension-status` reads the manifest, the launcher, the unpacked extension, the policy file, and the Chrome extension socket it says hello to, stats the external-extensions file and does not read it, and writes nothing but the bridge lock beside that socket; while a run holds the bridge it says so and writes no hello. Only `attach-chrome start` starts a browser, and it never drives the one it starts. All carry a stable `--json` shape; every detail is in the [Chrome reference](/docs/reference/chrome/), the procedure in [Map Blowhorn profiles to Chrome profiles](/docs/how-to/set-up/map-chrome-profiles/). The mapping is identity, never permission, and nothing that runs a browser reads it yet. The directory is `defaults.chrome.data_dir` / `BLOWHORN_CHROME_DATA_DIR`. `blowhorn profile status --chrome` shows the mapping from the profile's side. `blowhorn slack` is a group of three for an agent that answers in Slack as a person: `slack send` sends one message as one `--profile` (a thread reply from a message link, using the link's `?thread_ts=` parent, or a top-level post from a channel link), `slack whoami` asks Slack's `auth.test` who that profile's credential is, and `slack profiles` lists every profile's captured workspaces from the store without calling Slack (a profile whose `config.yaml` still carries Slack keys while the store holds no session reads `refused`, as its send would be). `send` is a dry run unless `--confirm`, and stays one under `--dry-run`, `BLOWHORN_DRY_RUN` or `defaults.dry_run` even with `--confirm` (its message then names that setting), takes its text from `--message`, `--file` or stdin (`-`) and sends it untouched, and sends a given idempotency key at most once: a key already sent replays its `ts` and permalink (the record is the store's send ledger), and a send whose answer was never recorded (interrupted, or still in flight) is reported as `outcome_unknown` (exit 4) and never resent automatically. All three take `--json`, print no secret, open no browser, and choose the credential with `--slack-auth`. Exit codes: 0 sent, replayed or previewed; 1 refused; 2 usage or key conflict; 3 store unavailable or a contended key; 4 outcome unknown; 5 rate-limited (with `retry_after`). See [Slack Messages](/docs/reference/platforms/#slack-messages). Desktop app: ```bash blowhorn desktop status blowhorn desktop install blowhorn desktop start blowhorn desktop build ``` The human-readable desktop command output groups its status details and labels successful, missing, and failed states. On an interactive terminal those states are colorized; `--json` always returns the unchanged machine-readable result, and setting `NO_COLOR` disables terminal color. `blowhorn desktop start` records the spawned pid (and its log path) under `logs/desktop/.desktop-start` in the checkout and refuses a second start while that pid is still alive, reporting the existing pid and log path and exiting non-zero. `--log-dir` moves the log file and never that record, so a second start with a different `--log-dir` is refused the same way. `--wait` records this process instead and clears the record when the app exits. A claim whose pid is dead is ignored and rewritten. If the pid is alive but no longer the desktop app (a reused pid after an unclean kill), the refusal names the file to delete: remove `logs/desktop/.desktop-start` and start again. Command → supported platforms (central mapping): | Command | Supported Platform(s) | |---------|------------------------| | `post` | `linkedin`, `x`, `reddit`, `slack`, `bluesky`, `github`, `all` - `github` is amplify-only | | `accept` | `linkedin`, `all` | | `withdraw` | `linkedin`, `all` | | `follow` | `x`, `bluesky`, `github`, `all` | | `source` | `github` | | `unfollow` | `bluesky` | | `comment` | `reddit`, `linkedin`, `bluesky`, `all` | | `analytics` | `linkedin`, `x`, `bluesky`, `all` | | `report` | `all` | | `find` | `bluesky`, `all` | | `profile` | `linkedin`, `x`, `reddit`, `slack`, `bluesky`, `all` | | `config` | `all` | | `schedule` | every platform above (see below) | | `desktop` | `all` | | `auth` | `all` (accepted and ignored) | | `service` | `all` | | `store` | `all` (accepted and ignored: the command reads no platform) | | `chrome` | `all` (accepted and ignored: the command reads no platform) | | `uninstall` | `all` (accepted and ignored: the command reads no platform) | | `slack` | `all` (accepted and ignored: the command is Slack's own) | Every action command rejects a platform outside its own row, naming the platform and listing what it does support. Each command's `--help` states its own accepted values - its row of this table, or that the option is ignored there - so `--platform` never sends you looking elsewhere for them. **An unsupported platform is rejected where it is used, and is inert everywhere else.** `blowhorn profile auth --platform github` used to report "no profiles have accounts for the requested platform(s)" - a false statement about your credentials, since GitHub has no browser login session for `profile auth` to refresh. It now gives the honest error naming `github` and listing what `profile` supports, whether you typed the platform or inherited it from `BLOWHORN_PLATFORM` or `defaults.platform`. The commands that never look at `--platform` do not reject anything: - `config path/show/get/set`, `desktop`, `service`, `store`, `chrome` and `slack` ignore the option entirely - their `--help` says so. A bad platform default can never lock you out of `blowhorn config show`, the very command you would run to find it. - `schedule` reads it as a filter over scheduled rows, and accepts every platform a row may carry. - `profile current/list/switch` never read it. - `profile auth --all` refreshes a fixed list and ignores `--platform` entirely, so an unsupported platform is inert there too. `schedule` is the one row that is not "what this command acts on". It dispatches whatever a schedule row names, and its `--platform` filters those rows, so every platform a row may carry is accepted here. A row is still validated against its own command's row of the table, so a `github` schedule row is a `follow` or a `post` (amplify). `--profile all` is supported for `post`, `accept`, `follow`, `unfollow`, `comment`, `analytics`, `report`, and `find`, and is always treated as an alias expansion (never a concrete profile). `blowhorn source` collects people into the store. `--platform github` requires at least one repeatable `--repo` (`owner/name`) and takes `--audience` (default `stargazers`: `owner`, `contributors`, `forks`, `stargazers`, `watchers`, `subscribers`, `issues`, or `all` only when you type it), and `--platform all` is rejected. `--limit` caps how many people a run examines; omitted, there is no cap. Once that many have been examined the run stops without pulling another login. A `--repo` that is blank, or that is not a single `owner/name` (a URL included), is rejected by name. A GitHub run lists that audience with the profile's stored `GH_TOKEN` and upserts one `github_contacts` row per login. `--dry-run` lists the audience, fetches no commit patches, and writes nothing. Email addresses are not printed. `source` is not a scheduled command, and it does not accept `--profile all`. Examples: ``` blowhorn post --profile your-profile blowhorn post --platform slack --target "#announcements" blowhorn post --platform slack --profile your-profile --target "#announcements" --message "Maintenance is complete" blowhorn post --platform x --amplify "https://x.com//status/" blowhorn post --platform reddit --amplify "https://www.reddit.com/r//comments///" blowhorn post --check-queue blowhorn post --check-queue --profile your-profile --platform linkedin blowhorn accept --profile your-profile --dry-run blowhorn accept --profile your-profile --limit 5 blowhorn follow --profile all --target example-handle --exclude second-profile --limit 0 blowhorn follow --profile profile-a,profile-b --target example-handle --limit 0 blowhorn follow --profile all --target example-handle,another-handle --limit 0 blowhorn follow --platform github --profile your-profile --target example-org --limit 25 blowhorn follow --platform github --profile your-profile --target example-org --limit 25 --dry-run blowhorn follow --platform github --profile all --target example-org,another-org --limit 0 blowhorn source --platform github --profile your-profile --repo owner/repo --audience stargazers --dry-run blowhorn unfollow --platform bluesky --profile your-profile --target example.bsky.social --dry-run blowhorn comment --profile all --exclude second-profile blowhorn comment --profile your-profile --platform linkedin --target "https://www.linkedin.com/feed/update/urn:li:activity:1234/" --message "Great post!" blowhorn analytics --profile your-profile --lookback 365 blowhorn analytics --platform x --profile your-profile,second-profile blowhorn analytics --platform x --profile all --exclude "" blowhorn analytics --platform x --profile all --exclude second-profile blowhorn analytics operations --group-by host --since 30d blowhorn report --dry-run blowhorn schedule validate --profile all blowhorn schedule list --due-only --profile all blowhorn schedule tick --profile all --limit 5 blowhorn schedule trigger-now 7 blowhorn schedule bootstrap --dry-run blowhorn schedule list --json blowhorn schedule pause --all --reason "LinkedIn challenge on your-profile" blowhorn schedule pause --row 7 blowhorn schedule resume --row 7 blowhorn schedule why 7 --json blowhorn schedule get 7 --json blowhorn schedule upsert --payload '{"Command":"report","Profile":"all","Run At":"2026-03-12 08:00:00"}' --json blowhorn schedule delete 7 --json blowhorn desktop status --json blowhorn service status --json blowhorn service install --dry-run blowhorn service tick-now --follow blowhorn service cancel --dry-run blowhorn service adopt --by file --dry-run blowhorn service stop --json blowhorn chrome profiles blowhorn chrome status --json blowhorn chrome suggest blowhorn chrome map your-profile 'Profile 12' blowhorn chrome unmap your-profile blowhorn chrome attach-chrome start --dry-run blowhorn profile status --chrome ``` `service install`, `service update` and `service stop` wait for the label to leave launchd after a bootout - `launchctl bootout` returns before the resident service has exited, and a bootstrap inside that window fails. The wait is bounded (a fixed poll interval times a maximum number of attempts covering a full drain); `stop` prints what it is waiting on and carries `stopped`, `waited_s` and the pass's pid in `--json`, and every one of the three exits 1 rather than bootstrapping over a label that has not gone. Each `launchctl` call is itself bounded at 60 s. For `follow --profile all`, if any `--target` handle matches one of your configured profile handles **on the platform being run**, that profile is auto-excluded to prevent self-targeting. The match is per platform: owning the X handle `yourhandle` says nothing about a GitHub run, which compares against the roster's `GitHub` handle instead. In `--limit 0` target-only mode, `--target` also accepts a comma-delimited list such as `example-handle,another-handle`. ### `--profile` and `--exclude` {#section-profile-and---exclude} Naming profiles explicitly makes `--exclude` inert: `--profile your-profile` runs `your-profile` even when the config excludes it, and only `--profile all` defers to the exclusion list. Commands say which way it went - `Excluding profiles: …` when the list is applied, `Ignoring configured exclusions (…): --profile named the profiles to run explicitly.` when it is not. They used to print the first line either way, so a run could announce excluding a profile it was about to process. `blowhorn follow` folds its target-owned auto-exclusion into the same list, so an explicit `--profile` overrides that too. Multi-platform commands that filter per profile inside `browser.iterate_profiles` are the exception: those exclusions always apply, and that command says so plainly. Profile workflows: ``` blowhorn profile current blowhorn profile list blowhorn profile status # what each profile is set up for, no browser blowhorn profile status --check-sessions # read-only session probes, recorded per profile blowhorn profile status --profile your-profile --check-platform github # one platform only: your-profile's GH_TOKEN in the store against GitHub, no browser blowhorn profile status --chrome # adds the chrome profile each one is mapped to here blowhorn profile get your-profile # settings with secrets masked (GH_* from the store) blowhorn profile set your-profile EMAIL=you@example.com blowhorn profile set your-profile GH_TOKEN=ghp_yourtoken # stored in the store, never in config.yaml blowhorn profile set new-profile --create EMAIL=new@example.com # its row in the store's profiles (bound to the Layer5 Cloud user holding the email), then the directory from the template blowhorn profile set new-profile --create EMAIL=new@example.com --subject # name the Layer5 Cloud user when the email alone cannot blowhorn profile set new-profile --register # add a profile that exists only under profiles/ to the store's profiles blowhorn profile delete new-profile --yes # retires the store row, then moves profiles/new-profile to profiles/.trash/ blowhorn profile switch your-profile blowhorn profile auth --platform x # a sign-in records the session as valid, checked now blowhorn profile auth --platform bluesky --profile your-profile # prompts for handle + app password; saved only if Bluesky accepts the login printf '%s\n' "$APP_PASSWORD" | blowhorn profile auth --platform bluesky --profile your-profile --handle you.bsky.social --app-password-stdin --json blowhorn profile auth --platform reddit --profile all --exclude "" blowhorn profile auth --all ``` -------------------------------------------------------------------------------- title: "Explanation" url: https://blowhorn.ai/docs/explanation/index.md description: Understand why: context and trade-offs, no procedures. -------------------------------------------------------------------------------- Explanations give the reasoning behind Blowhorn's behaviour: how a post travels from the queue to the platforms, where your data lives, who may post as whom, why Blowhorn uses your own Chrome, how it paces itself, how it avoids repeat posts, how scheduling works, what runs when the app is closed, and what the analytics numbers mean. Read these to build judgment. When you want to act, follow a [how-to guide](../how-to/) instead. -------------------------------------------------------------------------------- title: "Install the app" url: https://blowhorn.ai/docs/how-to/set-up/install-the-app/index.md description: Download the signed Mac app from blowhorn.ai and verify it. -------------------------------------------------------------------------------- # Install the app Download the signed Blowhorn app for macOS, verify it, install it, and open it. **Early access:** the app does not bundle its command-line engine yet. On first run it asks for a checkout of Blowhorn, which your Blowhorn administrator gives you access to (see [First run](#first-run)). ## Download Download the latest build from the [blowhorn-site releases](https://github.com/layer5io/blowhorn-site/releases), the same place the [blowhorn.ai](https://blowhorn.ai) download button links: - [Blowhorn-mac.dmg](https://github.com/layer5io/blowhorn-site/releases/latest/download/Blowhorn-mac.dmg) - always the latest build, for Apple silicon and Intel Macs. - [SHA256SUMS.txt](https://github.com/layer5io/blowhorn-site/releases/latest/download/SHA256SUMS.txt) - the checksums for that release. Verify the download before you open it. With both files in the same folder: ```bash shasum -a 256 -c SHA256SUMS.txt --ignore-missing ``` The line for `Blowhorn-mac.dmg` must read `OK`. ## Install 1. Open the downloaded `.dmg`. 2. Drag the app to **Applications**. 3. Open it from Applications. A notarized build opens without a warning. You need macOS 13 or newer and Google Chrome. Chrome is not bundled: Blowhorn drives the Chrome profiles you already sign in to, through its Chrome extension, which you add to each Chrome profile you post from ([Install the Chrome extension](/docs/how-to/set-up/install-the-chrome-extension/)). ## First run The app opens on its Setup screen and checks, in order, the Blowhorn checkout, its Python environment, the connection to your organization's data, your Chrome profile mappings ([Map Blowhorn profiles to Chrome profiles](/docs/how-to/set-up/map-chrome-profiles/)), and the background service. Each row says what it found, or what to do next. Every row is described under Setup in [The Blowhorn app, screen by screen](/docs/reference/use-the-desktop-app/#setup). Then check sessions and run a dry-run post. Nothing is published until you post without `--dry-run`. **Early access:** signing in with your Blowhorn account and a bundled engine that needs no checkout are still being built. When they ship, they replace the checkout and Python rows. ## Update Updates come from the same releases page. The app installs an update and relaunches when this machine can reach the releases; otherwise it opens the release page and you download the newer `.dmg` and drag the app over the old copy. The background service and your settings are kept. ## Related - [What runs when the app is closed](/docs/explanation/distribution/) - the background service, quitting, and updates. - [The Blowhorn app, screen by screen](/docs/reference/use-the-desktop-app/) - every screen, including Setup. - [Install the Chrome extension](/docs/how-to/set-up/install-the-chrome-extension/) - the extension each mapped Chrome profile needs. - [Uninstall Blowhorn](/docs/how-to/manage/uninstall/) - remove it cleanly -------------------------------------------------------------------------------- title: "Install the Chrome extension" url: https://blowhorn.ai/docs/how-to/set-up/install-the-chrome-extension/index.md description: Add the Blowhorn extension from the Chrome Web Store, or load it unpacked during early access. -------------------------------------------------------------------------------- # Install the Chrome extension Blowhorn drives Chrome through its Chrome extension. Chrome keeps an extension inside each Chrome profile, and the extension drives only the profile it is installed in. Install it in every Chrome profile you post from. **Early access:** the Chrome Web Store listing is awaiting review. Until it is approved, load the extension unpacked as described below. ## From the Chrome Web Store Open the [Blowhorn listing](https://chromewebstore.google.com/detail/fcflpiagmpcfifgknledeknedfopnapf) (item `fcflpiagmpcfifgknledeknedfopnapf`) in each Chrome profile you post from and choose **Add to Chrome**. Then confirm the extension answers: ```bash blowhorn chrome extension-status ``` An answered hello means the extension is loaded in some Chrome profile and talking. It does not name which profile. ## Load it unpacked in one Chrome profile When only one Chrome profile needs the extension, write the native host and follow the printed steps. Through the Blowhorn mapping: ```bash blowhorn chrome extension-install --profile kate ``` By the Chrome profile directory, as `blowhorn chrome profiles` prints it: ```bash blowhorn chrome extension-install --chrome-profile 'Profile 12' ``` `--chrome-profile` can be given more than once. Either flag with `--all-profiles` is refused. A `--profile` inherited from `BLOWHORN_PROFILE` or `defaults.profile` does not count: type it on this command. The printed steps are: switch to that Chrome profile, open `chrome://extensions`, turn on Developer mode, choose **Load unpacked**, select the `extension/` directory the command names, then check with `blowhorn chrome extension-status`. There is no file the command can write that installs an extension in one profile only, so this route always ends with those clicks. See the files first, and write nothing: ```bash blowhorn chrome extension-install --profile kate --dry-run ``` ## Every Chrome profile on this machine **Every profile** (the default) writes Chrome's `ExtensionInstallForcelist`. Chrome installs the extension the next time each profile starts, with no click in that profile. That is every Chrome profile on this machine, including ones no Blowhorn profile uses. Chrome shows **Managed by your organization**. macOS asks for an administrator password once. Cancelling that dialog writes nothing. ```bash blowhorn chrome extension-install --all-profiles ``` **Ask in each profile** writes Chrome's external-extensions file instead, so Chrome offers the install in each profile and you click once there. There is no managed banner. ```bash blowhorn chrome extension-install --all-profiles --external ``` On Linux both files live where only root can write. Blowhorn never asks for root and refuses to run under `sudo`: a write it cannot make is refused with the file, its exact content, and the one `sudo` command that writes that file. Write that file, then run the plain install as yourself for the host. ## See which route is on disk ```bash blowhorn chrome extension-status ``` The report includes `Install route: policy`, `external`, or `none`. A policy line ends `not confirmed in chrome`; an external line ends `waiting for your click`. Those lines say what Chrome was asked to do, never that a profile's copy was confirmed. ## From the desktop app Settings has one **chrome extension** card. The segmented control is the route: **every profile** or **ask in each profile**. The button stays **install chrome extension** either way. After the command, the card lists one row per Chrome profile with the same words the terminal prints. ## Move an install made before the extension id changed Installs made before the current store item used the retired id `alogghnpabnbkmhgeceffnlcbcnlpoap`, which has no store listing. After you update, the native host accepts only the new id, so an extension still running under the old id cannot connect until you move it: 1. Run the install again with the flags you used the first time. 2. In each Chrome profile that loaded the extension unpacked, open `chrome://extensions`, remove the old copy, and choose **Load unpacked** again with the `extension/` directory. 3. If you used `--all-profiles`, remove the old id's entry by hand: the command adds and removes only the current id. 4. Check with `blowhorn chrome extension-status`. Your profile mappings (`blowhorn chrome map`) do not name the extension id and do not change. ## Remove the host from before the rename A plain install keeps `com.outbox.chrome_extension.json` when it is present, and names it: a Chrome profile still running the old extension connects through it. Load the Blowhorn extension in that profile, then remove the old host with: ```bash blowhorn chrome extension-install --remove-legacy ``` That flag warns that a profile still on the old extension loses its connection until the Blowhorn extension is loaded there. ## Related - [Map Blowhorn profiles to Chrome profiles](/docs/how-to/set-up/map-chrome-profiles/) - which Chrome profile each Blowhorn profile belongs to. - [Chrome reference](/docs/reference/chrome/) - the flags, the files, and the JSON shapes. - [Requirements](/docs/reference/requirements/) - what each Mac needs -------------------------------------------------------------------------------- title: "Map Blowhorn profiles to Chrome profiles" url: https://blowhorn.ai/docs/how-to/set-up/map-chrome-profiles/index.md description: Say which real Chrome profile each Blowhorn profile belongs to on this machine. -------------------------------------------------------------------------------- # Map Blowhorn profiles to Chrome profiles Tell Blowhorn which of your real Google Chrome profiles each Blowhorn profile belongs to on this machine. Every run that drives a browser reads the mapping first and refuses a profile without one, so mapping is the one-time setup every browser run depends on. A mapping is identity, never permission: it does not make a profile eligible for any platform. Eligibility is credential existence, decided in the store ([Eligibility](/docs/explanation/eligibility/)). Nothing in this guide runs a browser. What each command prints and its JSON shape are in the [Chrome reference](/docs/reference/chrome/). How a run then reaches Chrome is [Choose how Blowhorn reaches Chrome](/docs/how-to/settings/launch-mode/): by default runs drive Chrome through the Blowhorn extension, with no Attach approval and no launched browser. ## From the desktop app Each profile on **Profiles** has a **chrome** heading. It names the Chrome profile the mapping records and whether that mapping holds. **Change** or **map** opens the Chrome profiles list; a directory mapped to someone else is disabled. **Confirm** maps the same directory again when Chrome's name or account has changed. A deleted Chrome profile has no confirm: pick another. The Setup screen's **chrome profiles** row reports the same state for every profile at once. ## Before you start - The store is reachable from this machine: `blowhorn store` says so. The mapping lives there, keyed by this machine's hostname, so it is the same on every terminal and in the desktop app on this machine, and does not follow you to another machine. - The Blowhorn profile is in the store's profiles. `blowhorn chrome suggest` lists every profile the store knows; a name missing there cannot be mapped. - Chrome's profile list is readable: `blowhorn chrome profiles` lists it. Chrome does not have to be running. ## See what Chrome has ```bash blowhorn chrome profiles ``` ``` chrome profiles (3) in /Users/you/Library/Application Support/Google/Chrome directory name account mapped to (on studio) Default Person 1 - - * Profile 2 Ada ada@example.com - Profile 5 Grace grace@example.com - * last used ``` The **directory** column is the name a mapping records, exactly as printed. Chrome silently creates a new, empty profile for any directory name it does not know, so `blowhorn chrome map` refuses a directory that is not in this list rather than recording it. ## Let Blowhorn propose a mapping ```bash blowhorn chrome suggest ``` ``` chrome mapping proposal for studio (3 profiles); nothing is applied profile directory name account note ada Profile 2 Ada ada@example.com name match grace Profile 5 Grace grace@example.com name match marcus - - - none to apply a row you agree with: blowhorn chrome map ada 'Profile 2' blowhorn chrome map grace 'Profile 5' ``` A row is proposed when exactly one Chrome profile's display name matches the Blowhorn profile's name (case and punctuation ignored). Two matches are named as ambiguous and nothing is proposed; no match is `none`; a profile that already has a mapping keeps it and says so. The command applies nothing. Copy the `blowhorn chrome map` line for each row you agree with. ## Map a profile ```bash blowhorn chrome map ada 'Profile 2' ``` ``` mapped profile ada to chrome profile 'Profile 2' 'Ada' (ada@example.com) on studio ``` The command checks three things and refuses with one sentence and exit 1 when any fails: the Blowhorn profile is in the store's profiles (`all` is every profile, not one, so map each profile by name), Chrome has that directory, and the directory is not already mapped to another Blowhorn profile on this machine. Then it records the display name and signed-in account Chrome shows for the directory and prints what it recorded. Quote a directory name that contains a space. Add `--dry-run` to run the checks and record nothing; add `--json` for the recorded row as JSON. Mapping a profile that already has a mapping replaces it. Mapping it to the directory it already has refreshes the recorded name and account, which is how you confirm a profile after Chrome's name or account changed. A directory still held by a profile you have since deleted is released the moment you map another profile to it; the command says so. ## Check the mappings ```bash blowhorn profile status --chrome ``` ``` | Profile | Linkedin | X | ... | Chrome | Notes | | ada | valid | - | ... | Profile 2 | | | grace | - | - | ... | Profile 5 (drift) | | | marcus | - | - | ... | - | | ``` The Chrome column shows the mapped directory and, when its recorded identity no longer holds, why: | shown | meaning | what to do | | --- | --- | --- | | `Profile 2` | mapped, and the name and account Chrome shows still match the ones recorded | nothing | | `Profile 5 (drift)` | Chrome now shows a different display name or signed-in account for that directory | check the profile in Chrome, then `blowhorn chrome map grace 'Profile 5'` to confirm the new identity | | `Profile 5 (missing)` | Chrome no longer has that directory | `blowhorn chrome profiles`, then map the profile again | | `Profile 5 (unknown)` | Chrome's profile list could not be read, so the identity cannot be checked | `blowhorn chrome status`, then fix what it names | | `-` | no mapping on this machine | `blowhorn chrome map ` | `blowhorn chrome profiles` shows the same thing from Chrome's side, in its `mapped to` column. A profile whose mapping does not hold is refused by name: `ERROR` and a non-zero exit when you named the profile, one `WARNING` line and the walk continues when `--profile all` reached it. Nothing falls back to another profile or to a Blowhorn-owned folder. ## Remove a mapping ```bash blowhorn chrome unmap ada ``` Only the store's row goes. Nothing under Chrome's directory is touched, and the Chrome profile keeps its sessions. ## The Chrome extension in that profile A mapping does not install the Chrome extension. Chrome keeps an extension per Chrome profile, and the Blowhorn extension drives only the profile it is installed in, so install it in every Chrome profile you post from. ## When the store cannot be reached `chrome profiles`, `chrome map`, `chrome unmap`, `chrome suggest` and `profile status --chrome` each read the store, so each ends with the store's one named sentence and exit 3 when it is unreachable, even when Chrome's profile list cannot be read either, because the store is asked first. `chrome status` never opens the store. ## Related - [Sign a profile in to each platform](/docs/how-to/set-up/platform-accounts/) - after the mapping, the sessions and credentials each platform needs. - [Chrome reference](/docs/reference/chrome/) - every `chrome` command and JSON shape. - [Eligibility](/docs/explanation/eligibility/) - why a mapping alone lets a profile post nowhere. -------------------------------------------------------------------------------- title: "Sign a profile in to each platform" url: https://blowhorn.ai/docs/how-to/set-up/platform-accounts/index.md description: Browser sign-ins, tokens, and app passwords each profile needs before it can post. -------------------------------------------------------------------------------- # Sign a profile in to each platform One section per platform. LinkedIn, X, and Reddit sign in by hand in your Chrome; Bluesky takes a handle and an app password; GitHub takes a token; Hacker News takes a username and password; Slack takes a captured session. Do the mapping first ([Map Blowhorn profiles to Chrome profiles](/docs/how-to/set-up/map-chrome-profiles/)): every browser sign-in below opens in the profile's mapped Chrome. ## LinkedIn, X, Reddit Each keeps a persistent browser session. Refresh it with `profile auth`. With the username and password in the profile's `config.yaml` (`LI_USERNAME` and `LI_PASSWORD`, `X_USERNAME` and `X_PASSWORD`, `RDDT_USERNAME` and `RDDT_PASSWORD`), the login is attempted for you; otherwise the browser opens and you sign in by hand. ```bash blowhorn profile auth --platform linkedin --profile ada blowhorn profile auth --platform x --profile ada blowhorn profile auth --platform reddit --profile ada ``` With `--headless` the browser still opens as a background window and only the automated login is attempted; the manual fallback is skipped. A CAPTCHA or second-factor challenge always hands back to you: unattended runs never wait for a person. ## Bluesky Log in with a handle and an app password. The pair is stored only when Bluesky accepts it, so a refused credential is never saved. ```bash blowhorn profile auth --platform bluesky --profile ada ``` In a terminal it asks for both (the stored handle is the default; a blank app password keeps the stored one). Supply them without a prompt with `--handle` and `--app-password-stdin`. Otherwise the stored pair is logged in with and nothing is written. Make the app password in your Bluesky account settings; your main password never goes here. ## GitHub Store a personal access token as the profile's `GH_TOKEN`. It lands in the profile's GitHub credential in the store, never in `config.yaml`: ```bash blowhorn profile set ada GH_TOKEN= ``` Then validate it without following or reacting to anything: ```bash blowhorn profile status --profile ada --check-platform github ``` The check reports the login the token signs in as and its scopes. A classic token needs `user:follow` to follow and `repo` to react; a fine-grained token needs read and write on Followers to follow, and on Issues (and Pull requests for PR targets) to react. ## Hacker News Store the login with `profile set`. Both keys are needed; the password is read from the store and never from `config.yaml`: ```bash blowhorn profile set ada HN_USERNAME= HN_PASSWORD= ``` Check it landed, with the password shown only as set or not set: ```bash blowhorn profile get ada ``` ## Slack Capture your signed-in web session into the profile's Slack credential in the store: ```bash blowhorn profile auth --platform slack --profile ada ``` A browser window opens; sign in to Slack, and the session is extracted. No Slack app install is needed. To capture a new workspace, name it: ```bash blowhorn profile auth --platform slack --profile ada --workspace acme.slack.com ``` Omit `--workspace` to refresh every workspace already listed under `slack_workspaces` in that credential. ## Related - [Map Blowhorn profiles to Chrome profiles](/docs/how-to/set-up/map-chrome-profiles/) - the mapping every browser sign-in opens in. - [Post content](/docs/how-to/publish/post-content/) - publish once every platform is signed in. - [Profile reference](/docs/reference/profiles/) - where each credential is stored. - [Eligibility](/docs/explanation/eligibility/) - a credential is permission. - [Requirements](/docs/reference/requirements/) - what each Mac needs -------------------------------------------------------------------------------- title: "Post content" url: https://blowhorn.ai/docs/how-to/publish/post-content/index.md description: Preview and publish queued posts across profiles and platforms. -------------------------------------------------------------------------------- # Post content Publish the rows waiting in the content queue. ## Check what a run would pick up Nothing is published, nothing is written back: ```bash blowhorn post --check-queue blowhorn post --check-queue --profile marcus --platform linkedin ``` `--check-queue` is read-only: it opens no browser and reaches no platform. ## Publish the queue ```bash blowhorn post --profile marcus # every platform with pending rows blowhorn post --platform linkedin --profile marcus --headless # unattended: a background window, never headless blowhorn post --profile all --exclude marcus,kanvas # fan out, holding profiles back ``` `--profile all` leaves out the excluded profiles (`--exclude`, else `BLOWHORN_EXCLUDE`, else `defaults.exclude`) and prints `Excluding profiles: …` when it does. To post as one of them, name it: `--profile marcus`. Rehearse first whenever the result matters. A dry run goes end to end without publishing and without marking the row done: ```bash blowhorn post --profile marcus --dry-run ``` A dry run still signs in where a real run would: on Hacker News it opens the browser and signs in exactly as a real run does, then stops short of the submit. ## Amplify or reply from the command line On X and Reddit, `--amplify` with the post URL amplifies it for this run (retweet, quote, or upvote), overriding the row's `Amplify` column. Replies ride on the row's `Comment on` column with `Message Text`. The full forms are [Amplify an existing post](/docs/how-to/publish/amplify-a-post/) and [Reply to a post](/docs/how-to/publish/reply-to-a-post/). GitHub amplify and the one-off Slack message need no row at all; each has its own page. ## Slow a long unattended run down ```bash blowhorn post --profile all --pace slow ``` `--pace` takes `fast`, `normal`, `slow`, or a numeric multiplier such as `2.0`. ## Attaching an image to a LinkedIn post A row that fills `Image URL`, `GDrive Link`, or `Video URL` attaches that file to the post. Two things are worth knowing when such a post fails: - A row whose text holds a URL and an image keeps the image: Blowhorn removes the link preview LinkedIn generates first, because the preview takes the slot the image needs. A row with a URL and no image keeps its preview. - A post never goes out without the image it asked for. If the file cannot be attached, the row fails and stays pending for a later run. Re-run once the cause is understood rather than filling in `Date Promoted` to silence it. ## Related - [Manage the content queue](/docs/how-to/publish/manage-the-content-worksheet/) - adding and editing the rows a run reads. - [Amplify an existing post](/docs/how-to/publish/amplify-a-post/) - reposts, retweets, upvotes, GitHub reactions. - [Reply to a post](/docs/how-to/publish/reply-to-a-post/) - comments and replies. - [Send a Slack message](/docs/how-to/publish/send-a-slack-message/) - the one-off message that needs no row. - [`blowhorn post` reference](/docs/reference/post/) - every flag, what "pending" means, and what a run writes back. - [You're in control](/docs/explanation/youre-in-control/) - what stops a run going wrong -------------------------------------------------------------------------------- title: "Manage the content queue" url: https://blowhorn.ai/docs/how-to/publish/manage-the-content-worksheet/index.md description: Add, edit, and track queued posts in the Content screen and from the command line. -------------------------------------------------------------------------------- # Manage the content queue Everything you publish waits in the content queue first. Add rows in the desktop app's Content screen, from the command line, or with the capture bookmarklet. `blowhorn post` publishes from these rows and nothing else. ## The columns a run reads | Column | What to put there | | --- | --- | | `Platform` | One `blowhorn post` publishes to: `linkedin`, `x`, `reddit`, `hn`, `slack`, `bluesky`, `github` (`twitter` and `bsky` also work) | | `Profile` | Who posts it, or `all`. Never blank: a blank profile is refused rather than read as `all` | | `Destination` | Where it goes: `r/` on Reddit, a link URL on Hacker News, the workspace on Slack (the channel goes in `Amplify`) | | `Message Text` | The post itself | | `Title` | Hacker News and Reddit titles | | `Approved?` | `yes` to let a run pick the row up | | `Promote On` | `YYYY-MM-DD HH:MM:SS` in this machine's own zone; blank means now | | `Image URL`, `GDrive Link`, `Video URL` | One attachment for the post | | `Amplify` | URL of existing content to amplify instead of publishing ([Amplify an existing post](/docs/how-to/publish/amplify-a-post/)) | | `Comment on` | URL to reply to, with the reply in `Message Text` ([Reply to a post](/docs/how-to/publish/reply-to-a-post/)) | | `Date Promoted`, `Promotion Link` | What the run writes back. Clear `Date Promoted` to queue the row again | Row numbers are the queue's own, stable for the life of a row: deleting a row never renumbers the rest, so a row number is safe to keep and script against. ## Row states `blowhorn content list` reports each row's state, decided by the same rules that make a row pending for a post run: - `pending`: a run will pick it up. - `scheduled`: `Promote On` is in the future. - `draft`: `Approved?` is not `yes`. - `done`: `Date Promoted` is set. - `unread`: the submit was clicked and nothing afterwards confirmed it (`Date Promoted` says `clicked, outcome not read`). The row is not pending and not done, and no run posts it again. Check the platform, then clear `Date Promoted` to try it again. - `invalid`: a `Promote On` that will not parse. A post run skips it with a warning. - `unsupported`: a `Platform` `blowhorn post` cannot publish to. ## From the command line ```bash # Every row with its state. blowhorn content list # Only what a run would pick up now, as JSON. blowhorn content list --pending --json # One profile's rows; only X rows. blowhorn content list --profile kate blowhorn content list --platform x # One row, every column, its state and its problems. blowhorn content get 12 --json # Create a row: it takes the next row number, which is never reused. blowhorn content upsert --payload '{"Platform":"linkedin","Profile":"kate","Message Text":"Our v2.0 release is out","Approved?":"yes"}' # Update in place: the payload is merged over the row; columns it does not # name are left as they were. blowhorn content upsert --row 12 --payload '{"Approved?":"yes"}' # Delete (requires --yes; --dry-run previews first). blowhorn content delete 12 --yes ``` A write refuses rather than guesses: an unknown `Platform`, a `Profile` the store does not know, a blank `Profile`, an unparseable `Promote On`, and unknown fields are all refused with the reason named. ## Capture a page from the browser The bookmarklet turns the page you are reading into a new content entry without retyping it. It holds no credential and reaches no store: it hands the page's address, title, and any selected text to the desktop app, which opens the Content screen on a new entry pre-filled from it. To install it: in the app, **Content → Bookmarklet → Copy code**, then add a bookmark in your browser named `Blowhorn: capture` and paste the code as its address. or paste this: ``` javascript:(function(w,d){var s=String((w.getSelection&&w.getSelection())||"").slice(0,4000);var u="blowhorn://content/new?url="+encodeURIComponent(String(w.location.href).slice(0,2048))+"&title="+encodeURIComponent(String(d.title||"").slice(0,300))+"&text="+encodeURIComponent(s);var left=false;function gone(){left=true}w.addEventListener("blur",gone);d.addEventListener("visibilitychange",gone);w.setTimeout(function(){w.removeEventListener("blur",gone);d.removeEventListener("visibilitychange",gone);if(left||d.visibilityState==="hidden"||(d.hasFocus&&!d.hasFocus()))return;var n=d.createElement("div");n.setAttribute("role","alert");n.textContent="Blowhorn did not open. Install or start the Blowhorn desktop app, then try the bookmarklet again.";n.style.cssText="position:fixed;top:16px;right:16px;z-index:2147483647;max-width:360px;padding:10px 14px;background:#111;color:#f5f5f5;font:13px/1.4 -apple-system,system-ui,sans-serif;border:1px solid #444;border-radius:6px;box-shadow:0 4px 16px rgba(0,0,0,.4);cursor:pointer";n.onclick=function(){n.parentNode&&n.parentNode.removeChild(n)};d.body.appendChild(n);w.setTimeout(function(){n.parentNode&&n.parentNode.removeChild(n)},8000);},2000);w.location.href=u;})(window,document) ``` You choose what the link is for (**post the link**, **amplify it**, **reply to it**: the URL lands in `Message Text`, `Amplify`, or `Comment on`), pick the platform and profile, and the editor's add button queues the entry. Nothing reaches the queue until you add it. ## Related - [Post content](/docs/how-to/publish/post-content/) - publish the rows. - [Amplify an existing post](/docs/how-to/publish/amplify-a-post/) - the `Amplify` column. - [Reply to a post](/docs/how-to/publish/reply-to-a-post/) - the `Comment on` column. - [`blowhorn post` reference](/docs/reference/post/) - what "pending" means and what a run writes back. -------------------------------------------------------------------------------- title: "Amplify an existing post" url: https://blowhorn.ai/docs/how-to/publish/amplify-a-post/index.md description: Repost, retweet, quote, upvote, or react to a post from every profile. -------------------------------------------------------------------------------- # Amplify an existing post Repost, retweet, quote, upvote, or react to something already published, from a content row. Put the URL in the row's `Amplify` column and set `Message Text` only when you add your own words: | Platform | `Amplify` | `Message Text` | Result | | --- | --- | --- | --- | | LinkedIn | post URL | empty | repost | | LinkedIn | post URL | your words | repost with thoughts | | X | post URL | empty | retweet | | X | post URL | your words | quote | | Reddit | thread URL | empty, and `Comment on` empty | upvote | | Bluesky | post URL | empty | repost | | GitHub | issue or PR URL | ignored | all five reactions | {{< major >}}Your launch deserves more than nine likes. Fall in, profiles.{{< /major >}} ```bash blowhorn post --platform linkedin --profile marcus --dry-run blowhorn post --platform linkedin --profile marcus ``` On X and Reddit, `--amplify` with a URL overrides the row's `Amplify` column for the whole run. You still need the rows: the flag retargets them, it does not replace them. On GitHub the flag needs no row at all ([Amplify an issue or pull request on GitHub](/docs/how-to/publish/amplify-on-github/)). ## What Blowhorn refuses to amplify twice On LinkedIn and X, a profile never amplifies its own post: when the `Amplify` URL names the acting profile's own author, that profile is left out of the run. The guard reads the author against the profile's handle, so a LinkedIn or X amplify row whose profile has no handle stays pending with a warning naming the missing handle, instead of running blind. Both halves of a LinkedIn amplify are guarded before anything is clicked: a post that already carries the reaction is left alone, and a post the profile already reposted is recorded as done with the amplify URL as its link. Re-running a row that already went out is safe. A Bluesky repost carries no such guard: once the row is done, leave it done. ## When the store cannot be updated The `Date Promoted` write happens after the amplify went out, so losing it does not undo anything: it hides it. The row stays pending and the next run acts again, which the LinkedIn guards above make a no-op rather than a duplicate. The write is retried on transient failures; when it still fails, one loud `!!!` line names the row, the profile, and exactly what went out publicly, the row counts as failed, and the rest of that profile's queue is not attempted. Re-run once the store is reachable. ## Comments are a different column Replying uses `Comment on`, never `Amplify` ([Reply to a post](/docs/how-to/publish/reply-to-a-post/)). A Reddit row whose `Comment on` is a Reddit post or comment URL and whose `Message Text` is set comments, even when it also carries a thread URL in `Destination` or `Amplify`. With a malformed `Comment on` or no `Message Text`, the row upvotes or posts instead. ## Related - [Amplify an issue or pull request on GitHub](/docs/how-to/publish/amplify-on-github/) - the GitHub reaction set, scopes, and previews. - [Reply to a post](/docs/how-to/publish/reply-to-a-post/) - the `Comment on` column. - [Post content](/docs/how-to/publish/post-content/) - running `blowhorn post` in general. - [Why a public action is never repeated](/docs/explanation/reliability-and-anti-bot-design/) - the rule behind the guards. - [You're in control](/docs/explanation/youre-in-control/) - the limits on every amplify -------------------------------------------------------------------------------- title: "Amplify an issue or pull request on GitHub" url: https://blowhorn.ai/docs/how-to/publish/amplify-on-github/index.md description: React to GitHub issues and pull requests from every profile. -------------------------------------------------------------------------------- # Amplify an issue or pull request on GitHub React to an issue or pull request from every eligible profile. `blowhorn post --platform github` does exactly one thing: it adds the uplifting reaction set to the target, 👍 `+1`, 😄 `laugh`, 🎉 `hooray`, ❤️ `heart`, 🚀 `rocket`, each under that profile's own `GH_TOKEN`. All five, every time. It cannot publish a new post to GitHub, and it refuses rather than run with no target. No browser opens. GitHub runs over the REST API end to end. ## Before the first run Store a token as the profile's `GH_TOKEN`: ```bash blowhorn profile set marcus GH_TOKEN= ``` The token lands in the profile's GitHub credential in the store, never in `config.yaml`. A classic token needs the `repo` scope; a fine-grained token needs read and write on **Issues**, and on **Pull requests** when the target is a PR. Then check the token without reacting to anything: ```bash blowhorn profile status --profile marcus --check-platform github ``` ## React from the command line ```bash # Every eligible profile reacts to one issue. No content row needed. blowhorn post --platform github --profile all --amplify "https://github.com/layer5io/layer5/issues/1234" # A pull request is an issue to GitHub's API, so a PR URL routes the same way. blowhorn post --platform github --profile all --amplify "https://github.com/layer5io/layer5/pull/6210" # One profile, or an explicit subset. blowhorn post --platform github --profile marcus --amplify "https://github.com/layer5io/layer5/issues/42" # Hold one profile out of this run. blowhorn post --platform github --profile all --exclude marcus --amplify "https://github.com/layer5io/layer5/issues/42" # Preview: reads which of the five each profile already left, and adds nothing. blowhorn post --platform github --profile all --amplify "https://github.com/layer5io/layer5/issues/42" --dry-run ``` ## React from the queue Set `Platform` to GitHub and `Amplify` to the issue or PR URL on a row, then run with no `--amplify` at all: ```bash blowhorn post --platform github --profile marcus ``` With the flag, the flag wins over the row. A row whose `Amplify` value is empty or is not an issue or PR URL is skipped with a per-row error, and the run continues. ## Who runs `--profile all` expands to every profile that holds a `GH_TOKEN`; a profile you name that holds none is reported and skipped, and the rest run. Naming profiles explicitly makes `--exclude` inert, as everywhere else. Reacting to your own issue or pull request is allowed: it is ordinary on GitHub, so leave that profile out with `--exclude` when you do not want it. ## Already reacted is not an error GitHub answers `201` when it creates a reaction and `200` when the reaction was already there. A second run reports "already amplified" per reaction: not an error, and not a duplicate. A run that confirmed nothing exits non-zero; a `--dry-run` never fails for confirming nothing. ## A 404 that means the token, not the issue GitHub answers `404` rather than `403` for a token lacking the scope, so "that issue is gone" and "your token cannot react" look identical by status. Blowhorn reads the cause off the response's own scope headers and says which scope is missing and which command stores a reissued token, instead of reporting "Not Found". A token that can follow but cannot react is told it is missing the reacting scope. When the headers show no shortfall, the status is reported as it came back. A shortfall belongs to the token, so the run explains it once for that profile, in full, and stops that profile's reactions, rather than failing once per reaction. Every other profile in the run is still tried. ## Related - [Amplify an existing post](/docs/how-to/publish/amplify-a-post/) - amplifying on the other platforms. - [Follow accounts on GitHub](/docs/how-to/grow/follow-on-github/) - the other GitHub path, and the same 404 diagnosis for follows. - [Sign a profile in to each platform](/docs/how-to/set-up/platform-accounts/) - storing and checking the token. -------------------------------------------------------------------------------- title: "Post to Reddit and Bluesky" url: https://blowhorn.ai/docs/how-to/publish/post-to-reddit-and-bluesky/index.md description: Queue Reddit posts and Bluesky posts, threads, and reposts. -------------------------------------------------------------------------------- # Post to Reddit and Bluesky Queue posts for Reddit and Bluesky from content rows, then publish them with `blowhorn post`. ## Reddit: post to a subreddit Set `Title`, put the text in `Message Text`, and put the subreddit in `Destination` as `r/`: ```bash blowhorn content upsert --payload '{"Platform":"reddit","Profile":"kate","Title":"Our v2.0 release is out","Message Text":"What is new in this release...","Destination":"r/devops","Approved?":"yes"}' blowhorn post --platform reddit --profile kate --dry-run blowhorn post --platform reddit --profile kate ``` A title longer than 300 characters is cut short with a warning. A row with no subreddit in `Destination` or `Amplify` is skipped with an error naming the row. A comment on a thread is a `Comment on` row, and an upvote is an `Amplify` row; both are on their own pages. ## Bluesky: post and thread Put the text in `Message Text`. Past 300 characters the post splits into a thread at word boundaries: ```bash blowhorn content upsert --payload '{"Platform":"bluesky","Profile":"kate","Message Text":"Our v2.0 release is out...","Approved?":"yes"}' blowhorn post --platform bluesky --profile kate --dry-run blowhorn post --platform bluesky --profile kate ``` A thread that stops part-way is recorded with what landed and never retried: re-running it would post the landed part twice. Read the run's `!!!` line before you touch that row again. A row with `Image URL` attaches that file. A comment on a post is a `Comment on` row, on its own page. ## Bluesky: repost Put the post's Bluesky URL in `Amplify` and leave `Message Text` empty: ```bash blowhorn content upsert --payload '{"Platform":"bluesky","Profile":"kate","Amplify":"https://bsky.app/profile/yourproject.bsky.social/post/abc","Approved?":"yes"}' ``` Unlike LinkedIn reposts, a Bluesky repost is not guarded against a second run: once the row is done, leave `Date Promoted` alone. ## Related - [Post content](/docs/how-to/publish/post-content/) - running `blowhorn post` in general. - [Amplify an existing post](/docs/how-to/publish/amplify-a-post/) - Reddit upvotes in full. - [Reply to a post](/docs/how-to/publish/reply-to-a-post/) - comments on both platforms. - [Sign a profile in to each platform](/docs/how-to/set-up/platform-accounts/) - the Reddit session and the Bluesky login each run needs. -------------------------------------------------------------------------------- title: "Post to Hacker News" url: https://blowhorn.ai/docs/how-to/publish/post-to-hacker-news/index.md description: Submit links and text posts, comment, and stay within the daily cap. -------------------------------------------------------------------------------- # Post to Hacker News Submit a link or a text post to Hacker News, or comment on an item, from a content row. {{< major >}}Hacker News votes? Absolutely not. I have standards. Dry-run it first: a submission is forever.{{< /major >}} Hacker News submissions **cannot be deleted**. Preview every new row with `--dry-run` before the first real run. A row that fails costs you one run; a row that posts the wrong thing is permanent. Blowhorn never votes on Hacker News. There is nothing to configure for that and no way to turn it on. ## Before the first run 1. **Store the profile's Hacker News login.** Both keys are needed; the password is read from the store and never from `config.yaml`. ```bash blowhorn profile set your-profile HN_USERNAME= HN_PASSWORD= ``` Check it landed, with the password shown only as set or not set: ```bash blowhorn profile get your-profile ``` 2. **Map the profile to a Chrome profile**, if it is not mapped already. Hacker News is driven through a browser, like LinkedIn, X, and Reddit. See [Map Chrome profiles](/docs/how-to/set-up/map-chrome-profiles/). If a named profile has no Hacker News login stored, `blowhorn post --profile your-profile --platform hn` says so by name and exits non-zero before any browser opens. ## Submit a link Set `Title` and put the URL in `Destination`. Leave `Comment on` blank. ```bash blowhorn content upsert --payload '{"Platform":"HN","Profile":"your-profile","Title":"Example v1.0 is out","Destination":"https://example.com/news/v1-0","Approved?":"yes"}' ``` Keep the title to **80 characters or fewer**. A longer one fails the row rather than being cut short, because a Hacker News title cannot be edited afterwards. ## Submit a text post Set `Title` and put the body in `Message Text`. Leave `Destination` and `Comment on` blank. ```bash blowhorn content upsert --payload '{"Platform":"HN","Profile":"your-profile","Title":"Ask HN: How do you visualize your clusters?","Message Text":"We have been trying a few approaches...","Approved?":"yes"}' ``` A row with both a `Destination` URL and body text submits the link and tells you how much of the body it dropped. Pick one. ## Comment or reply Put the item's URL in `Comment on` and the comment in `Message Text`. To reply to a comment, use that comment's own permalink: the link on its timestamp. ```bash blowhorn content upsert --payload '{"Platform":"HN","Profile":"your-profile","Comment on":"https://news.ycombinator.com/item?id=41234567","Message Text":"We hit the same thing; what fixed it for us was...","Approved?":"yes"}' ``` Replies on a comment permalink have not been proven the way story comments have: if Hacker News shows no comment box there, the row fails with "No comment form" and nothing is posted. Try a reply on one row before queueing many. Once `Comment on` holds anything, the row is a comment and nothing else. If the URL is mistyped or the body is blank, the row fails. It is never submitted as a story instead, even when it also carries a `Title` and `Destination`. ## Preview, then run ```bash # See what would be submitted. Submits nothing and records nothing. blowhorn post --platform hn --profile your-profile --dry-run # Publish. blowhorn post --platform hn --profile your-profile ``` A dry run is not offline. It opens the browser and **signs in to Hacker News** exactly as a real run does, then stops short of the submit. It is a real visit by a real account, so Hacker News can rate-limit it like one. ## If a row is held or fails - **Held (daily cap).** A profile may make 2 link submissions per UTC day. The row is left pending and the run exits 0; run again tomorrow. Text posts are not counted. - **"already submitted to HN by profile …".** Another profile submitted that URL first. Change or remove the row: submitting one URL from several accounts is what gets domains banned. - **"HN did not create a story … already submitted by …".** Somebody outside Blowhorn posted that URL and Hacker News redirected to their story. The URL is recorded as already on Hacker News and not ours: from now on every profile is refused it before the browser opens, and it counts toward nobody's daily cap. The row keeps failing until you change or remove it. Comment on the existing story instead, using the link the message prints. - **"… is already on Hacker News: submitted by …".** That same URL, on a later run or for another profile. Nothing was opened in the browser. To submit it after all, have an operator remove the URL's row from the store's `hn:submissions` ledger, then run again; there is no command for that yet. - **"the new comment could not be found on the page HN landed on".** The row is marked done with the thread's link rather than the comment's own. The comment may well be there, on a later page of a long thread; the row is never failed for this alone, because re-running it would post the comment twice. Check the thread and set the Promotion Link by hand if you need the comment's own link. - **Rate limited.** The row fails and the rest of that profile's Hacker News rows are skipped for this run. Wait before running again; do not retry immediately. - **"could not be confirmed".** The click went through but neither the page nor Hacker News's public checks showed the submission as yours. Check the account's submissions page before running again. ## Related - [Post content](/docs/how-to/publish/post-content/) - running `blowhorn post` in general. - [Reply to a post](/docs/how-to/publish/reply-to-a-post/) - comments on the other platforms. - [Platform reference](/docs/reference/platforms/) - every column, outcome, and limit. - [Reliability and anti-bot design](/docs/explanation/reliability-and-anti-bot-design/) - why these rules are strict. - [You're in control](/docs/explanation/youre-in-control/) - the limits on every platform. -------------------------------------------------------------------------------- title: "Reply to a post" url: https://blowhorn.ai/docs/how-to/publish/reply-to-a-post/index.md description: Comment on LinkedIn, Reddit, and Bluesky rows, and reply on X. -------------------------------------------------------------------------------- # Reply to a post Comment on a post from a content row. Put the post's URL in `Comment on` and the reply in `Message Text`, approve the row, and run `blowhorn comment`: ```bash blowhorn content upsert --payload '{"Platform":"linkedin","Profile":"kate","Comment on":"https://www.linkedin.com/feed/update/urn:li:activity:123/","Message Text":"Great post!","Approved?":"yes"}' blowhorn comment --platform linkedin --profile kate --dry-run blowhorn comment --platform linkedin --profile kate ``` `blowhorn comment` covers LinkedIn, Reddit, and Bluesky. It only ever comments: a row whose `Comment on` is not a post on the row's own platform, or whose `Message Text` is empty, is skipped and stays pending. ## Reply on X X replies ride with `blowhorn post`, not `blowhorn comment`. Set the tweet's URL in `Comment on` and the reply in `Message Text`, then run `post` for that profile and platform: ```bash blowhorn post --platform x --profile kate ``` ## Reply without a row `--target` with `--message` comments once, with no row read or marked: ```bash blowhorn comment --platform linkedin --profile kate --target "https://www.linkedin.com/feed/update/urn:li:activity:123/" --message "Great post!" ``` Name one profile: ad-hoc mode refuses `--profile all`. For a Slack thread reply, use `blowhorn post --target` with a Slack message link instead ([Send a Slack message](/docs/how-to/publish/send-a-slack-message/)). ## Run one or the other over the same rows `blowhorn post` also acts on `Comment on` rows, so a reply row left pending is picked up by whichever runs first. Queue reply rows and run `comment`; or let the `post` run take them with everything else. Do not run both over the same rows expecting each to take half. `post` is not as strict as `comment`. It comments only when `Comment on` is a post on the row's own platform and `Message Text` is set; otherwise it publishes the row as an ordinary post. A mistyped `Comment on` URL, or on LinkedIn any `Destination` at all, sends the reply text out as a public post. Run reply rows through `comment` when you cannot vouch for every URL. Hacker News comments are rows too, but they publish through `post`, on [Post to Hacker News](/docs/how-to/publish/post-to-hacker-news/). There, once `Comment on` holds anything, the row is a comment and never a submission. ## Related - [Amplify an existing post](/docs/how-to/publish/amplify-a-post/) - reposts and reactions, the other use of a URL on a row. - [Post content](/docs/how-to/publish/post-content/) - running `blowhorn post` in general. - [Post to Hacker News](/docs/how-to/publish/post-to-hacker-news/) - Hacker News comments. - [Send a Slack message](/docs/how-to/publish/send-a-slack-message/) - Slack thread replies. -------------------------------------------------------------------------------- title: "Send a Slack message" url: https://blowhorn.ai/docs/how-to/publish/send-a-slack-message/index.md description: Queue Slack rows or send one message typed on the command line. -------------------------------------------------------------------------------- # Send a Slack message Send a Slack message as yourself, from a queued row or typed on the command line. Both send under your captured session, with no app attached. ## Before the first run Capture your signed-in Slack session into the profile ([Sign a profile in to each platform](/docs/how-to/set-up/platform-accounts/)): ```bash blowhorn profile auth --platform slack --profile kate ``` ## Send one message, no row Name one profile, the destination, and the text, and preview it first: ```bash blowhorn post --platform slack --profile kate --target "#announcements" --message "Maintenance is complete" --dry-run blowhorn post --platform slack --profile kate --target "#announcements" --message "Maintenance is complete" ``` `--target` takes `#channel`, `@user`, a Slack channel link, or a Slack message link (a thread reply). For a one-off message anything else is refused, never sent somewhere you did not name. Give a link when the profile's credentials are per workspace, so Blowhorn knows which workspace to send with. `--target` without `--message` is not a one-off message: it redirects the queued Slack rows for that run. Nothing is written back, so running the command twice sends the message twice. ## Send the queued rows Queue rows with `Platform` set to Slack. Put where each message goes in `Amplify` (`#channel`, `@user`, or a Slack channel or message link), or a Slack message link in `Comment on` for a thread reply. `Destination` only picks the workspace whose credentials send. Then run: ```bash blowhorn post --platform slack --profile kate ``` A row with no target in `Amplify` or `Comment on` goes to `#general`, and so does one whose target Blowhorn cannot read, such as `announcements` without the `#`. Preview with `--dry-run` first. `--target` on that run sends the queued rows to that destination instead of their own. Unlike a one-off message, it is not checked: a value it cannot read sends every queued row to `#general`. ## Pick which credential sends `--slack-auth` names which Slack credential may send for one run: `session` (your captured signed-in session), `user-token`, or `bot-token`, or a comma-delimited list of them. ## Related - [Sign a profile in to each platform](/docs/how-to/set-up/platform-accounts/) - capturing the session each message sends with. - [Post content](/docs/how-to/publish/post-content/) - running `blowhorn post` in general. - [Manage the content queue](/docs/how-to/publish/manage-the-content-worksheet/) - queueing the rows a run reads. -------------------------------------------------------------------------------- title: "Follow accounts" url: https://blowhorn.ai/docs/how-to/grow/follow-on-github/index.md description: Follow accounts on X, Bluesky, and GitHub, and unfollow on Bluesky. -------------------------------------------------------------------------------- # Follow accounts Follow accounts on X, Bluesky, or GitHub with `blowhorn follow`. One contract on all three: `--limit 0` follows only the named target, and a positive `--limit` follows up to that many accounts drawn from the target's follower graph, never the target itself. ```bash # Preview: names exactly who would be followed, and follows nobody. blowhorn follow --platform x --profile marcus --target somehandle --limit 10 --dry-run # Follow up to 10 accounts from that account's followers. blowhorn follow --platform x --profile marcus --target somehandle --limit 10 # Target-only mode: follow just the named account. blowhorn follow --platform github --profile marcus --target layer5io --limit 0 # Several accounts at once, only in target-only mode. blowhorn follow --platform github --profile marcus --target layer5io,octocat --limit 0 # Every eligible profile follows from the same target. blowhorn follow --platform bluesky --profile all --target somehandle.bsky.social --limit 10 --exclude marcus ``` Already-followed accounts are skipped, not re-followed, and counted apart: the summary's `Followed` column counts only accounts this run newly followed. In follower traversal they do not consume `--limit` either: the limit bounds follows, not candidates examined. A live X run is the exception: it reads nothing after clicking Follow, so it counts none of its follows as followed and reports them as `clicked, outcome not read (N)`. Those handles are still logged, so later runs skip them. Under `--profile all`, a profile whose own handle matches a target is left out of the run. Naming profiles explicitly makes `--exclude`, and that self-exclusion, inert: a named profile runs even against its own handle. Each platform authenticates the way it always does: X through the profile's browser session, Bluesky through its handle and app password, GitHub through its `GH_TOKEN` ([Sign a profile in to each platform](/docs/how-to/set-up/platform-accounts/)). ## Previewing a GitHub follow run `--dry-run` on GitHub is not a generic stop-before-the-browser gate: there is no browser on this path. It performs the real read-side walk, paging the target's followers and asking GitHub who is already followed, then reports exactly who a real run would follow, issuing not a single `PUT`: ```bash blowhorn follow --platform github --profile marcus --target layer5io --limit 25 --dry-run ``` ## Unfollow on Bluesky ```bash blowhorn unfollow --platform bluesky --profile marcus --target somehandle.bsky.social ``` Unfollow takes one Bluesky handle from each profile. X unfollow does not run: it is refused before a browser opens, so no X account is unfollowed or logged as unfollowed. Unfollow cannot be scheduled. ## When a GitHub follow fails GitHub answers `404` rather than `403` for a token lacking the scope, so "that account is gone" and "your token cannot follow anyone" look identical by status. Blowhorn reads the cause off the response's own scope headers and names the missing scope (`user:follow` for a classic token; read and write on Followers for a fine-grained one) and the command that stores a reissued token, instead of reporting "Not Found". When the headers show no shortfall, the status is reported as it came back. A shortfall belongs to the token, so the run explains it once, in full, and stops, rather than failing once per account. ## Related - [Find conversations on Bluesky](/docs/how-to/grow/find-on-bluesky/) - search Bluesky for the accounts worth following. - [Build a GitHub audience](/docs/how-to/grow/build-a-github-audience/) - list a repository's people into the store first. - [Amplify an issue or pull request on GitHub](/docs/how-to/publish/amplify-on-github/) - the other GitHub path. - [Sign a profile in to each platform](/docs/how-to/set-up/platform-accounts/) - the session, login, or token each platform follows with. -------------------------------------------------------------------------------- title: "Find conversations on Bluesky" url: https://blowhorn.ai/docs/how-to/grow/find-on-bluesky/index.md description: Search Bluesky posts by query, date, author, and other filters. -------------------------------------------------------------------------------- # Find conversations on Bluesky Search Bluesky for posts matching a query, with date, author, and relevancy filters. Read-only: results are printed, never posted, and no browser opens. ```bash # Name the query. It is required. blowhorn find --query "developer relations" --profile kate # Newest or most relevant first. blowhorn find --query "developer relations" --sort latest --profile kate blowhorn find --query "developer relations" --sort top --profile kate # Narrow by date, author, mention, language, linked domain, URL, or tag. blowhorn find --query "developer relations" --since 2026-09-01 --until 2026-10-01 --profile kate blowhorn find --query "developer relations" --author yourproject.bsky.social --profile kate blowhorn find --query "developer relations" --tag opensource --limit 10 --profile kate ``` The query takes Lucene syntax. `--limit` takes 1 to 100 and defaults to 25. The run authenticates as the profile, so the profile needs its Bluesky login stored first. ## Related - [Follow accounts](/docs/how-to/grow/follow-on-github/) - follow the accounts worth following. - [Post to Reddit and Bluesky](/docs/how-to/publish/post-to-reddit-and-bluesky/) - join the conversations worth joining. - [Sign a profile in to each platform](/docs/how-to/set-up/platform-accounts/) - the Bluesky login the search runs with. -------------------------------------------------------------------------------- title: "Build a GitHub audience" url: https://blowhorn.ai/docs/how-to/grow/build-a-github-audience/index.md description: List a repository's stargazers, contributors, or watchers into the store. -------------------------------------------------------------------------------- # Build a GitHub audience List the people around one or more repositories into the store with `blowhorn source --platform github`: stargazers unless you name another audience, one stored row per login. It does not follow, connect, or send mail. The run lists with the profile's stored `GH_TOKEN`. ```bash # Stargazers of one repository. Preview first: it lists and writes nothing. blowhorn source --platform github --profile marcus --repo layer5io/layer5 --dry-run blowhorn source --platform github --profile marcus --repo layer5io/layer5 # Another audience, or several repositories at once. blowhorn source --platform github --profile marcus --repo layer5io/layer5 --audience contributors blowhorn source --platform github --profile marcus --repo layer5io/layer5 --repo layer5io/blowhorn-site --audience watchers ``` `--audience` is one of `owner`, `contributors`, `forks`, `stargazers`, `watchers`, `subscribers`, or `issues`. `--audience all` lists their union, and only when you type it. `--repo` repeats; at least one is required. `--limit` caps the people examined in the run. ## Related - [Follow accounts](/docs/how-to/grow/follow-on-github/) - follow the audience you built. - [Amplify an issue or pull request on GitHub](/docs/how-to/publish/amplify-on-github/) - react in the repositories you listed. - [Sign a profile in to each platform](/docs/how-to/set-up/platform-accounts/) - the token the listing runs with. -------------------------------------------------------------------------------- title: "Accept incoming invitations" url: https://blowhorn.ai/docs/how-to/grow/accept-invitations/index.md description: Answer received LinkedIn invitations; flagged ones are skipped. -------------------------------------------------------------------------------- # Accept incoming invitations Answer the LinkedIn invitations other people sent you with `blowhorn accept`. It walks your received list accepting one invitation at a time, with a pause between each. ```bash blowhorn accept --profile marcus --dry-run # preview: accepts nobody blowhorn accept --profile marcus # accept up to 20 blowhorn accept --profile marcus --limit 5 # cap a single run blowhorn accept --profile all --headless # every eligible profile, unattended ``` ## What `--limit` counts Confirmed acceptances: only an invitation confirmed gone from the received list counts. A click is not an acceptance, and an invitation skipped because LinkedIn warned about it never consumes the budget. `--limit 0` means accept nobody. ## Flagged invitations are skipped, never accepted An invitation LinkedIn flags with its "Take care when connecting" warning is left alone, never accepted, and the run carries on. Skips are reported by name, apart from acceptances. Ignoring or declining an invitation is out of scope: this command only accepts. ## What a preview can and cannot tell you `--dry-run` walks the same received list a real run would and stops where clicking would start. What it cannot tell you is which people a real run would skip: LinkedIn raises its warning only once Accept is clicked. The preview says so rather than implying it lists only acceptable invitations. It is bounded like the run it previews: it lists at most `--limit` people, and says so with numbers when it stopped short. Three invitations in a row that could not be accepted halt the run with the halt reported: repeated non-confirmation means the page changed underneath the run, and continuing would click blindly down a list. ## Related - [Withdraw stale invitations](/docs/how-to/grow/withdraw-invitations/) - the sent list, the opposite direction. - [Why flagged invitations are skipped](/docs/explanation/invitation-safety/) - the rule behind the skip. - [Sign a profile in to each platform](/docs/how-to/set-up/platform-accounts/) - the LinkedIn session the walk runs with. -------------------------------------------------------------------------------- title: "Withdraw stale invitations" url: https://blowhorn.ai/docs/how-to/grow/withdraw-invitations/index.md description: Preview and withdraw old sent invitations to reclaim headroom. -------------------------------------------------------------------------------- # Withdraw stale invitations Reclaim invitation headroom with `blowhorn withdraw`. Sent invitations never expire on their own: they pile up against LinkedIn's ceiling on unaccepted invitations, and once that ceiling is near, new connection requests start failing. An invitation unaccepted for three weeks is not going to be accepted, so withdrawing it costs nothing and returns headroom. ```bash blowhorn withdraw --profile marcus # preview, withdraws nothing blowhorn withdraw --profile marcus --apply # withdraw up to 20 blowhorn withdraw --profile marcus --older-than 60 --limit 40 --apply ``` | Flag | Default | Meaning | |---|---|---| | `--older-than` | `21` | Minimum pending age, in days, for an invitation to be eligible | | `--limit` | `20` | Maximum withdrawals **confirmed** in one run | | `--apply` | off | Actually withdraw. Without it the run is a preview | **Preview is the default.** Withdrawing is hard to undo: LinkedIn blocks re-inviting a withdrawn person for up to three weeks. A preview withdraws nothing. It lists each eligible invitation as name, profile URL, and parsed age, and prints that three-week consequence. `--dry-run` forces preview, so `--apply` with `--dry-run` still withdraws nothing. `--limit 0` means zero withdrawals. ## People only The sent list has People and Pages tabs, and the run works the People tab. It proves that tab is selected before it acts, and does nothing at all when it cannot prove it. ## How age is read LinkedIn renders only relative ages ("19 hours ago", "3 weeks ago", "1 month ago"), so ages are parsed from that text. Coarse units count their lower bound: a month counts 28 days, not 30, so an invitation is never treated as older than it provably is. An age worded in a way Blowhorn does not recognize is **kept**, counted, and reported as Unreadable: a wording change on LinkedIn's side shows up as a number, never as silent over-withdrawal. ## What counts `--limit` counts confirmed withdrawals. A click that leaves the card still listed is not a withdrawal: it is reported as Unconfirmed, is not counted, and is left for a later run. | Outcome | Meaning | Counts against `--limit`? | |---|---|---| | `WITHDRAWN` | read as gone from the list | **yes** | | `TOO_NEW` | parsed age below the threshold | no | | `UNREADABLE_AGE` | age text not recognized | no | | `UNCONFIRMED` | acted on, still listed afterwards | no | | `FAILED` | control missing or undeliverable | no | Three outcomes in a row that are not a confirmed withdrawal halt the run, and a halted run is reported as halted, never as success. ## Related - [Accept incoming invitations](/docs/how-to/grow/accept-invitations/) - the received list, the opposite direction. - [Sign a profile in to each platform](/docs/how-to/set-up/platform-accounts/) - the LinkedIn session the walk runs with. -------------------------------------------------------------------------------- title: "Schedule a job" url: https://blowhorn.ai/docs/how-to/schedule/schedule-a-job/index.md description: Run posts and other commands on a schedule, from the Jobs screen or the command line. -------------------------------------------------------------------------------- # Schedule a job Put a command on a schedule so it runs on its own, with or without the app open. ## From the app Open the "jobs" screen and choose "new job". Pick the command, the profiles and platforms it runs for, and how often it repeats. The "browser driver" control pins which Chrome route that job uses; leave it at the default unless one job needs its own route. If the schedule is empty, the screen offers "bootstrap schedule" instead: it previews a starter set of rows first, and writes them only when you apply the preview. It never edits a schedule that already holds rows. Open a row to change it, or run it now without waiting for its time. A dry run previews the pass and claims nothing. ## From the command line List what is scheduled and whether each row is due: ```bash blowhorn schedule list blowhorn schedule list --due-only ``` Check every row against the supported schema before it costs you a pass: ```bash blowhorn schedule validate ``` Seed an empty schedule with the starter set. It refuses a schedule that already holds rows unless you pass `--replace`, which deletes them all first: ```bash blowhorn schedule bootstrap --dry-run blowhorn schedule bootstrap ``` Run one pass by hand, or force one row now: ```bash blowhorn schedule tick --profile all blowhorn schedule trigger-now 4 ``` Read or write a single row. Row numbers are stable: they are assigned when a row is created and never reused, so a number you noted keeps meaning the same row: ```bash blowhorn schedule get 4 blowhorn schedule upsert --payload '{"Command": "post", "Profile": "all", "Recurrence": "daily"}' blowhorn schedule delete 4 ``` `upsert` and `delete` accept the fingerprint `schedule get` returns, and refuse a stale edit rather than writing over someone else's change. ## What a job can run A job runs one Blowhorn command with its own profiles, platforms and parameters. `post`, `comment`, `follow`, `accept`, `withdraw`, `analytics` and `report` are schedulable. `find` is not: it prints search results and changes nothing. `Recurrence` takes `once`, `hourly`, `daily`, `weekly`, `monthly`, or `every:` for a minute count. A job whose profile is `all` runs for every profile except the excluded ones; name a profile in the row to include an excluded one for that job only. Each pass writes a per-job log under `/scheduler/`, records its heartbeat so `blowhorn schedule status` can report it, and appends one line per pass to the run ledger. When the pass cannot reach the store it claims nothing, exits 3, and says so in one line. ## Related - [Pause and resume posting](/docs/how-to/schedule/pause/) - stop every claim without stopping the service - [Find out why a job did not run](/docs/how-to/schedule/why-not-run/) - read one row's state in one command - [Keep posting when the app is closed](/docs/how-to/schedule/background-service/) - the background service that runs the passes - [Change your defaults](/docs/how-to/settings/defaults/) - pace, excluded profiles, logs - [You're in control](/docs/explanation/youre-in-control/) - the limits every scheduled job runs under -------------------------------------------------------------------------------- title: "Pause and resume posting" url: https://blowhorn.ai/docs/how-to/schedule/pause/index.md description: Stop every scheduled claim on one machine, every machine, or one row, then resume. -------------------------------------------------------------------------------- # Pause and resume posting Stop Blowhorn from claiming scheduled work without stopping the service. Reach for this when a platform challenges a sign-in, when a session expires, or on any day you want nothing to go out. {{< major >}}Whole unit standing down. Your call. Unpause when you're ready.{{< /major >}} ## Pause this machine ```bash blowhorn schedule pause --reason "LinkedIn challenge on kate" blowhorn schedule status blowhorn schedule resume ``` The flagless pause writes a local file beside your config, so it works even when everything else is broken, and it survives a crash, a restart and a reboot. Ticks keep running every ten minutes but claim nothing and exit 0. Due times carry forward: a recurring row that fell due during the pause runs once on resume, not once per missed interval. A job already running finishes first. Killing a browser flow mid-post can leave a half-done action on the platform, so the pause stops the next claim, never the running job. In the app, choose "pause this machine" in the popover or on the console. While paused, the app also skips its session probe, so a paused Blowhorn opens no browser at all. ## Pause every machine, or one row ```bash blowhorn schedule pause --all --reason "X is rate limiting us" blowhorn schedule pause --row 7 --reason "waiting on the page owner" blowhorn schedule resume --all blowhorn schedule resume --row 7 ``` A row is claimed only when none of the three scopes says paused: this machine, all machines, or that row. `--all` and `--row` live in the store, so every machine sharing it honours them on its next claim; the flagless pause never touches the store. In the app the same three scopes are "pause this machine", "pause all machines" and "pause this row", each with a reason. Lifting one scope leaves the other two as they were. A held row still reads `due: yes` beside `paused: yes` in `blowhorn schedule list`: due keeps meaning its time has passed. A pause is not `Enabled: no`: disabling a row is editorial, with no reason and no intent to resume. `trigger-now` on a held row exits 1 and names the scope. `--ignore-pause` on `tick` or `trigger-now` overrides every scope, and a dry run previews through the pause: held rows read `held` rather than `would-run`. ## Related - [Schedule a job](/docs/how-to/schedule/schedule-a-job/) - create rows and run passes by hand - [Find out why a job did not run](/docs/how-to/schedule/why-not-run/) - see which scope holds a row - [Keep posting when the app is closed](/docs/how-to/schedule/background-service/) - the service that keeps ticking while paused - [Change your defaults](/docs/how-to/settings/defaults/) - pace, excluded profiles, logs - [You're in control](/docs/explanation/youre-in-control/) - every other safety control -------------------------------------------------------------------------------- title: "Find out why a job did not run" url: https://blowhorn.ai/docs/how-to/schedule/why-not-run/index.md description: Ask Blowhorn why one scheduled row ran or not, in one read-only command. -------------------------------------------------------------------------------- # Find out why a job did not run One command names the reason a row has or has not run on this machine, in the order the scheduler itself decides it, and ends in one plain sentence: ```bash blowhorn schedule why 7 ``` ```text row 7 · post · your-profile · linkedin enabled yes valid yes paused no - row not held, studio not paused, all-machines not paused due yes - next_run_at 2026-08-31 06:00:00, 14 min ago claimed yes - laptop:9912 since 2026-08-31 06:02:10, lease expires 2026-08-31 06:32:10 this machine studio · store reachable · last tick 2026-08-31 06:14:02 (2 min ago) last run 2026-08-30 06:03:11 on laptop · ok · 12 attempted, 12 confirmed studio did not run row 7 because laptop:9912 claimed it at 2026-08-31 06:02:10 and still holds the lease (expires 2026-08-31 06:32:10). ``` Read it top to bottom: enabled, valid, paused (naming the scope and how to lift it), due, claimed, this machine, last run. The closing sentence is the answer; quote it in a bug report with `--json` for the same answer machines can read. ## What the common answers mean - **Paused.** The scope is named: the row, this machine, or all machines. Lift it with `blowhorn schedule resume`, with `--all` or `--row 7` to match. - **Claimed by another machine.** A claim is a lease, not a lock: the holder renews it every ten minutes while the job runs, and a dead machine stops renewing, so its rows return to the pool when the lease lapses. `schedule list` names every holder in its Locked By column; your own claims read `(this machine)`. - **Not due.** `Next Run At` has not passed yet. `schedule status` shows an estimated next tick for this machine. - **Invalid.** `blowhorn schedule validate` names what the row breaks. The command is read-only: it claims nothing, runs nothing and changes nothing, so it is safe against a row that is mid-flight. ## Related - [Schedule a job](/docs/how-to/schedule/schedule-a-job/) - create rows and run passes by hand - [Pause and resume posting](/docs/how-to/schedule/pause/) - lift the scope that holds the row - [Review what ran](/docs/how-to/measure/review-runs/) - the ledger behind "last run" - [Troubleshooting](/docs/how-to/troubleshoot/troubleshooting/) - start from a symptom instead -------------------------------------------------------------------------------- title: "Keep posting when the app is closed" url: https://blowhorn.ai/docs/how-to/schedule/background-service/index.md description: Run scheduled passes from the background service, and quit cleanly. -------------------------------------------------------------------------------- # Keep posting when the app is closed Scheduled passes keep running with no window open. One background service owns the timer and runs a pass every ten minutes, starting with one as soon as it starts. ## Check the service ```bash blowhorn service status blowhorn service tick-now --follow ``` `service status` names the owner, whether the service is running, the last pass and the pass in flight. `tick-now` asks the running service for a pass now and watches it. In the app, the "service" screen shows the same state. The owner tells you who manages it: the desktop app registered it itself, or the command line installed it from a file. When the app owns it, the app's setting "keep the background service running after quit" (on by default) decides whether quitting stops it. Quitting with a pass in flight asks what to do: finish, cancel at a safe point, or stay. ## Install it without the app On a machine without the desktop app: ```bash blowhorn service install ``` `service test` validates the schedule and previews a pass the way the service would run it. `service stop` drains the pass in flight before it stops; the definition stays, and `service start` loads it again. `service update` brings a file-installed definition up to date with the checkout, and only reloads when it differs. Every verb takes `--dry-run` to print what it would do, and `--json` for the shape the app reads. ## Cancel a pass in flight ```bash blowhorn service cancel ``` Cancelling stops at the next safe point, never mid-post: the row the pass was on returns to the queue exactly as claimed, with no retry counted, and the next pass takes it up again. The run ledger records the pass as cancelled, never failed. ## Related - [Schedule a job](/docs/how-to/schedule/schedule-a-job/) - create the rows the passes run - [Pause and resume posting](/docs/how-to/schedule/pause/) - hold claims without stopping the service - [Find out why a job did not run](/docs/how-to/schedule/why-not-run/) - read one row's state - [Uninstall Blowhorn](/docs/how-to/manage/uninstall/) - remove the service for good - [You're in control](/docs/explanation/youre-in-control/) - what an unattended run will and won't do -------------------------------------------------------------------------------- title: "Collect and read your analytics" url: https://blowhorn.ai/docs/how-to/measure/analytics/index.md description: Collect LinkedIn and X follower numbers, then read the history and the HTML report. -------------------------------------------------------------------------------- # Collect and read your analytics Collect follower numbers from LinkedIn and X, then read them back as dated series in the app or the command line. Blowhorn collects LinkedIn pages and X profiles. A Bluesky scope is accepted by the command but collects nothing today, so scope your runs to `linkedin` and `x`. ## Collect ```bash blowhorn analytics --platform linkedin --profile ada --lookback 30 blowhorn analytics --platform x --profile all ``` LinkedIn exports honour `--lookback`; X appends a follower and following trend row per profile. Each run prints a receipt of the rows it collected. In the app, the "analytics" screen collects on demand with "collect now" and draws the same series. ## Read the history ```bash blowhorn analytics history --since 30d blowhorn analytics history --subject ada --json ``` `history` renders every LinkedIn page and every X profile as one dated series from the store, whichever machine collected it. `--since` takes days, weeks, months, years or `all`, and filters which points print, never what the 30-day figures compute from. Treat the 30-day net, growth rate and unfollowed figures as derived, not measured: the output labels them so. LinkedIn measures new followers, so only LinkedIn pages carry an unfollowed figure. X measures nothing but the total, so an X profile has a net change and no unfollowed figure at all. ## Read the HTML report ```bash blowhorn report blowhorn report --no-open ``` `report` builds `analytics/analytics_report.html` from the exports already on disk and opens it; `--no-open` writes the file and leaves it closed. Collect first when you want fresh numbers: the report reads local files only. ## Related - [Review what ran](/docs/how-to/measure/review-runs/) - per-machine operations from the same ledger - [Schedule a job](/docs/how-to/schedule/schedule-a-job/) - collect on a schedule - [Change your defaults](/docs/how-to/settings/defaults/) - pace, excluded profiles, logs - [What the analytics numbers mean](/docs/explanation/analytics/) - measured versus trended -------------------------------------------------------------------------------- title: "Review what ran" url: https://blowhorn.ai/docs/how-to/measure/review-runs/index.md description: Read the runs ledger: what each machine did, what failed, and what was a preview. -------------------------------------------------------------------------------- # Review what ran Every action run appends one line per profile and platform to the run ledger, dry or real. Read it back in the app's "runs" screen, or grouped any way you like: ```bash blowhorn analytics operations --group-by host blowhorn analytics operations --group-by profile --since 7d blowhorn analytics operations --kind tick ``` `--group-by` takes `profile`, `platform`, `command`, `day` or `host`: the ledger has always recorded which machine a run happened on, so grouping by host shows who did what across the machines sharing your organization. `--kind tick` aggregates the scheduler's own passes instead of the action runs. A dry run counts apart and never as an operation: a preview is not work done. Records exist from the first run after this command shipped, so an older window reports zero rather than history. To re-run a failure, run its command again by hand or trigger its row; a dry run first shows what the retry would do. ## Related - [Collect and read your analytics](/docs/how-to/measure/analytics/) - follower numbers, not operations - [Find out why a job did not run](/docs/how-to/schedule/why-not-run/) - one row's state in one command - [Schedule a job](/docs/how-to/schedule/schedule-a-job/) - trigger a row now -------------------------------------------------------------------------------- title: "Change your defaults" url: https://blowhorn.ai/docs/how-to/settings/defaults/index.md description: Set pace, excluded profiles, dry-run default, notifications and logs, in the app or the config file. -------------------------------------------------------------------------------- # Change your defaults Set the defaults every run starts from: pace, excluded profiles, dry-run, notifications and logs. Change them in the app's Settings, or with `blowhorn config set` for the whole checkout. ## See what is in effect ```bash blowhorn config show blowhorn config show --json ``` ## Change a default ```bash blowhorn config set defaults.pace slow blowhorn config set defaults.exclude ada,kate blowhorn config set defaults.dry_run true ``` A flag beats an environment variable, which beats `.blowhorn.yaml` in the directory you run from, which beats the built-in. Every option and its environment variable is listed in the configuration reference. To read one option and learn which document holds it: ```bash blowhorn config get defaults.profile blowhorn config get store.password # set / not set, never the value ``` The store connection is configured the same way, but its `store.*` keys live in a private file, never in `.blowhorn.yaml`. During early access your Blowhorn administrator sets them up. ## Choose where output is written ```bash blowhorn post --profile ada --log-dir ./logs # this run only export BLOWHORN_LOG_DIR=./logs # this shell blowhorn config set defaults.logs.directory ./logs ``` ## Choose how long the daily logs are kept ```bash blowhorn config set defaults.logs.retention_days 7 # keep a week blowhorn config set defaults.logs.retention_days 0 # keep every daily log export BLOWHORN_LOG_RETENTION_DAYS=7 # this shell ``` Every run deletes daily logs older than that (30 days if you set nothing), judged by the date in the file name. Nothing else in the directory is pruned. ## Related - [Choose how Blowhorn reaches Chrome](/docs/how-to/settings/launch-mode/) - the launch and driver settings these defaults sit above - [Schedule a job](/docs/how-to/schedule/schedule-a-job/) - per-job parameters that beat these defaults - [Review what ran](/docs/how-to/measure/review-runs/) - read the logs these settings keep - [You're in control](/docs/explanation/youre-in-control/) - how pace and dry run fit with the built-in limits -------------------------------------------------------------------------------- title: "Choose how Blowhorn reaches Chrome" url: https://blowhorn.ai/docs/how-to/settings/launch-mode/index.md description: Stay on the extension default, or attach to your Chrome or launch Blowhorn's own. Advanced. -------------------------------------------------------------------------------- # Choose how Blowhorn reaches Chrome Most runs should keep the default: Blowhorn drives Chrome through the Blowhorn Chrome extension and launches nothing of its own. Change it only when one run or one job needs a real Chrome window. Advanced page: the default is the right choice until you have a reason. ## Before you start Every profile the run acts for must be mapped to a Chrome profile first. A run refuses an unmapped profile by name in every mode. `blowhorn chrome status` reports which mode a run would use right now, and every run names the driver it used once. ## The default: the extension With nothing set, a run drives your mapped Chrome profiles through the extension. It opens one mapped profile per process, a scheduled job included, and refuses by name what the extension cannot do yet. During early access your Blowhorn administrator helps you install it. One case needs another mode today: the extension's typing does not reach X's editor or sign-in form, so X posts and X sign-ins stop before anything is typed. Run those with `--chrome-launch persistent`. ## Launch Blowhorn's own Chrome ```bash blowhorn post --profile ada --chrome-launch persistent ``` For one shell instead of one run: `BLOWHORN_CHROME_LAUNCH=persistent`. To move the whole process back to launching: `BLOWHORN_EXTENSION_ROLLBACK=1`. The run launches Blowhorn's own Chrome from its automation directory, with your mapped Chrome directory as the profile inside it. No allow dialog ever, and it runs beside your open daily Chrome without contending for its lock. Each automation profile starts empty, so sign it in once with `--headed` before a real run. ## Attach to the Chrome you have open ```bash blowhorn post --profile ada --chrome-launch cdp ``` Mac only. The run connects to your open Chrome, Chrome asks you to allow the connection once per run, and Blowhorn's tab opens in the mapped profile. The dialog has no "remember me", so every run asks again: this is an interactive mode, for runs you watch. Never pin an overnight job to it: with nobody there to click allow, the job waits out the short unattended bound and fails by name, every time. A dialog outlives the run that raised it. Clicking allow in a leftover dialog approves nothing: click cancel there and run again. ## An unattended attach: no Allow dialog For the authenticity of a real Chrome profile with no dialog, run a second Chrome on a tree of its own. Chrome 136 and later ignore the classic port switch on the default data directory, which is why this is a separate Chrome and not your daily one. Mac only: ```bash blowhorn chrome attach-chrome start ``` That starts Chrome on `~/.blowhorn/attach-chrome`, prints the steps it leaves to you, and steps back: Blowhorn never drives that Chrome itself. Do them in order: ```bash blowhorn config set defaults.chrome.data_dir ~/.blowhorn/attach-chrome blowhorn chrome profiles blowhorn chrome map ada 'Default' blowhorn post --profile ada --chrome-launch cdp ``` On its own, `blowhorn chrome attach-chrome` reports that tree without starting anything, and `start --dry-run` prints the command line without running it. The profiles there start empty and are signed in once, like automation profiles. Starting is refused when the tree is Chrome's own default data directory, or when a port is already recorded there: a crashed Chrome leaves its `DevToolsActivePort` behind too, so the refusal names the file to delete. ## One scheduled job, its own driver A job names its driver in its `Driver Mode`: `default`, `extension`, `cdp` or `persistent`. Set "browser driver" in the job editor, or the key on `blowhorn schedule upsert`. The choice belongs to that job alone and applies to nothing else in the pass. Two explicit overrides still win over the job: a launch mode forced on the whole tick (`--chrome-launch` on the tick, or `BLOWHORN_CHROME_LAUNCH` in its environment), and `BLOWHORN_EXTENSION_ROLLBACK=1` over a job set to `extension`. The job's log names the driver it used and says when an override beat it. ## Where the setting is read Top wins over everything below it, with one exception: the rollback switch also beats a job set to `extension`, and it replaces whatever the file says. | layer | spelling | | --- | --- | | one run | `--chrome-launch auto\|cdp\|persistent\|extension`, on every command | | your shell | `BLOWHORN_CHROME_LAUNCH=cdp` | | one scheduled job | its `Driver Mode` (`default`, `extension`, `cdp`, `persistent`) | | debugging | `BLOWHORN_BROWSER_DRIVER=cdp` (no file key, no flag) | | whole process fallback | `BLOWHORN_EXTENSION_ROLLBACK=1` selects launching unless a layer above decides - a job's `cdp` or `persistent`, or the variable's `auto`, `cdp` or `persistent`; a job's `extension`, the variable's `extension` and the file's value all give way to it | | this checkout | `defaults.chrome.launch_mode` in `.blowhorn.yaml` | | nothing set | the extension default | A value outside the four is refused before anything runs. A forced `cdp` against a Chrome that is not ready is refused with the next step `blowhorn chrome status` prints; only `auto` falls back to launching, and says so once. A scheduler tick adds no layer of its own: unless the flag is on the tick's argv, each job resolves its own driver. ## When something refuses | sentence | what to do | | --- | --- | | `profile ada has no chrome profile mapped on ; …` | `blowhorn chrome map ada ''` | | `chrome launch mode is cdp but chrome is not ready: ` | do the next step, or drop the forced `cdp` | | `chrome's 'Allow remote debugging?' dialog was not answered within 120 s; …` (20 s for a scheduled job) | click cancel in the dialog still open in Chrome, run again and click allow in the new one, or run with `--chrome-launch persistent` | | `profile ada on linkedin is in use by blowhorn process ; …` | another run is acting as that profile: wait for it, or stop it | | `… is google chrome's own data directory; …` | the dedicated tree must never be Chrome's own default directory | ## Related - [Change your defaults](/docs/how-to/settings/defaults/) - the file layer these settings sit in - [Schedule a job](/docs/how-to/schedule/schedule-a-job/) - give one job its own driver - [Fix the Chrome extension connection](/docs/how-to/troubleshoot/extension-connection/) - when the default route breaks - [Post to X when it will not confirm](/docs/how-to/troubleshoot/x-posting/) - the one case that needs another mode -------------------------------------------------------------------------------- title: "Troubleshooting" url: https://blowhorn.ai/docs/how-to/troubleshoot/troubleshooting/index.md description: Start from the symptom: find the fix for what Blowhorn just told you. -------------------------------------------------------------------------------- # Troubleshooting Start from what you saw. Each symptom names its fix page. ## A post did not go out, or might have - **X post NOT sent under the extension driver.** The extension's typing does not reach X's editor, so the run stops before anything is typed and tells you to rerun with `--chrome-launch persistent`. See [Post to X when it will not confirm](/docs/how-to/troubleshoot/x-posting/). - **The run clicked Post but read nothing after it.** The row says `clicked, outcome not read`: check the platform, then clear the date or leave it. See [Settle something Blowhorn could not confirm](/docs/how-to/troubleshoot/clicked-not-confirmed/). ## Sign-in problems - **LinkedIn, X or Reddit says the session expired.** Sign in again in your mapped Chrome profile, or run `blowhorn profile auth` for that platform. See [Fix a sign-in that stopped working](/docs/how-to/troubleshoot/sign-in-problems/). - **A CAPTCHA or second factor appears mid-run.** The run stops and hands the browser back to you. Complete the challenge by hand, then run again. See [Fix a sign-in that stopped working](/docs/how-to/troubleshoot/sign-in-problems/). - **X sign-in under the extension driver stops at once.** The extension's typing does not reach X's form either. Sign in by hand in the tab, or run with `--chrome-launch cdp`. See [Fix a sign-in that stopped working](/docs/how-to/troubleshoot/sign-in-problems/). ## Chrome and the extension - **The extension says not connected, or the versions mismatch.** Re-approve the connection after an update, or check which install route is on disk with `blowhorn chrome extension-status`. See [Fix the Chrome extension connection](/docs/how-to/troubleshoot/extension-connection/). - **Another run holds the bridge or the profile.** Wait for it, or stop it; your rows stay queued. See [Fix the Chrome extension connection](/docs/how-to/troubleshoot/extension-connection/). - **A run refuses an unmapped profile, or asks for an allow dialog you cannot see.** Check the mapping, or switch mode for that run. See [Choose how Blowhorn reaches Chrome](/docs/how-to/settings/launch-mode/). ## Schedule and store - **A job did not run.** Ask the row directly: `blowhorn schedule why `. See [Find out why a job did not run](/docs/how-to/schedule/why-not-run/). - **Nothing is claimed on this machine.** You may be paused. See [Pause and resume posting](/docs/how-to/schedule/pause/). - **Every store-backed command exits 3 saying the store is unavailable.** Bring your connection back; nothing was claimed and nothing is lost. The full story lives in the store-unavailable guide, which ships when the customer store path does. Until then: run `blowhorn store` to probe the connection now, and `blowhorn schedule status` to read the last tick's view of it. It never probes, because the app polls it every 15 seconds. ## Related - [Find out why a job did not run](/docs/how-to/schedule/why-not-run/) - one row's state in one command - [Settle something Blowhorn could not confirm](/docs/how-to/troubleshoot/clicked-not-confirmed/) - the held-row procedure - [Change your defaults](/docs/how-to/settings/defaults/) - pace, excluded profiles, logs -------------------------------------------------------------------------------- title: "Settle something Blowhorn could not confirm" url: https://blowhorn.ai/docs/how-to/troubleshoot/clicked-not-confirmed/index.md description: Check the platform after a clicked-unread row, then clear it or leave it. -------------------------------------------------------------------------------- # Settle something Blowhorn could not confirm Blowhorn clicks a public action once per run and never clicks again on a guess. When the click lands but nothing read after it confirms the outcome, the run records `clicked, outcome not read` and stops. That record is a hold, not a result: only you settle it, by looking at the platform yourself. ## A content row When no confirmation follows the click, `Date Promoted` carries `clicked, outcome not read` with the time, no promotion link is written, and the row is not counted as posted. No later run posts it again. 1. Open the profile on the platform and check whether the post is there. 2. If it is there, record it on the row so it stays done. 3. If it is not there, and you want a run to try the row again, clear `Date Promoted` with `blowhorn content upsert`: ```bash blowhorn content upsert --row 12 --payload '{"Date Promoted": ""}' ``` Never clear the date on a guess. Clearing it re-queues the row, and if the first click did go out, the retry publishes it twice. When in doubt, leave the row held and ask for help instead of re-running it. Once the sending control was clicked, the run never reports the row as not sent. "Not sent" is kept for runs where nothing was clicked at all. ## Related - [Post to X when it will not confirm](/docs/how-to/troubleshoot/x-posting/) - what X shows after Post, and what to check - [Troubleshooting](/docs/how-to/troubleshoot/troubleshooting/) - start from a symptom instead - [Find out why a job did not run](/docs/how-to/schedule/why-not-run/) - when the row never ran at all -------------------------------------------------------------------------------- title: "Fix a sign-in that stopped working" url: https://blowhorn.ai/docs/how-to/troubleshoot/sign-in-problems/index.md description: Recover expired LinkedIn, X and Reddit sessions, and hand CAPTCHAs back to yourself. -------------------------------------------------------------------------------- # Fix a sign-in that stopped working Sessions expire. When one does, sign in again by hand in the mapped Chrome profile, or run `blowhorn profile auth` for that platform. Scheduled passes never wait for a person: an unattended job whose session is gone fails by name instead of hanging. ## LinkedIn When LinkedIn challenges the session or signs it out, the run stops and names the step. Sign in again in the mapped Chrome profile's window, complete any challenge LinkedIn shows, then run again. If the challenge keeps returning, pause the schedule until it clears so no pass spends itself against it. ## X Check the session first without posting anything: ```bash blowhorn profile auth --platform x --profile ada ``` Under the extension driver the walk runs nowhere: the extension's typing does not reach X's sign-in form, so sign in by hand in the tab, or run with `--chrome-launch cdp` to use the stored credentials. Blowhorn types only into the sign-in dialog's own field, and only into a field a person could actually click. It never navigates the page away, never submits the username twice, and leaves the form where it is when the walk stops so you can finish it yourself. When the page says the account is limited or disabled, the run stops rather than waiting or retrying. Wait until X permits another sign-in before trying again. ## Reddit When Reddit reports the session expired, sign in again in the mapped Chrome profile and run again. A failed step leaves a screenshot and a scrubbed copy of the page under the checkout's `cache/diagnostics//` directory, and names the step and the reason. Field values are never in that copy. ## CAPTCHA and second factors Any CAPTCHA or second factor hands the browser back to you: the run stops, completes nothing on your behalf, and leaves the page where it is. Complete the challenge once by hand, then run again. For a login that always needs you present, run with `--headed` so the window stays visible. ## Related - [Post to X when it will not confirm](/docs/how-to/troubleshoot/x-posting/) - the X composer and its confirmation - [Troubleshooting](/docs/how-to/troubleshoot/troubleshooting/) - start from a symptom instead - [Pause and resume posting](/docs/how-to/schedule/pause/) - hold the schedule while a challenge clears - [Choose how Blowhorn reaches Chrome](/docs/how-to/settings/launch-mode/) - which mode a sign-in runs under - [You're in control](/docs/explanation/youre-in-control/) - why CAPTCHAs and 2FA are left to you -------------------------------------------------------------------------------- title: "Post to X when it will not confirm" url: https://blowhorn.ai/docs/how-to/troubleshoot/x-posting/index.md description: What Blowhorn reads after Post on X, the extension typing limit, and what to check. -------------------------------------------------------------------------------- # Post to X when it will not confirm Blowhorn posts to X through the browser, not the X API. It clicks Post once per row and never a second time in the same run. After the click it reads back what it can: the page address for a `/status/` link, the sent banner's view link, or the post's own words in the feed. None of those readings was ever captured against the live service, so every one of them is a model, not a fact. ## Under the extension driver, nothing is typed The extension's typing sets X's editor text without the input event X reads, so X keeps an empty post and Post stays disabled. The run stops before anything is typed or clicked: ```text X post NOT sent: the Chrome extension driver's typing does not reach X's editor (it sets the text and fires no input event, so X keeps an empty post and Post stays disabled). Nothing was typed. Run this post with --chrome-launch persistent. ``` Nothing was clicked, so "not sent" is the true report here and the row stays pending. Run the post again with `--chrome-launch persistent`. A dry run stops with the same line, so preview a row under the driver you will post with. ## After the click, check the account When the click lands but the read-back shows nothing, the row is held as `clicked, outcome not read` and no later run posts it again. Open the account on x.com before the next pass: if the post is there, record it on the row; if it is not, clear `Date Promoted` only when you have looked and want a retry. The full procedure is [Settle something Blowhorn could not confirm](/docs/how-to/troubleshoot/clicked-not-confirmed/). Long posts split into a thread at 274 characters, at sentence and paragraph breaks, with media on the first post only. ## Related - [Settle something Blowhorn could not confirm](/docs/how-to/troubleshoot/clicked-not-confirmed/) - the held-row procedure - [Fix a sign-in that stopped working](/docs/how-to/troubleshoot/sign-in-problems/) - expired X sessions - [Choose how Blowhorn reaches Chrome](/docs/how-to/settings/launch-mode/) - run X under another mode - [Troubleshooting](/docs/how-to/troubleshoot/troubleshooting/) - start from a symptom instead -------------------------------------------------------------------------------- title: "Fix the Chrome extension connection" url: https://blowhorn.ai/docs/how-to/troubleshoot/extension-connection/index.md description: Reconnect the extension, resolve version mismatch, and clear bridge and profile contention. -------------------------------------------------------------------------------- # Fix the Chrome extension connection When a run says the extension is not connected, ask the extension itself first. This command opens no tab and contacts no platform: ```bash blowhorn chrome extension-status ``` It reports whether the extension directory is installed, whether the native host manifest is missing, current or stale against the checkout, which install route is on disk (policy, external, or none), and whether the extension answers hello on its socket. An answered hello proves the extension is loaded in the real Chrome and talking. ## Not connected Work through the report top to bottom: a missing directory or manifest means the install did not finish, so install it again with your administrator. A stale manifest means the checkout moved on without it: reinstall the native host from the current checkout, then reload the extension on `chrome://extensions`. ## Version mismatch after an update A Blowhorn upgrade does not reinstall the extension. After an update, take the store update, or reload the unpacked copy on `chrome://extensions`, then run `blowhorn chrome extension-status` again until hello answers. ## Bridge busy ```text another blowhorn run holds the Chrome extension bridge ``` One mapped profile runs in one process at a time. Another run is acting as that profile: wait for it, or stop it. Your rows stay queued, and a scheduled job tries again next tick. ## Profile in use When a run says the profile is in use by another Blowhorn process, the same rule holds under any driver: wait for that run, or stop it. The profile is skipped and its rows stay queued. ## Related - [Choose how Blowhorn reaches Chrome](/docs/how-to/settings/launch-mode/) - run under another mode instead - [Troubleshooting](/docs/how-to/troubleshoot/troubleshooting/) - start from a symptom instead - [Keep posting when the app is closed](/docs/how-to/schedule/background-service/) - stop a pass cleanly before retrying -------------------------------------------------------------------------------- title: "Uninstall Blowhorn" url: https://blowhorn.ai/docs/how-to/manage/uninstall/index.md description: Remove the app, the service, the extension host and the checkout's build products. macOS only. -------------------------------------------------------------------------------- # Uninstall Blowhorn Remove Blowhorn from your Mac: the background service, the Chrome native host, the launchers, the desktop app and the checkout's build products. Your data stays unless you ask for it to go, and the shared store is never touched. macOS only. On Linux the command is refused by name and nothing is read or removed. ## See what would be removed Always start with the dry run. It lists everything the installer, the app, the service and the extension install can leave on this machine, says whether each is present, and removes nothing: ```bash blowhorn uninstall --dry-run ``` Each line carries one result, `would remove`, `not present`, `not checked`, `kept`, `not unregistered`, or `never touched` for the shared store, with the path. `not checked` means the item's state could not be read, with the reason; nothing is done to it, and it is never reported as absent. ## Remove Blowhorn and keep your data ```bash blowhorn uninstall ``` The command removes, in order, reporting each item as `removed`, `not present`, `not unregistered` or `failed`: 1. **The background service.** Each loaded label is booted out and waited for, then its file under `~/Library/LaunchAgents` is removed. A label the app registered as its login item is booted out the same way; its Login Items entry is unregistered by the app itself in step 5. 2. **The Chrome native host.** The manifests under Chrome's `NativeMessagingHosts`, the launchers and the bridge socket beside them. 3. **The every-profile install entries.** The external-extensions file and Blowhorn's own entry in the managed policy, removed with the same administrator prompt that wrote them; other keys in that file stay. 4. **The launchers.** `~/.local/bin/blowhorn` and the pre-rename shim, when they carry the installer's marker. A file there that `install.sh` did not write is kept and says so. Pass `--bin-dir` if you installed elsewhere. 5. **The desktop app.** Each bundle first unregisters its own Login Items entries, then is removed. A bundle too old to unregister itself is reported `not unregistered`, naming the step left to you under System Settings > General > Login Items. 6. **The checkout's build products.** The virtualenv (removed only when it holds `pyvenv.cfg`), `node_modules/`, `desktop/node_modules/` and `logs/`. A failed item does not stop the run: everything else is still removed, and the command exits 1. Kept at the end, and named: the store connection file, the app's preferences and engine record, Blowhorn's own Chrome trees, the per-user Library state, the checkout's `cache/`, and the scheduler pause file. ## Remove the local data too ```bash blowhorn uninstall --purge ``` `--purge` removes the kept list above as well. It shows what it is about to remove and asks you to type `purge`; anything else cancels and removes nothing. From a script with no terminal, pass `--yes`. `--json` needs `--yes` too, because the prompt would otherwise land in the JSON output. The shared store, the content queue, the schedule, the profiles and the ledgers on it, is not local data and is never touched, under any flag. Other machines keep using it. ## When the venv is already gone `uninstall.sh` at the checkout root runs the same code with the system Python and takes the same flags: ```bash ./uninstall.sh --dry-run ./uninstall.sh ./uninstall.sh --purge ``` ## When it refuses The command removes nothing, in a dry run too, while any of these is running, and names it: a scheduler tick, a pass of the background service, a run holding the extension bridge, the desktop app, or run records still waiting in the local spool for the store. Send those records first: ```bash blowhorn store --replay-spool --log-dir blowhorn uninstall ``` When the store will never be reachable again from this machine, give the records up instead with `blowhorn uninstall --discard-unsynced-runs`, which asks you to type `discard`. Quit the app or wait for the run to finish, then run the command again. `blowhorn service status` and `blowhorn chrome extension-status` show what is running. ## Reinstalling afterwards A fresh `./install.sh` from the same checkout produces a working install. Your kept store connection file is picked up as it was. ## Related - [Keep posting when the app is closed](/docs/how-to/schedule/background-service/) - stop the service before removing it - [Fix the Chrome extension connection](/docs/how-to/troubleshoot/extension-connection/) - check what is running first - [Change your defaults](/docs/how-to/settings/defaults/) - log directories the dry run reports on -------------------------------------------------------------------------------- title: "System requirements" url: https://blowhorn.ai/docs/reference/requirements/index.md description: What your Mac needs to run Blowhorn: macOS version, Chrome, and a Layer5 Cloud organization. -------------------------------------------------------------------------------- # System requirements What your Mac needs before you install Blowhorn. | Need | Requirement | Checked by | |---|---|---| | macOS | 13 or newer, Apple silicon or Intel (one universal build) | The build declares the floor; Setup checks the rest | | Browser | Google Chrome, installed and signed in | Setup, "chrome profiles" row; `blowhorn chrome status` | | Engine | A Blowhorn checkout with a working Python environment | Setup, "blowhorn checkout" and "python environment" rows | | Organization | A Layer5 Cloud organization your profile belongs to | Setup, "store" row; `blowhorn store` | | Background runs | The background service, registered from the app | Setup, "background service" row | macOS 13 is the floor the desktop build declares (`minimumSystemVersion: 13.0`), and every release is one universal DMG (`Blowhorn--universal.dmg`) for Apple silicon and Intel Macs. The engine underneath also runs on Linux; the desktop app needs a Mac. Windows is not supported. Chrome must be the real Google Chrome, not Chromium and not a headless build. Blowhorn drives the Chrome profiles you already use, so install Chrome the normal way and sign in before you map anything. The extension needs Chrome profiles to attach to; without Chrome every browser run refuses before it acts. Python 3.14 is the floor `install.sh` enforces, and it is the only supported version. The desktop app carries its own engine check on the Setup screen, so a broken checkout or interpreter shows up there first, not mid-run. Network access to your Layer5 Cloud organization is required for the content queue, the schedule, profiles, and run history. The store holds those; your Mac holds the app, the engine, and your Chrome sessions. Offline behavior is a plans question, not a requirements one: see [Plans and limits](/docs/reference/plans/). ## Related - [The Blowhorn app, screen by screen](/docs/reference/use-the-desktop-app/) - what the Setup screen checks. - [Plans and limits](/docs/reference/plans/) - what "free during early access" covers. - [How Blowhorn works](/docs/explanation/how-it-works/) - why the store and the app are separate. -------------------------------------------------------------------------------- title: "The Blowhorn app, screen by screen" url: https://blowhorn.ai/docs/reference/use-the-desktop-app/index.md description: The menu-bar popover, the console screens, Setup, Settings, and what each one does. -------------------------------------------------------------------------------- # The Blowhorn app, screen by screen What each part of the Blowhorn desktop app shows and does. This page describes; the steps live in the how-to guides linked under Related. ## The menu bar and the popover Blowhorn lives in the menu bar. Opening it shows the popover, which has two boards. The running board shows the scheduler, what is due now, what is running, what needs you, and the content queue with a countdown to the next tick. The paused board shows PAUSED with each tick-level pause scope stated once, and the rows that would post now wait instead of posting. A store outage puts its line above the board. The popover footer holds the pause controls and, when an update is ready, the update card. The update installs and relaunches the app when this machine can reach the release; otherwise it opens the release page in your browser. ## The console The console is the full window, reached from the popover. Its rail, in order: "jobs", "content", "runs", "profiles", "analytics", "service", "settings". Each screen reads live state on open. Nothing here edits on a timer. | Screen | Shows | Acts | |---|---|---| | jobs | Every schedule row, whether it is due, paused, or held; a job drawer with the row behind it; a bootstrap preview for a starter set | Create, edit, pause or resume one row; run one row now; apply the bootstrap set | | content | The queue: pending rows, rows still owed to some profile, clicked-but-unread rows | Edit a row; clearing a clicked row's "Date Promoted" re-queues it | | runs | Past runs with their ledger lines; per-job logs under the log directory | Re-run a failure from its row | | profiles | Every profile, its Chrome mapping, its per-platform sessions | Add a profile; map or unmap its Chrome profile | | analytics | Followers, growth, and operations per profile; collection freshness in the header | Collect now; open the HTML report | | service | Who registered the background service, whether it runs, the last tick heartbeat | Install, start, stop, restart, update, or uninstall the service | | settings | Engine defaults rendered from the CLI's own option list, the extension card, the store card | Change defaults, check the extension, test the store connection | The analytics screen states how fresh its numbers are in the header and has no refresh control of its own. The runs screen reaches per-job logs through the log directory listing, which includes each capture file and its rollover. The service screen is the visible face of `blowhorn service`; every button there runs the matching command. ## Setup Setup is the first-run checklist, five rows read from this machine: "blowhorn checkout", "python environment", "store", "chrome profiles", and "background service". Each row reports ok, warn, or off with the one fix that clears it. The "map chrome profiles" panel and the "background service" row act directly: mapping a profile and registering the service happen here, not in a terminal. ## Settings Settings edits the same `.blowhorn.yaml` defaults the CLI reads: pace, excluded profiles, dry run, notifications, logs, and the Chrome launch mode. The engine-defaults card renders from `blowhorn config show`, so the app can never offer an option the engine does not know. The extension card reports whether the extension is installed, which version answers, and whether it says connected. Quitting the app leaves posting to the background service and the keep-running setting; see [What runs when the app is closed](/docs/explanation/distribution/). ## Related - [Map Blowhorn profiles to Chrome profiles](/docs/how-to/set-up/map-chrome-profiles/) - the Setup panel behind the mapping. - [Change your defaults](/docs/how-to/settings/defaults/) - pace, exclusions, logs. - [Settings and configuration](/docs/reference/configuration/) - every option the app edits. - [How Blowhorn works](/docs/explanation/how-it-works/) - app, engine, extension, and store. - [You're in control](/docs/explanation/youre-in-control/) - the controls behind pause and Settings. - [Install the app](/docs/how-to/set-up/install-the-app/) - get the app onto a Mac. -------------------------------------------------------------------------------- title: "Platforms" url: https://blowhorn.ai/docs/reference/platforms/index.md description: What Blowhorn does on each platform: actions, browser or API, sign-in, and limits. -------------------------------------------------------------------------------- # Platforms What Blowhorn can do on each of the seven platforms, how it reaches each one, and the limits it honors. This page describes; the steps live in the how-to guides linked under Related. {{< major >}}Two hundred seventy-four characters before X wants a thread. I don't make the rules. I thread them.{{< /major >}} Four platforms drive your own signed-in Chrome through the extension: LinkedIn, X, Reddit, and Hacker News. Three post through their APIs above the browser: Slack, Bluesky, and GitHub. A platform is usable the moment its credential exists; see [Who can post as whom](/docs/explanation/eligibility/). `blowhorn post --platform` accepts one of: `linkedin`, `x`, `reddit`, `hn`, `slack`, `bluesky`, `github`, or `all`. `twitter` reads as `x` and `bsky` as `bluesky`. GitHub is amplify-only under `post`: it needs an issue or pull-request URL and refuses without one. ## LinkedIn Browser, through the extension, in the Chrome profile you mapped. Posts to the feed from a row; comments on a post when the row's "Comment on" holds its address; amplifies by reposting. Sign-in is your LinkedIn username and password in your Chrome, by hand; the credential Blowhorn checks is `LI_USERNAME` with `LI_PASSWORD`. ### LinkedIn comments A row with "Comment on" set to a `linkedin.com` post address and a "Message Text" becomes a comment on that post instead of a new post. The same row is also picked up by `blowhorn comment`. A row whose "Comment on" names anything else stays a normal post. ## X (Twitter) Browser, through the extension. Posts from a row; a message past 274 characters goes out as a thread. Replies go through a post row that targets the conversation. Amplifies by retweet or quote. Follows accounts with `blowhorn follow`. Analytics appends a follower and following trend row per run. Sign-in is your X username in your Chrome, by hand; the credential Blowhorn checks is `X_USERNAME`. Follows: at most 400 per profile a day and 15 in any 15 minutes, counted from that profile's follow log on this Mac. ### X (Twitter) replies Replying is a post row addressed at the conversation, not a separate command. There is no comment flow for X under `blowhorn comment`. ## Reddit Browser, through the extension. Posts to the subreddit the row's "Destination" names as `r/`; with no subreddit the row is skipped, never guessed. Comments when "Comment on" holds a Reddit post or comment address. Amplifies with an upvote. Sign-in is your Reddit username in your Chrome, by hand; the credential Blowhorn checks is `RDDT_USERNAME`. ### Reddit posts and comments Posting needs the subreddit in "Destination" or, for an upvote, in "Amplify". Commenting needs a Reddit address in "Comment on" plus a "Message Text". Both `post` and `comment` act on comment rows; a row already marked done is never acted on twice. ## Hacker News Browser, through the extension, signed in with the username and password in the store (`HN_USERNAME` with `HN_PASSWORD`). Submits link and text posts and comments from rows. Titles hold 80 characters; a longer title fails the row rather than truncating, because a submission cannot be edited after it lands. At most 2 link submissions per profile per UTC day count; only link submissions are recorded, so only they count toward the cap. Blowhorn never votes: there is nothing to configure and no way to turn it on. ### Hacker News submissions and comments A link row needs the URL; a text post needs the title and text. A row with "Comment on" set becomes a comment on that item. Every submission is permanent and every comment lands under your name, so preview each new row with `--dry-run` first. Submissions HN rate-limits fail the row; see [Messages and exit codes](/docs/reference/messages/). ## Slack API, never the browser. Sends a message as you to a channel, a person, or a thread: the target is `#channel`, `@user`, a channel id, a channel link, or a message link for a thread reply. One profile sends; the credential is a Slack token in the store (`SLACK_USER_TOKEN`, `SLACK_BOT_TOKEN`, or a workspace session captured through the extension). `blowhorn post --platform slack --message` sends one ad-hoc message with no queue row. ### Slack messages Send from a queue row or ad hoc. A row sends to its own target; `--message` with `--target` sends one message you type, with no row read or marked done, and `--target` redirects the run's queued Slack rows. The target is a `#channel`, `@user`, channel id, channel link, or message link for a thread reply; anything else is refused, never sent somewhere else. One profile sends per run, named with `--profile`; the profile from the defaults file does not count as named. ## Bluesky API, never the browser. Posts up to 300 characters per post; longer messages split into threads. Reposts for amplify rows. Follows with `blowhorn follow`, unfollows one account with `blowhorn unfollow`, searches posts with `blowhorn find`. Analytics appends a follower and following trend row per run. The credential is `BLUESKY_HANDLE` with `BLUESKY_APP_PASSWORD`: an app password, not your main password. ## GitHub API, never the browser. Follows accounts with `blowhorn follow` and lists an audience with `blowhorn source --platform github --repo`. Under `post`, GitHub amplifies only: it adds all five uplifting reactions (+1, laugh, hooray, heart, rocket) to the issue or pull request the `--amplify` flag or the row's "Amplify" column names, from every selected profile under its own token. Re-running adds none of them twice. The credential is `GH_TOKEN` in the store. ## Captured versus unverified, by platform Blowhorn works on each platform by reading signals off it: a selector, a button's wording, the page a submit lands on, the shape of an API answer. Each table below says how much evidence stands behind each of those readings, so you know how far to trust a line in your run's output. There are three kinds, strongest first: - **Captured** - a saved page, a dump, or quoted markup from the real service stands behind it, on a known date. - **Probed** - someone watched the real service do it on a known date and kept nothing of the page. - **Unverified** - never observed against the real service; written from a model of the site or from its documentation. Unverified is a statement about Blowhorn's evidence, not a claim that a platform is broken or that your post will fail. What it changes is what your run tells you: where a success rests on an unverified reading, the run says what it did or read - `Clicked Like; nothing was read to confirm LinkedIn accepted it.` - rather than stating the outcome as a fact. Where nothing could be read after a click, the row is recorded as *clicked, outcome not read*: it is not counted, not repeated, and waits for you to check it. Only one landing after a submit has been captured on any platform: a Hacker News reply, on 2026-09-18. A run that worked and a green test suite never upgrade a reading; only a dated capture of the page a submit lands on does. ### LinkedIn | State | Captured, probed or unverified | |---|---| | The Page composer's media and link-preview controls | Captured 2026-08-17 - a showcase Page | | **A Page post landed: found in the Page's published list** | Probed 2026-08-18 - watched the check find a real post and refuse an absent one; nothing kept. The one landing Blowhorn confirms by going and looking | | The Repost menu, the repost composer, the reaction control | Captured 2026-08-18 - a post page; a second wording on 2026-08-28 | | The sign-in page's fields and submit control | Probed 2026-08-24 - a signed-out browser; nothing kept | | The Accept control on received invitations | Captured 2026-08-25 - the received list | | A row with no Accept control, and the toast after a real accept | Probed 2026-08-25 - the received list, watched live; nothing kept | | A post's author, and whether you already reposted it | Captured 2026-08-28 - one post, six sessions, on both page designs | | The withdraw confirmation | Probed 2026-08-10 - the sent list, during a manual run; nothing kept | | **A feed post or a group post landed** | Unverified (model) - Blowhorn compares the newest item on your activity page or the group page before and after the click. A new item of yours marks the row done with its link; anything else holds the row as clicked, outcome not read | | **A comment landed** | Unverified (model) - nothing is read after the click | | **An instant repost landed** | Unverified (model) - the repost counts only when your activity feed shows it; otherwise the row is held as clicked, outcome not read, and Repost is not clicked again | | **A repost with thoughts, or a like, landed** | Unverified (model) - nothing is read after the click; a repost with thoughts is held as clicked, outcome not read | | **A Page post landed**: a toast, or the composer closing | Unverified (model) - a false "not sent" is caught by the published-list check above | | **An acceptance or a withdrawal went through**: the row leaves the list | Unverified (model) - doubt halts the run; it never reports one that did not happen | | The "Take care when connecting" warning | Unverified (model) - if missed, the flagged invitation stays on the list and is never accepted | | Which page means signed in, signed out, or a challenge | Unverified (model) - a wrong reading falls back to signing in by hand | | A new post's link | Unverified (model) - if wrong, the row records no link | | Page analytics: date range, export, highlight labels | Unverified (model) - read-only; a wrong reading records a wrong number, or none | ### X | State | Captured, probed or unverified | |---|---| | The signed-in markers on a profile page | Captured 2026-08-07 - a signed-in profile page | | The sign-in username step: two forms, the dialog field, Continue | Captured 2026-09-17 - a signed-out page, nothing submitted | | **A post or thread landed** | Unverified (model) - after the dialog closes, Blowhorn reads the post back under your own handle. A match marks the row done; anything else holds it as clicked, outcome not read. Post is clicked once per run | | **A repost, reply, like, or follow landed** | Unverified (model) - nothing is read after the click; a repost or reply is held as clicked, outcome not read, and a follow is skipped next time but not counted as followed | | A new post's link | Unverified (model) - if wrong, the row records no link | | The compose page, editor, Post button, and media button | Unverified (model) - a miss sends nothing; a failed upload posts the text without its image | | The password step, signed-out markers, and challenge wording | Unverified (model) - a wrong reading falls back to signing in by hand | | The follower and following counts | Unverified (model) - read-only | | The 274-character limit | Unverified (model) - if wrong, a post splits into a thread early | ### Reddit | State | Captured, probed or unverified | |---|---| | **A new post landed**: the browser reaches the post's comments page | Unverified (model) - Blowhorn then looks for your title on that page. A match marks the row done; anything else holds it as clicked, outcome not read, and it is not posted again | | **A comment, reply, or upvote landed** | Unverified (model) - nothing is read after the click; the row is held as clicked, outcome not read | | A new comment's link | Unverified (model) - printed when read, not recorded | | The upvote control and its already-upvoted state | Unverified (model) - if that is not the real signal, a second run could undo the upvote | | The switch to Markdown | Unverified (model) - if missed, Markdown publishes as literal characters | | The submit form, comment box, session markers, sign-in, and challenges | Unverified (model) - a miss sends nothing, or falls back to signing in by hand | ### Hacker News | State | Captured, probed or unverified | |---|---| | The sign-in page's two forms, and `/submit` signed out | Captured 2026-08-18 - signed out, read-only | | Story and comment rows, permalinks, and the comment form | Captured 2026-08-18 - an item page, signed out | | The expired-link page | Captured 2026-08-18 - signed out | | The signed-out top bar and bylines | Captured 2026-08-18 - listing and item pages | | The read-only API's answers | Captured 2026-08-18 - user and item lookups, unauthenticated | | The signed-in submit form | Captured 2026-09-18 - saved by hand, session tokens removed | | Form pages show no logout link even signed in | Captured 2026-09-18 - `/submit` and `/reply` | | The signed-in top bar, reply links, and vote arrows | Captured 2026-09-18 - item and listing pages. Blowhorn never clicks an arrow | | **A reply landed**: the story page, with the new comment carrying edit and delete links | Captured 2026-09-18 - after a reply posted by hand. The one landing captured on any platform | | **A story landed** | Unverified (model) - if neither the page nor the API confirms it, the story is reported unconfirmed | | HN sends a repeat URL to the existing story | Unverified (docs) | | The API lists a new story within seconds | Unverified (model) | | The rate-limit pages and the reCAPTCHA signal | Unverified (model) - a limiter worded otherwise is missed for one more attempt | | A comment's own page shows the reply form | Unverified (model) - if not, the row fails with "No comment form" and nothing is posted | | **A top-level comment landed** | Unverified (model) - if wrong, the comment is reported unfound; the row is still done with the thread's link | ### Bluesky | State | Captured, probed or unverified | |---|---| | **A post, repost, reply, follow, or unfollow was accepted** | Unverified (docs) - the SDK's documented answers, never recorded from a real response | | A post's link, built from your handle and the answer | Unverified (docs) - built, not read | | The sign-in errors that separate a refused app password from an outage | Unverified (docs) - misread, you could be told to replace an app password that works | | The 300-character limit, link shapes, and thread and profile reads | Unverified (docs) | | Where to create an app password, as the refusal tells you | Unverified (model) | ### Slack | State | Captured, probed or unverified | |---|---| | **A message was posted**: Slack answers `ok` | Unverified (docs) - never recorded from a real response | | A message's link, built from the answer | Unverified (docs) | | How a captured workspace session is used | Unverified (model) - Slack's web client convention, undocumented by Slack | | Capturing a workspace session from your browser | Unverified (model) - a miss captures nothing and posts nothing | | Sign-in errors, OAuth answers, and link shapes | Unverified (docs) | | A resend is not duplicated | Unverified (docs) - Blowhorn's own send record keeps each send to once | | The marks of an app-attributed message | Unverified (docs) - a miss reports an attributed message as clean | | A rate limit's wait, and an unreadable answer | Unverified (docs, model) - such a send is marked outcome unknown and never resent | ### GitHub | State | Captured, probed or unverified | |---|---| | A missing token scope answers 404, not 403 | Probed 2026-08-25 - reproduced live; nothing kept | | Pull-request reactions use the issues endpoint | Probed 2026-08-26 - answered live; nothing kept | | The reactions endpoint asks for the `repo` scope | Captured 2026-08-26 - a request against a repository that does not exist, so nothing was created | | **A reaction was added, or was already there** | Unverified (docs) - re-running never duplicates a reaction | | **A follow went through**, and the follow-state read | Unverified (docs) | | Rate-limit signals, pagination, and listings | Unverified (docs) | | A repository audience's usernames, public emails, and earliest commits | Unverified (docs) | ## Related - [Preview and publish queued posts](/docs/how-to/publish/post-content/) - the posting run. - [Post to Hacker News](/docs/how-to/publish/post-to-hacker-news/) - the strictest platform, worked. - [Content queue columns and row states](/docs/reference/content-queue/) - every column each platform reads. - [Messages and exit codes](/docs/reference/messages/) - what a refusal means. - [Who can post as whom](/docs/explanation/eligibility/) - why a credential is permission. - [Why Blowhorn uses your own Chrome](/docs/explanation/your-own-chrome/) - browser versus API. - [You're in control](/docs/explanation/youre-in-control/) - every safety control in one place. - [Send a Slack message](/docs/how-to/publish/send-a-slack-message/) - Slack, worked. -------------------------------------------------------------------------------- title: "Content queue columns and row states" url: https://blowhorn.ai/docs/reference/content-queue/index.md description: Every content column, what each platform reads from a row, and what each row state means. -------------------------------------------------------------------------------- # Content queue columns and row states The queue is the set of content rows `blowhorn post` publishes. Each row is one message; each run publishes the rows that are still owed to the profiles in scope. Columns may arrive in any order, and extra columns beside them are preserved on write. ## Columns | Column | Means | Read by | |---|---|---| | Platform | Where the row goes: `linkedin`, `x`, `reddit`, `hn`, `slack`, `bluesky`, or `github` (`twitter` reads as `x`, `bsky` as `bluesky`; `Venue` is the old spelling) | Every run, as the platform gate | | Profile | Which profile publishes it; blank or naming nothing resolvable is refused, never widened to everybody | The profile match | | Destination | Where on the platform: a subreddit as `r/` for Reddit; the channel, person, or thread for Slack's "Amplify" use | Reddit, Slack | | Message Text | The message itself | Every platform that publishes | | Title | The title where the platform needs one: Hacker News submissions | Hacker News | | Encoded Post | The pre-encoded form, when the row carries one | The post path | | Approved? | `yes` or `true` selects the row; anything else leaves it out | `get_posts_to_process` | | Promote On | When the row becomes due; unparseable is refused, never guessed | The due check | | Date Promoted | When each profile published it; clear it to try the row again | The done bookkeeping | | Promotion Link | Where the published post landed, when a read-back showed it | Reporting | | GDrive Link | Drive attachment source for rows that carry one | The post path | | Image URL | Image attached to the post | Platforms that take images | | Video URL | Video attached to the post | Platforms that take video | | Amplify | Address of existing content to amplify instead of publishing: a post URL for repost, retweet, quote, or upvote; an issue or pull request for GitHub reactions; a Slack channel, person, or thread as the send target | The amplify path per platform | | Comment on | Address to comment on instead of publishing: a LinkedIn, Reddit, or Bluesky post or comment address with a Message Text | `post` and `comment` | A row with "Comment on" set can only ever be a comment. A row with an "Amplify" GitHub target can only ever be reactions. Timestamps in content values read `%Y-%m-%d %H:%M:%S`. ## Row states | State | Means | |---|---| | pending | Eligible by the row's own fields; whether a profile picks it up is per-profile | | scheduled | Due in the future under "Promote On" | | partial | Landed for some profiles, still owed to others | | done | Published and recorded, with "Date Promoted" stamped | | unread | Clicked, outcome not read: the sending control was clicked and no confirmation was read back. Not pending again, and not done. "Date Promoted" carries `clicked, outcome not read` and no promotion link is written | | draft | Not yet approved for publishing | | invalid | Refused: an unresolvable profile, a blank profile, an unparseable date, an overlong HN title | | unsupported | Names a platform no command in scope acts on | Clearing an unread row's "Date Promoted" tries the row again. Neither a retry nor a drop is a run's to decide: the run records, you decide. ## Related - [Manage the content queue](/docs/how-to/publish/manage-the-content-worksheet/) - add, edit, and approve rows. - [Preview and publish queued posts](/docs/how-to/publish/post-content/) - the run that reads the rows. - [Post to Hacker News](/docs/how-to/publish/post-to-hacker-news/) - titles, caps, permanence. - [Platforms](/docs/reference/platforms/) - what each platform does with a row. - [Messages and exit codes](/docs/reference/messages/) - what each refusal sentence means. - [How Blowhorn avoids repeat posts](/docs/explanation/never-twice/) - why unread is not done. -------------------------------------------------------------------------------- title: "Job options and schedules" url: https://blowhorn.ai/docs/reference/jobs/index.md description: Schedule rows, recurrence words, driver mode, and the three pause scopes. -------------------------------------------------------------------------------- # Job options and schedules What a schedule row holds, when it runs, and what keeps it from running. This page describes; the steps live in the how-to guides linked under Related. A pass runs every ten minutes and claims the rows that are due. Several Macs sharing one store share the queue: a running row carries a lease, and a row whose lease lapsed is reclaimable by the next pass. ## The row | Field | Means | |---|---| | Command | What the pass runs: `post` and the other schedulable commands (`source` is not schedulable) | | Platforms | Row filter, not a posting target: which rows the command reads | | Profiles | Which profiles the run acts as | | Recurrence | When the row comes due again | | Driver mode | How the run reaches Chrome: `default`, `extension`, `cdp`, or `persistent` | | Status | Where the row stands; `running` is a lease, never a lock | `blowhorn schedule list` shows each row and whether it is due. `blowhorn schedule get` returns one row with all fields. `blowhorn schedule upsert` creates or updates a row from a JSON payload (`--row` names the row; `--expected-fingerprint` rejects the write when the row changed underneath you). `blowhorn schedule delete` removes a row by number. `blowhorn schedule validate` checks every row against the schema. `blowhorn schedule bootstrap` previews or writes a starter set. `blowhorn schedule trigger-now` forces one row to run immediately instead of waiting for the pass. ## Recurrence | Word | Means | |---|---| | `once`, `none`, or blank | Runs one time, then never again | | `hourly` | Every hour | | `daily` | Every day | | `weekly` | Every week | | `monthly` | Every 30 days | | `every:N` | Every N minutes | Anything else is refused as an unsupported recurrence. ## Driver mode `default` follows the same precedence every run follows: the `--chrome-launch` flag, then the job's own mode, then the configured default, which is the extension. Naming `extension`, `cdp`, or `persistent` pins the job to that driver. A job pinned to a driver that cannot do the work is refused by name before anything opens. ## Pause scopes A row is claimed only when none of the three scopes says paused: | Scope | Pauses | Lifted by | |---|---|---| | `machine` | This Mac only, in `.blowhorn.pause.json` | `blowhorn schedule resume` on this Mac | | `all` | Every Mac sharing the store | Resuming the organization-wide pause | | `row` | One scheduled row, everywhere | Resuming that row | `blowhorn schedule pause` pauses; `blowhorn schedule resume` resumes. `blowhorn schedule status` shows whether processing is paused and the last tick's heartbeat. `blowhorn schedule why ` explains why one row has or has not run on this machine. Quitting the app pauses nothing: the file, the organization-wide pause, and the row pauses keep exactly what they said. ## Related - [Schedule a job](/docs/how-to/schedule/schedule-a-job/) - create, pause, and resume jobs. - [The Blowhorn app, screen by screen](/docs/reference/use-the-desktop-app/) - the "jobs" screen and the popover boards. - [Settings and configuration](/docs/reference/configuration/) - the launch-mode default a job inherits. - [How scheduling works](/docs/explanation/scheduling/) - passes, leases, and why a row waited. - [CLI reference](/docs/reference/cli/) - the schedule commands. -------------------------------------------------------------------------------- title: "`blowhorn post` reference" url: https://blowhorn.ai/docs/reference/post/index.md description: The posting command in full: pending rows, every flag, and per-platform behavior. -------------------------------------------------------------------------------- # `blowhorn post` reference Publishes the queue's pending rows and amplifies existing content. This page describes the command in full; the [CLI reference](/docs/reference/cli/) lists every command with its flags, and the steps live in the how-to guides under Related. ## What "pending" means A row is pending when "Approved?" says `yes` or `true`, "Date Promoted" is empty, "Promote On" is due or blank, and the profile and platform resolve. `--check-queue` lists the pending rows without publishing anything, opening no browser and hitting no API. `--dry-run` walks the whole run, printing what would go out, and marks nothing done. Posting is one click per run. A clicked control with no confirmation read back is recorded as clicked, outcome not read, counted for nothing, and never retried by the run. See [How Blowhorn avoids repeat posts](/docs/explanation/never-twice/). ## Flags | Flag | Means | |---|---| | `--profile` | Name, comma-delimited names, or `all` (the default). A profile you name that is eligible for nothing in scope stops the run with the reason; `all` filters quietly | | `--platform` | One of `bluesky`, `github`, `hn`, `linkedin`, `reddit`, `slack`, `x`, or `all` | | `--exclude`, `--e` | Profiles left out of `--profile all`; naming a profile explicitly still runs it | | `--target` | Ad-hoc target overriding the row for this run. Slack: `#channel`, `@user`, a channel link, or a message link for a thread reply. Other platforms: leave unset and use "Comment on" | | `--message` | One ad-hoc Slack message instead of queue rows: no row read or marked done. Needs `--platform slack`, one named profile, and `--target`; refused with `--check-queue` or `--amplify` | | `--amplify` | URL of existing content to amplify instead of publishing: LinkedIn repost, X retweet or quote, Reddit upvote, Bluesky repost, GitHub reactions. Overrides the row's "Amplify" column; ignored for Slack rows | | `--check-queue` | List pending rows and stop; read-only | | `--dry-run` | Simulate everything: no publishing, no following, no writing | | `--pace` | `fast`, `normal`, `slow`, or a numeric multiplier on the pauses between actions | | `--headless` | Unattended: a background window, never headless Chrome, and no waiting for a person at a sign-in or challenge | | `--headed` | Keep the browser visible and in front; wins over `--headless` from a higher layer | | `--chrome-launch` | How this run reaches Chrome, for one run | | `--slack-auth` | Which Slack credential may send, for one run | | `--config`, `--log-dir`, `--verbose` | Config file path, log directory, verbose output | GitHub is amplify-only under `post`: `--platform github` needs an `--amplify` issue or pull-request URL, or a GitHub row's "Amplify" column, and refuses without one. ## Platform behavior LinkedIn, X, Reddit, and Hacker News drive your Chrome through the extension. Slack, Bluesky, and GitHub post through their APIs. Reddit rows need their subreddit as `r/` in "Destination" or "Amplify"; without one the row is skipped. A "Comment on" row addressed to a LinkedIn or Reddit location becomes a comment under `post` as well as under `comment`; X has no comment flow. ## Examples ```bash blowhorn post --check-queue blowhorn post --dry-run --platform hn blowhorn post --profile all --platform x --pace slow blowhorn post --platform github --amplify https://github.com/org/repo/issues/1 blowhorn post --platform slack --profile your-profile --target "#general" --message "Ship day is here." ``` ## Related - [Preview and publish queued posts](/docs/how-to/publish/post-content/) - the run in practice. - [Content queue columns and row states](/docs/reference/content-queue/) - what the rows hold. - [Platforms](/docs/reference/platforms/) - per-platform actions and limits. - [CLI reference](/docs/reference/cli/) - every command and flag. - [How Blowhorn avoids repeat posts](/docs/explanation/never-twice/) - one click per run. - [You're in control](/docs/explanation/youre-in-control/) - dry run, pace and the built-in limits. -------------------------------------------------------------------------------- title: "Profile reference" url: https://blowhorn.ai/docs/reference/profiles/index.md description: What a profile is: its directory, its store row, its keys per platform, and the Chrome column. -------------------------------------------------------------------------------- # Profile reference A profile is one identity Blowhorn posts as: a directory beside a store row. The directory holds `config.yaml` and `data/`. The store row holds the organization, the credential keys that moved there, and the Chrome mapping. This page describes; the steps live in the how-to guides under Related. ## Creating a profile `blowhorn profile set --create` (or `--register`) creates both halves, row first. It resolves the profile's email to one live Layer5 Cloud user: no match or several matches refuses by name, and only `--subject ` gets past several. It refuses a silent rename and the two slug conflicts, writes the row, and only then scaffolds the directory. A store outage refuses before anything is written, the directory included. `blowhorn profile delete` retires the row first, then moves the directory. `blowhorn profile get ` prints one profile. `blowhorn profile status` reports every profile with its sessions. `blowhorn profile set KEY=VALUE` writes keys; unknown keys for `config.yaml` are refused by name. ## Credential Loading Eligibility is credential existence: a profile may post on a platform exactly when the secret for that platform exists. Handles, sessions in Chrome, and follower counts grant nothing. | Platform | Keys | Where the run reads them | |---|---|---| | LinkedIn | `LI_USERNAME`, `LI_PASSWORD` | `config.yaml` | | X | `X_USERNAME` (password beside it for automated sign-in) | `config.yaml` | | Reddit | `RDDT_USERNAME` (password beside it for automated sign-in) | `config.yaml` | | Hacker News | `HN_USERNAME`, `HN_PASSWORD` | The store | | Slack | `SLACK_USER_TOKEN`, `SLACK_BOT_TOKEN`, or a captured workspace session | The store, never `config.yaml` | | Bluesky | `BLUESKY_HANDLE`, `BLUESKY_APP_PASSWORD` | `config.yaml` | | GitHub | `GH_TOKEN` | The store | GitHub, Hacker News, and Slack are the store-credential platforms: a run reads their secrets from the store, and `profile set` writes their keys there. Every other platform still reads `config.yaml` until its module moves. `blowhorn profile auth --platform slack` captures a workspace session through the run's browser driver and lands it in the store; no Slack app install is needed. A profile you name that holds no credential for anything in scope stops the run with the reason naming it. `--profile all` skips such profiles quietly. ## The Chrome column The store row beside each profile carries the Chrome column: which real Chrome profile this Blowhorn profile acts in on this machine, keyed by hostname. `blowhorn chrome map` writes it; `blowhorn chrome unmap` clears it. A browser run with no mapping for its profile refuses before anything opens, and never falls back to another profile. The mapping is identity, never permission: it says where the run acts, not what it may do. ## Related - [Map Blowhorn profiles to Chrome profiles](/docs/how-to/set-up/map-chrome-profiles/) - write the Chrome column. - [Platforms](/docs/reference/platforms/) - what each platform acts with. - [Chrome reference](/docs/reference/chrome/) - the mapping commands and their shapes. - [Who can post as whom](/docs/explanation/eligibility/) - why a credential is permission. -------------------------------------------------------------------------------- title: "Configuration reference" url: https://blowhorn.ai/docs/reference/configuration/index.md description: Precedence, every `.blowhorn.yaml` option, environment variables, and output logging. -------------------------------------------------------------------------------- # Configuration reference How Blowhorn resolves what a run uses, and where its output goes. `blowhorn/config_schema.py` is the one list of options; `blowhorn config show` prints each with its stored, environment, effective, and source values. `blowhorn config set` edits the file without rewriting it: comments, order, and quoting survive. Precedence, highest first: the CLI flag, the environment variable, the `.blowhorn.yaml` default, then the built-in. The file lives beside the current directory by default (`./.blowhorn.yaml`); `--config` points at another one. ## Every option `.blowhorn.yaml` supports | Default key | Environment | Built-in | Means | |---|---|---|---| | `defaults.profile` | `BLOWHORN_PROFILE` | `all` | The profile a run uses when `--profile` is not given | | `defaults.platform` | `BLOWHORN_PLATFORM` | `all` | The platform a run targets when `--platform` is not given | | `defaults.exclude` | `BLOWHORN_EXCLUDE` | none | Profiles left out of `--profile all` | | `defaults.headless` | `BLOWHORN_HEADLESS` | off | Unattended background window | | `defaults.headed` | `BLOWHORN_HEADED` | off | Visible window in front | | `defaults.dry_run` | `BLOWHORN_DRY_RUN` | off | Simulate every run | | `defaults.pace` | `BLOWHORN_PACE` | `normal` | `fast`, `normal`, `slow`, or a number; commands that never pause accept and ignore it | | `defaults.no_mouse_move` | `BLOWHORN_NO_MOUSE_MOVE` | off | Skip the pointer simulation before clicks | | `defaults.logs.directory` | `BLOWHORN_LOG_DIR` | `logs/` in the checkout | Where daily logs go | | `defaults.logs.retention_days` | `BLOWHORN_LOG_RETENTION_DAYS` | 30 | Days of daily logs kept; 0 keeps all | | `defaults.chrome.launch_mode` | `BLOWHORN_CHROME_LAUNCH` | `extension` | How a run reaches Chrome | | `defaults.chrome.automation_data_dir` | `BLOWHORN_CHROME_AUTOMATION_DATA_DIR` | Blowhorn's own directory | Where a launched Chrome keeps its session | | `defaults.chrome.data_dir` | `BLOWHORN_CHROME_DATA_DIR` | Your real Chrome directory | Where the installed Chrome is read from | | `defaults.slack.auth_mode` | `BLOWHORN_SLACK_AUTH` | your captured session first | Which Slack credential may send: session, user-token, bot-token, or a list tried in order | | (flag only) | `BLOWHORN_TEE_STDOUT` | off | Flush the stdout copy per write for a supervising wrapper | | `defaults.store.backend` | `BLOWHORN_STORE_BACKEND` | `postgres` | The one store backend; anything else is refused by name | | `defaults.store.organization` | `BLOWHORN_STORE_ORGANIZATION` | none; while unset, nothing runs | The organization every row is scoped to | | `defaults.store.config` | `BLOWHORN_STORE_CONFIG` | a private file outside the checkout | The private document holding the connection | `headed` and `headless` are one choice with two spellings: the higher layer wins, so a `--headless` flag beats a `headed` default from the file, and `--headed` wins a tie. The settings screen reports each on its own; a flagless run resolves them together so exactly one survives. A version-1 file that still says `persistent` reads as `extension` on every run; only a validated `config set` of another key moves the file to version 2. The ten `store.*` connection options are private: they stay out of `config show`. ## Output logging The daily log is a copy; the terminal streams are never dropped, piped or not. The directory resolves as `--log-dir`, then `BLOWHORN_LOG_DIR`, then `defaults.logs.directory`, then the checkout's `logs/`. A default that cannot be used fails silently; a directory you named reports why. Only files the retention pass recognizes by name are ever deleted: daily logs past their days (30 by default), and the three dateless capture files rolling past their size bound. Per-job logs, the ledger, and the scheduler heartbeat live beneath the same directory and are never pruned by it. `--tee-stdout` is about liveness, not logging: it flushes the stdout copy per write, and with no usable log directory it line-buffers the real stdout instead. It is read before command parsing, so it is not a per-command flag. ## Related - [Change your defaults](/docs/how-to/settings/defaults/) - change the defaults. - [Choose how Blowhorn reaches Chrome](/docs/how-to/settings/launch-mode/) - the launch-mode option in practice. - [The Blowhorn app, screen by screen](/docs/reference/use-the-desktop-app/) - the Settings screen. - [Messages and exit codes](/docs/reference/messages/) - what a bad value reports. - [How scheduling works](/docs/explanation/scheduling/) - what the heartbeat and per-job logs are for. - [You're in control](/docs/explanation/youre-in-control/) - which options are safety dials. - [CLI reference](/docs/reference/cli/) - every command and flag. -------------------------------------------------------------------------------- title: "Messages and exit codes" url: https://blowhorn.ai/docs/reference/messages/index.md description: Every refusal sentence keyed by its first words, with the cause, the fix, and the exit code. -------------------------------------------------------------------------------- # Messages and exit codes What a failed run prints, why, and what fixes it. Entries are keyed by the sentence's first words. Exits: 0 ran clean; 1 the run failed; 2 the invocation was wrong; 3 the store was unreachable. A run cancelled between passes exits 75. ## The store A run that needs the store and cannot reach it prints the cause's own wording, then `error: store unavailable`, and exits 3. There is no traceback. Check the connection and run again; nothing was published and nothing was marked done. Any other store error prints its own sentence and exits 1. A ledger write the store refuses names the person and stops the run before its next platform action, so the run halts rather than acting without recording. ## Eligibility and plans A profile you named that is eligible for nothing in scope stops the run with the reason and exits 1. `--profile all` skips such profiles quietly instead. A run the organization's plan does not cover exits 1; an unknown `BLOWHORN_ENTITLEMENT` value is refused by name rather than read as off. Dry runs are never refused: they publish nothing. See [Plans and limits](/docs/reference/plans/). ## Chrome and the extension `another blowhorn run holds the Chrome extension bridge`: two runs want the extension at once. Wait for the other run or stop it; this run did nothing. `chrome refused the connection (Allow was declined ...)`: the attach handshake did not complete. Keep a Chrome window open, approve the Allow dialog, and run again. In a scheduled pass the Allow window is short; prefer the extension default. `extension_outdated` with two versions: the installed extension is older than the run needs. Reload or update the extension and run again. A profile with no Chrome mapping on this machine, a mapped directory Chrome no longer has, or an identity that drifted since binding is refused by name before anything opens, with the command that re-maps it. There is never a fallback to another profile. ## Sign-in `X session expired ... redirected to login page` and `Reddit session expired ... redirected to login page`: sign in again in that Chrome profile, by hand, then run again. `X rejected the credentials for @...`: the stored username or password is wrong; fix it and run again. A CAPTCHA or second-factor challenge always hands back to you; an unattended run never waits. For X, see [Fix a sign-in that stopped working](/docs/how-to/troubleshoot/sign-in-problems/). ## The queue and the schedule `title is ... characters; Hacker News allows 80 ...`: shorten the title to 80 characters; the row failed rather than truncating because a submission cannot be edited after it lands. `Rate limited: HN said ...`: Hacker News throttled the run; the row failed, wait before retrying, and never retry to beat the limiter. `Unsupported Recurrence '...'` and `Invalid recurrence interval '...'`: the schedule row's word is not one Blowhorn knows; use `once`, `hourly`, `daily`, `weekly`, `monthly`, or `every:N`. See [Job options and schedules](/docs/reference/jobs/). A row `clicked, outcome not read` is not an error: the sending control was clicked and no confirmation was read back. Check the platform, then clear "Date Promoted" to try again or leave it. See [Content queue columns and row states](/docs/reference/content-queue/). ## Related - [Content queue columns and row states](/docs/reference/content-queue/) - row states including unread. - [Job options and schedules](/docs/reference/jobs/) - recurrence words. - [Plans and limits](/docs/reference/plans/) - plan refusals. - [Fix a sign-in that stopped working](/docs/how-to/troubleshoot/sign-in-problems/) - expired sessions. - [How Blowhorn paces itself](/docs/explanation/reliability-and-anti-bot-design/) - why a run stops instead of retrying. -------------------------------------------------------------------------------- title: "Chrome extension permissions" url: https://blowhorn.ai/docs/reference/extension-permissions/index.md description: Each permission the Blowhorn extension asks for, the sites it runs on, and why. -------------------------------------------------------------------------------- # Chrome extension permissions What the Blowhorn extension may do, and why each capability exists. The extension acts only on requests from the Blowhorn app on your Mac; it makes no requests of its own to any server, sells no data, and injects no ads. Its single purpose is carrying out the posting, amplifying, and session-check actions you queue, inside the Chrome profile you already use. It adds no toolbar button by design: it is a companion, not a tool you click. ## Sites The extension runs content only on the sites Blowhorn acts on, and reads a workspace session only from its sign-in pages: - `x.com`, `twitter.com` - `linkedin.com`, `www.linkedin.com` - `reddit.com`, `www.reddit.com` - `news.ycombinator.com` - `mentorship.lfx.linuxfoundation.org`, `sso.linuxfoundation.org` - `*.slack.com` No other site is touched. You remain responsible for each destination's terms and acceptable-use rules; Blowhorn gives you the controls, not permission to ignore them. ## Permissions | Permission | Why | |---|---| | `nativeMessaging` | Talks to the Blowhorn app on this Mac. Every action is a request from that app | | `alarms` | A one-minute alarm re-opens the connection to the app after Chrome suspends the worker, so a scheduled run finds the extension ready | | `tabs` | Finds the tab already open on a supported site, or opens one, so the action runs where you are signed in. Only the sites above are queried | | `cookies` | Reads cookies on the supported sites only when the app asks, to reuse a sign-in you already have. A cookie goes to the local app; the one the app keeps beyond your Mac is your Slack workspace session, stored in your organization's store as that profile's Slack credential | | `debugger` | Delivers your queued clicks and keystrokes as real input, watches the page's own requests to tell when an action finished, and takes a screenshot when an action needs a person to check it. Attached only to a tab the app is driving, released when the action ends | | `downloads` | Catches the one export you asked for (an analytics or connections export) for the app, then cancels and clears it. A finished file stays where it is; the extension never starts a download on its own | ## Related - [Chrome reference](/docs/reference/chrome/) - install, status, and connection checks. - [Platforms](/docs/reference/platforms/) - the sites above, per platform. - [Where your data lives](/docs/explanation/your-data/) - what the extension reads and where it goes. - [Install the Chrome extension](/docs/how-to/set-up/install-the-chrome-extension/) - install it. -------------------------------------------------------------------------------- title: "Plans and limits" url: https://blowhorn.ai/docs/reference/plans/index.md description: Blowhorn is free during early access, with no tiers and no per-action charges. -------------------------------------------------------------------------------- # Plans and limits Blowhorn is free during early access. There are no tiers, no per-action charges, and no limits on the platforms or profiles you run. Every feature is all-you-can-eat while early access lasts. Future packages will be priced by which platforms you automate and how many profiles you run: never a charge per action, and never a surprise bill. Prices are not confirmed yet, so this page names no numbers; when tiers exist, they will be listed here. What the plan counts, when plans arrive, is profiles and platforms: how many identities post, and where. The app, the engine, and dry runs stay free in every future: a dry run publishes nothing and is never refused. Plan enforcement is off unless you set the environment variable `BLOWHORN_ENTITLEMENT=required` on your Mac; with it off, nothing is ever refused for being offline. With it on, if your Mac loses its connection to Layer5 Cloud, publishing keeps working for 72 hours on the plan already held, then refuses until the connection returns. See [Plans, offline use and lapses](/docs/explanation/plans/). ## Related - [Get help](/docs/reference/support/) - questions about access and billing. - [System requirements](/docs/reference/requirements/) - what you need before anything else. - [Plans, offline use and lapses](/docs/explanation/plans/) - why plans count what they count. - [How Blowhorn works](/docs/explanation/how-it-works/) - where the plan is checked. -------------------------------------------------------------------------------- title: "Get help" url: https://blowhorn.ai/docs/reference/support/index.md description: Where to look first, how to report a problem, and how to report a security issue. -------------------------------------------------------------------------------- # Get help Start with the self-serve pages: the [troubleshooting index](/docs/how-to/troubleshoot/troubleshooting/) for something broken, and [Messages and exit codes](/docs/reference/messages/) for a sentence a run printed. Most failures are a drifted mapping, an expired session, or an unreachable store, and each page names its fix. To report a problem, file an issue on the public site repository, `layer5io/blowhorn-site`, with what you ran, what it printed, and what you expected. Include the exit code and the first words of the error; leave out credentials, session files, and anything from your Chrome profile directories. For a security problem, do not file a public issue: email [security@blowhorn.ai](mailto:security@blowhorn.ai) instead, as the site repository's security policy describes. Questions about access, organizations, and billing go through your Layer5 Cloud organization. See [Plans and limits](/docs/reference/plans/) for what early access covers. ## Related - [Troubleshooting](/docs/how-to/troubleshoot/troubleshooting/) - the troubleshooting index. - [Messages and exit codes](/docs/reference/messages/) - every refusal sentence, including store outages. - [Plans and limits](/docs/reference/plans/) - access and billing questions. - [Where your data lives](/docs/explanation/your-data/) - what never belongs in a report. -------------------------------------------------------------------------------- title: "Chrome reference" url: https://blowhorn.ai/docs/reference/chrome/index.md description: The mapping commands, the two launch modes, the extension default, and Linux differences. -------------------------------------------------------------------------------- # Chrome reference How Blowhorn finds and reaches your Google Chrome. Every browser run reads the profile mapping first and refuses a profile without one, before anything opens. This page describes; the steps live in the how-to guides under Related. ## What it reads, and what it never reads `blowhorn chrome` reads two files under Chrome's data directory, `Local State` and `DevToolsActivePort`, and nothing else: never cookies, saved passwords, storage, or the keychain. It writes nothing there and opens no browser, DevTools, or network connection. Whether Chrome is running is one bounded process lookup. ## The mapping Each Blowhorn profile maps to one real Chrome profile on this machine, kept in the store and keyed by hostname. The mapping does not follow you to another machine. `blowhorn chrome profiles` lists every profile Chrome knows, with the directory name and the display identity. Chrome does not have to be running. `blowhorn chrome suggest` prints a proposed mapping for every Blowhorn profile the store knows. `blowhorn chrome map ` writes one mapping; it checks the directory exists in Chrome and refuses a name Chrome no longer has. `blowhorn chrome unmap ` removes one mapping. A mapping is identity, never permission: it says where the run acts, not what it may do, and there is never a fallback to another profile. ## Launch modes How the run reaches Chrome once the mapping holds: - The extension default: the run drives your signed-in Chrome through the Blowhorn extension. No second browser, no Allow dialog in the happy path. - Attach (`cdp`): the run attaches to the Chrome you have open over the debugging protocol. Needs a window open and the Allow dialog approved; a scheduled pass gets a short Allow window. - Launch (`persistent`): the run launches Blowhorn's own Chrome from the automation directory, minimized in the background. Chrome is never launched headless and never replaced with bundled Chromium: a headless run would announce itself to every site. ### The extension default The extension driver starts no Chrome and launches no Playwright. One mapped profile holds the bridge per run; a second run that wants it while it is held stops with `another blowhorn run holds the Chrome extension bridge` and does nothing. A hello that omits the run's version, or an installed extension older than the run needs, stops as `extension_outdated` naming both versions. Anything the extension cannot do is refused by name. Under the extension default, `blowhorn profile auth` is a sign-in by hand: it opens the platform's sign-in page once in your mapped Chrome profile, types nothing, and waits a bounded time for you to finish signing in, then confirms the session once. ## `blowhorn chrome status` Whether Google Chrome is installed, whether it is running, and whether its debugging switch is on. On macOS the installed path is `/Applications/Google Chrome.app`; on Linux the `PATH` names and `/opt/google/chrome/chrome`. A missing Chrome stops the run with `ChromeNotInstalled` before any driver starts. ## Flags `--profile` and `--chrome-profile` pin one Chrome profile for one run. `--chrome-launch` pins the launch mode for one run. `--pace`, `--headless`, and `--dry-run` behave as they do everywhere. ## Linux The `extension` and `persistent` drivers run on Linux as on macOS; the `cdp` attach driver is macOS only. Chrome is found by its `PATH` names and `/opt/google/chrome/chrome`. Everything else on this page reads the same. ## A dedicated attach Chrome (no Allow dialog) `blowhorn chrome attach-chrome` starts a second Chrome on a tree of its own (`~/.blowhorn/attach-chrome`), open to remote debugging, so `--chrome-launch cdp` can attach unattended with no Allow dialog. Chrome 136 and later ignore the debugging switch on the default data directory, which is why this is never your daily Chrome. `status` reads that tree's two files and opens nothing; `start` launches Chrome detached and prints the steps that finish the setup. It writes no configuration: pointing `chrome.data_dir` at the tree stays your explicit next step. A recorded port is evidence a Chrome is up, not proof a connect would succeed. ## `blowhorn chrome extension-install` Writes the native-messaging host file Chrome launches to reach the local Blowhorn app. Re-run it after moving the checkout or reinstalling the app; without a current host file the extension cannot talk to the engine. ## What a site sees: the user agent by driver Your own user agent, on every driver. The extension and attach drivers run inside your installed Chrome, so sites see the same agent string your ordinary browsing sends. The launch driver starts that same installed Chrome, never bundled Chromium, which would announce itself as HeadlessChrome. A `--headless` request becomes the minimized background window for exactly this reason: headless Chrome sends `HeadlessChrome` in every request while its hints still claim Google Chrome. ## `blowhorn chrome extension-status` Whether the extension is installed, which version answers, and whether it says connected. A version older than the run needs is the `extension_outdated` refusal above. ## One run per profile and platform A profile's session on a browser platform (LinkedIn, X, Reddit, Hacker News) belongs to one Blowhorn process at a time on a Mac. Before a run opens the session it takes a lock keyed by profile and platform, under every launch mode. A second run that wants the same profile on the same platform is refused with "profile `` on `` is in use by another blowhorn process; wait for it to finish or stop it": that profile is skipped, its rows stay queued, and a scheduled job leaves it for the next pass. Different profiles, or different platforms of one profile, can run side by side. See [You're in control](/docs/explanation/youre-in-control/). ## Related - [You're in control](/docs/explanation/youre-in-control/) - every safety control in one place. - [Install the Chrome extension](/docs/how-to/set-up/install-the-chrome-extension/) - install it. - [Map Blowhorn profiles to Chrome profiles](/docs/how-to/set-up/map-chrome-profiles/) - write the mapping. - [Choose how Blowhorn reaches Chrome](/docs/how-to/settings/launch-mode/) - when to leave the default. - [Chrome extension permissions](/docs/reference/extension-permissions/) - what the extension may do. - [Messages and exit codes](/docs/reference/messages/) - every refusal sentence. - [Why Blowhorn uses your own Chrome](/docs/explanation/your-own-chrome/) - why your Chrome and not a bundled one. -------------------------------------------------------------------------------- title: "How Blowhorn works" url: https://blowhorn.ai/docs/explanation/how-it-works/index.md description: The app, the engine, the extension, the background service, and your organization's store. -------------------------------------------------------------------------------- # How Blowhorn works Blowhorn is a social media console that takes one message and broadcasts, reposts and amplifies it across every profile and platform your community runs, on autopilot. Five parts do it together. The app is what you see: the menu-bar popover and the console. It shows the queue, the schedule, the runs, and the settings, and it installs updates. It never posts on its own. The engine is what acts: `blowhorn ` run from the app, the background service, or your terminal. Every run resolves its profiles, reads the queue, reaches Chrome or an API, and writes the ledger. The same engine answers the app's screens, so the app can never offer what the engine does not know. The extension is how the engine reaches your signed-in Chrome. It carries out the clicks and keystrokes the engine queues, inside the Chrome profile you already use, on the sites Blowhorn acts on. It makes no requests of its own. The background service is what runs the schedule while the app is closed: one pass every ten minutes claiming the rows that are due. Quitting the app never pauses it; pausing is explicit, per machine, per organization, or per row. Your organization's store is what the parts share: the content rows, the schedule, the profiles and their credentials, the ledgers, and the run history. Your Mac holds the app, the engine, and your Chrome sessions; the store holds everything the Macs share. A post travels this path: a row in the queue, approved and due; a run acting as the row's profile; the platform reached through your Chrome or its API; the outcome written back to the row and the ledger. Any step the run cannot confirm stops the run instead of guessing: the row reads clicked, outcome not read, and waits for you. ## Related - [The Blowhorn app, screen by screen](/docs/reference/use-the-desktop-app/) - what you see. - [Platforms](/docs/reference/platforms/) - what each platform leg does. - [Why Blowhorn uses your own Chrome](/docs/explanation/your-own-chrome/) - the extension leg. - [How scheduling works](/docs/explanation/scheduling/) - the service leg. - [Preview and publish queued posts](/docs/how-to/publish/post-content/) - watch the engine work. - [You're in control](/docs/explanation/youre-in-control/) - what Blowhorn will and won't do on its own. -------------------------------------------------------------------------------- title: "Where your data lives" url: https://blowhorn.ai/docs/explanation/your-data/index.md description: What stays on your Mac, what lives in your organization's store, and what the extension reads. -------------------------------------------------------------------------------- # Where your data lives Three places hold Blowhorn's data, and each holds a different kind. Nothing here is a step; for the sharing behind it see [How Blowhorn works](/docs/explanation/how-it-works/). On your Mac: the app, the engine checkout, and your Chrome sessions. Browser sign-ins live in your Chrome profiles, where you made them; Blowhorn reads them there and copies none of them elsewhere, with one exception: a Slack workspace session you capture is kept in your organization's store, below. The daily logs, the run ledger copy, and the scheduler heartbeat live under the log directory. The store connection file lives outside the checkout at mode 0600. A plan attestation, once fetched, is kept in your keychain for the offline grace. In your organization's store: the content rows, the schedule, the profiles, the ledgers, and the run history. The store-credential platforms keep their secrets there too: GitHub tokens, Hacker News passwords, and Slack tokens and captured sessions. Every row is scoped to the organization, and a machine without the organization configured runs nothing. On the platforms: whatever Blowhorn published, amplified, or followed as you, under your name, subject to each destination's own retention. A published post is the platform's copy; Blowhorn keeps the link and the outcome, not the content's master. The extension queries tabs only on the supported sites and reads cookies there only when the app asks, handing both to the Blowhorn app on your Mac. The cookies and sign-ins it reads stay on your Mac, with one exception: today that is your Slack workspace session, which the app keeps in your organization's store as that profile's Slack credential. What a run records, such as a new post's link and its outcome, goes to your organization's store as described above. The extension makes no requests of its own to any server, sells nothing, and injects no ads. The [privacy page at blowhorn.ai](https://blowhorn.ai/legal/privacy/) states the same promises; this page follows it, and the privacy page wins on any difference. What never belongs in a report, an issue, or a chat message: credentials, session files, the private connection file, and anything from your Chrome profile directories. A refusal sentence and an exit code are enough to diagnose with. ## Related - [Get help](/docs/reference/support/) - report a problem without secrets. - [The Blowhorn app, screen by screen](/docs/reference/use-the-desktop-app/) - the log directory listing. - [Profile reference](/docs/reference/profiles/) - which secrets live where. - [Chrome extension permissions](/docs/reference/extension-permissions/) - what the extension may read. -------------------------------------------------------------------------------- title: "Who can post as whom" url: https://blowhorn.ai/docs/explanation/eligibility/index.md description: A credential is permission, a handle is not; a Chrome mapping is identity, not permission. -------------------------------------------------------------------------------- # Who can post as whom Blowhorn decides who may post as whom from one fact: whether the profile holds the platform's credential. Nothing else grants it. A handle is not authorization. A username in a roster, a display name in Chrome, or a follower count says who someone is, never what Blowhorn may do as them. The roster notices that explain a thin selection are diagnostics, not gates: nothing in the run treats a listed handle as consent to post. What each platform's credential is, and where the run reads it, is [Profile reference](/docs/reference/profiles/). A Chrome session is not authorization either. Being signed in to a site in a Chrome profile lets the run act there, but the run still checks the profile's credential first. A named profile with no credential for anything in scope stops the run with the reason; `--profile all` skips such profiles quietly. A Chrome mapping is identity, not permission. It says which real Chrome profile the run acts in on this machine, and its refusal checks run before anything launches or attaches. It never grants, and no fallback to another profile or folder ever applies when it does not hold. One profile is one person's posting identity, created row-first in the store and scaffolded beside it. Creating it resolves the person's email to one Layer5 Cloud user and refuses none-or-several by name, because two people sharing a posting identity could never be told apart in the ledger. ## Related - [Profile reference](/docs/reference/profiles/) - the credential per platform. - [Platforms](/docs/reference/platforms/) - what each credential unlocks. - [Map Blowhorn profiles to Chrome profiles](/docs/how-to/set-up/map-chrome-profiles/) - the mapping half. - [Content queue columns and row states](/docs/reference/content-queue/) - the refused blank profile. -------------------------------------------------------------------------------- title: "Why Blowhorn uses your own Chrome" url: https://blowhorn.ai/docs/explanation/your-own-chrome/index.md description: Signed-in Chrome through the extension, APIs where they exist, and never headless. -------------------------------------------------------------------------------- # Why Blowhorn uses your own Chrome Platforms trust people, not programs. A post that arrives from a browser you signed in to, carrying your session, your history, and your ordinary behavior, reads as you. A post from a datacenter browser that never slept reads as automation. Blowhorn therefore acts in the Chrome you already use, and reaches for an API only where the platform offers one it can honestly use. The extension drives your signed-in Chrome: the tabs you have open, on the sites Blowhorn acts on, with the sessions you made by hand. There is no second browser to sign in to, no session to export, and nothing that rots when a cookie expires except the session itself, which you renew by signing in again. Scheduled passes drive the same Chrome through the same extension; an unattended run never waits for a person at a sign-in or a challenge, it stops and says so. Slack, Bluesky, and GitHub post through their APIs above the browser, because those calls carry a token, not a pretense. Everything else drives Chrome, because anything else would be a poorer copy of you. Chrome is never launched headless, and never swapped for bundled Chromium. A headless Chrome announces itself in every request while its hints still claim Google Chrome, and the bundled build announces itself in both. Either would mark the run as automation to every site it touches. An unattended run takes the minimized background window instead: invisible, but honest about what it is. What a site sees is therefore your own user agent, your own session, and input delivered as real keystrokes and clicks with human pauses between them. That honesty is also the limit: a CAPTCHA or a second-factor challenge hands back to you, always, because passing it for you would stop being you. ## Related - [Chrome reference](/docs/reference/chrome/) - launch modes and the extension default. - [Chrome extension permissions](/docs/reference/extension-permissions/) - what the extension may do. - [Platforms](/docs/reference/platforms/) - which platforms drive Chrome and which use APIs. - [How Blowhorn paces itself](/docs/explanation/reliability-and-anti-bot-design/) - human pauses between actions. - [Map Blowhorn profiles to Chrome profiles](/docs/how-to/set-up/map-chrome-profiles/) - the Chrome half of setup. - [You're in control](/docs/explanation/youre-in-control/) - one automation per profile and platform. - [Install the Chrome extension](/docs/how-to/set-up/install-the-chrome-extension/) - put the extension in your Chrome. -------------------------------------------------------------------------------- title: "How Blowhorn paces itself" url: https://blowhorn.ai/docs/explanation/reliability-and-anti-bot-design/index.md description: Human typing and pauses, per-run pace, and why a failed post is not retried. -------------------------------------------------------------------------------- # How Blowhorn paces itself Blowhorn acts at human speed, and stops at the first thing it cannot confirm. Both are deliberate: speed and retries are what get an account flagged. {{< major >}}Nobody can promise you won't get flagged. I can promise I won't rush, won't repeat myself and won't touch your CAPTCHA.{{< /major >}} Typing arrives keystroke by keystroke with a computed delay between keys, and actions are separated by random pauses of a few seconds, scaled by the run's pace. `--pace` sets it: `fast`, `normal`, `slow`, or a numeric multiplier, resolved from the flag, then the environment, then the defaults file. A long unattended run earns the slow pace; a failure never earns a retry click. Public actions are never repeated on a guess. The sending control is clicked once per run; "no confirmation, the dialog still reads open" reports the row not sent, never sends it again, and never reports it sent either. What the platform showed after the click is a reading, not a fact, so the row reads clicked, outcome not read, and waits for you. See [How Blowhorn avoids repeat posts](/docs/explanation/never-twice/). A typing failure fails only its own row. The dispatcher has no per-row rescue around the loop, so a lost typing target would abandon every remaining queued row for that profile; every call site therefore contains the failure to its own unit of work instead. Within a run, Blowhorn doesn't retry a failed post; it moves on to the next item. If a post goes out but Blowhorn can't record it, it stops that profile's queue rather than risk posting twice. Scheduled jobs only re-run a failure if you turn on retries, and they're off by default. ## Hacker News is the least forgiving platform A Hacker News submission cannot be deleted, and coordinated-looking promotion gets the whole domain banned. Blowhorn treats it accordingly: at most 2 link submissions per profile per UTC day, titles held to 80 characters with the row failing rather than truncating, no voting ever, and a comment row that can only ever be a comment. A rate-limited run fails the row and waits; it never retries to beat the limiter. ## Related - [Preview and publish queued posts](/docs/how-to/publish/post-content/) - pace your run. - [Post to Hacker News](/docs/how-to/publish/post-to-hacker-news/) - the strictest rules, worked. - [Platforms](/docs/reference/platforms/) - per-platform limits. - [How Blowhorn avoids repeat posts](/docs/explanation/never-twice/) - one click per run. - [Change your defaults](/docs/how-to/settings/defaults/) - the pace default. - [You're in control](/docs/explanation/youre-in-control/) - every safety control in one place. -------------------------------------------------------------------------------- title: "How Blowhorn avoids repeat posts" url: https://blowhorn.ai/docs/explanation/never-twice/index.md description: One click per run, preview first, and what clicked-outcome-not-read means. -------------------------------------------------------------------------------- # How Blowhorn avoids repeat posts A public action cannot be taken back, and a guess repeated is spam. Blowhorn therefore clicks once per run and records exactly what it read: posted where it read a confirmation, clicked where it did not, and never "not sent" once the control was clicked. {{< major >}}One click per action. If I'm not sure it landed, I report it and wait for you.{{< /major >}} Preview first. `--check-queue` lists what a run would do, and `--dry-run` walks it end to end, without publishing, following, or writing anything. A Hacker News row gets both before its first real run, because its landing is permanent. One click per run. The sending control is clicked a single time; there is no retry click. "No confirmation, the dialog still reads open" reports the row not sent and stops. The page a flow started on is never the new post's link: a reader that starts on a post refuses it at every rung and records no link rather than the wrong one. "Clicked, outcome not read" is the honest middle. Once the control was clicked, the absence of a confirmation is no reading of "nothing went out", so the run records the click, counts nothing, writes no promotion link, and leaves the row out of the pending set without marking it done. A person left in that state is held the same way: recorded, kept off the drain, and listed at the start of every run until settled. Neither a retry nor a drop is a run's to decide. A lost write-back is recorded the same way. A post whose store write-back fails is not spooled anywhere: the row stays pending in form but carries the unread stamp, LinkedIn amplify re-checks before acting, and any other platform risks a duplicate on re-run. Within a run, Blowhorn doesn't retry a failed post; it moves on to the next item. If a post goes out but Blowhorn can't record it, it stops that profile's queue rather than risk posting twice. Scheduled jobs only re-run a failure if you turn on retries, and they're off by default. ## Related - [Preview and publish queued posts](/docs/how-to/publish/post-content/) - check before publishing. - [Content queue columns and row states](/docs/reference/content-queue/) - the unread state. - [Messages and exit codes](/docs/reference/messages/) - what the sentences mean. - [Post to Hacker News](/docs/how-to/publish/post-to-hacker-news/) - permanence first. - [How Blowhorn paces itself](/docs/explanation/reliability-and-anti-bot-design/) - why no retry click. - [You're in control](/docs/explanation/youre-in-control/) - every safety control in one place. -------------------------------------------------------------------------------- title: "How scheduling works" url: https://blowhorn.ai/docs/explanation/scheduling/index.md description: Ten-minute passes, one shared queue across Macs, leases, and the three pause scopes. -------------------------------------------------------------------------------- # How scheduling works Posting on autopilot means a pass that runs without you, a queue every Mac can share, and pauses that mean what they say. This page explains the arrangement; the words and commands are in [Job options and schedules](/docs/reference/jobs/). {{< major >}}One awake Mac, set up for the profile. That's all the schedule needs.{{< /major >}} One pass runs every ten minutes. It wakes, claims the rows that are due, runs them one at a time, writes the ledger, and sleeps again. A pass that finds nothing due costs nothing and changes nothing. The service owns the passes while the app is closed; your terminal owns the pass you trigger by hand, which never waits for the interval. Several Macs share one queue through leases, not locks. A claimed row says running with a lease timestamp, and the holder renews the lease as it works. A row whose lease lapsed is some dead machine's, and the next pass reclaims it. That is why a row's Status never reads as a lock, and why two Macs never work the same row: the claim statement reads the lease alone, atomically, and only one claimant wins it. Three pause scopes cover the three reasons to stop. This machine's file stops this Mac and nothing else, for maintenance on one machine. The organization-wide pause stops every Mac sharing the store, with a reason every banner repeats. One row's pause holds that row everywhere until resumed. A row is claimed only when none of the three says paused, and quitting the app touches none of them: pausing is always explicit, resuming always named. A row that did not run is never a mystery. `schedule why` reads the claim the pass would make: paused at some scope, not yet due, lease held by a living run, or eligible and waiting for the next pass. The popover says the same thing in fewer words: due now, running, paused, or needs you. ## Related - [Schedule a job](/docs/how-to/schedule/schedule-a-job/) - create and edit jobs. - [Pause and resume posting](/docs/how-to/schedule/pause/) - the pause scopes in practice. - [Find out why a job did not run](/docs/how-to/schedule/why-not-run/) - read `schedule why`. - [Job options and schedules](/docs/reference/jobs/) - recurrence words and scopes. - [What runs when the app is closed](/docs/explanation/distribution/) - the service behind the passes. - [The Blowhorn app, screen by screen](/docs/reference/use-the-desktop-app/) - the "jobs" screen. - [Collect and read your analytics](/docs/how-to/measure/analytics/) - read what the passes produced. - [You're in control](/docs/explanation/youre-in-control/) - pause, one job at a time, one Mac per job. -------------------------------------------------------------------------------- title: "What runs when the app is closed" url: https://blowhorn.ai/docs/explanation/distribution/index.md description: The background service, quitting, keep-running, and how updates reach a closed app. -------------------------------------------------------------------------------- # What runs when the app is closed Closing the app stops the window, not the work. The background service keeps running the schedule: one pass every ten minutes, claiming due rows and writing the ledger, with no window open and no person watching. Quitting pauses nothing and resumes nothing; the pause scopes keep exactly what they said. The service is per-user, registered from the app or with `blowhorn service install`. It restarts itself between passes, reports its heartbeat for `schedule status` and the popover, and writes per-job logs beneath the log directory. `service stop` waits for the pass in flight before stopping; `service restart` picks up a changed definition between passes; `service uninstall` stops and removes it whole. The service screen shows who registered it and whether it runs. Keep-running is the one setting that matters here. With it on, installing the app installs the service and quitting leaves it running; with it off, quitting ends scheduled posting until you open the app again. Login Items in System Settings decides whether the service returns after a reboot. If registration fails, the app says so once in a notification rather than failing silently every pass. Updates reach a closed app through the same service. The update card in the popover installs the new build and relaunches; when the app is closed, the next open shows the card instead. A release is one universal DMG per desktop tag, downloaded from the release page; the app installs it into Applications and relaunches when this machine can, and otherwise opens the release page for you. ## Related - [Keep posting when the app is closed](/docs/how-to/schedule/background-service/) - install and control the service. - [Job options and schedules](/docs/reference/jobs/) - what the passes run. - [How scheduling works](/docs/explanation/scheduling/) - passes, leases, and pauses. - [The Blowhorn app, screen by screen](/docs/reference/use-the-desktop-app/) - the service screen. -------------------------------------------------------------------------------- title: "Why flagged invitations are skipped" url: https://blowhorn.ai/docs/explanation/invitation-safety/index.md description: A flagged invitation is LinkedIn warning you, so Blowhorn walks away instead of confirming. -------------------------------------------------------------------------------- # Why flagged invitations are skipped Some invitations arrive with a warning, and the only safe answer to a warning is to walk away. When LinkedIn is unsure you know the person, accepting opens a "Take care when connecting" dialog offering to view the profile or accept anyway. That dialog is LinkedIn telling you it does not think you know this person. Clicking through it would defeat the only warning there is. `blowhorn accept` therefore dismisses the flagged invitation and skips it entirely, never confirming it, and carries on with the rest. Accepts and skips are reported separately, by name, so the skipped ones stay visible instead of vanishing into a count. Ignoring or declining is out of scope: the command only accepts, one at a time, with a randomized pause between each. This is the same rule the whole product follows: a public action is never taken on a guess. A flagged invitation is LinkedIn saying it is guessing about the person; acting anyway would spend your account's standing on that guess. ## Related - [Accept incoming invitations](/docs/how-to/grow/accept-invitations/) - the command in practice. - [How Blowhorn avoids repeat posts](/docs/explanation/never-twice/) - never act on a guess. - [Messages and exit codes](/docs/reference/messages/) - what the run reports. - [Platforms](/docs/reference/platforms/) - LinkedIn actions and limits. - [You're in control](/docs/explanation/youre-in-control/) - every safety control in one place. - [Withdraw invitations](/docs/how-to/grow/withdraw-invitations/) - the outgoing side. -------------------------------------------------------------------------------- title: "What the analytics numbers mean" url: https://blowhorn.ai/docs/explanation/analytics/index.md description: Measured versus trended counts, what each tile counts, and how fresh the numbers are. -------------------------------------------------------------------------------- # What the analytics numbers mean Analytics counts what the platforms showed, not what Blowhorn hoped. Every number is either measured, read off the platform during collection, or trended, appended to the profile's history so growth reads across runs. Nothing is estimated and no reach is promised. LinkedIn tiles count the page: total followers, new followers over 30 days, impressions, and reactions, each read from the page's own analytics export during collection. LinkedIn collects for one profile at a time; a run asked for all profiles says so and collects none for LinkedIn. X and Bluesky append one follower and following trend row each per run instead of tiles: two counts and a timestamp, growing the CSV history the report reads. `blowhorn analytics` collects into the repository-root `analytics/` directory and records the readings beside the exports, so a later collection reads as change, not as a fresh claim. `blowhorn report` summarizes the exports already sitting there into `analytics_report.html`; it reads local files only, so stale exports give a stale report and it says which exports it read. The analytics screen states each number's freshness in its header for the same reason: a count without a date is decoration. A dry run prints the tile shapes with no values, proving the shape of the read without touching a platform. Bluesky page-style tiles do not exist: Bluesky gives the trend row, and asking the tiles for more would be inventing numbers the platform never showed. ## Related - [Collect and read your analytics](/docs/how-to/measure/analytics/) - collect now, read the report. - [The Blowhorn app, screen by screen](/docs/reference/use-the-desktop-app/) - the analytics screen. - [Platforms](/docs/reference/platforms/) - which platforms collect what. - [Preview and publish queued posts](/docs/how-to/publish/post-content/) - dry runs print shapes too. -------------------------------------------------------------------------------- title: "Plans, offline use and lapses" url: https://blowhorn.ai/docs/explanation/plans/index.md description: Why plans count profiles and platforms, the 72-hour offline grace, and why dry runs stay free. -------------------------------------------------------------------------------- # Plans, offline use and lapses Plans count what costs: profiles and platforms. Each posting identity is someone's account acting, each platform a destination with its own rules and risk, so "how many identities, where" is the honest meter. Actions are never the meter: counting clicks would punish checking before publishing, which is the behavior the whole product exists to protect. During early access there is one plan and it is free, with no tiers and no limits. When tiers arrive they will differ in profiles and platforms, and dry runs will stay free in all of them: a dry run publishes nothing, and refusing it would charge for caution. See [Plans and limits](/docs/reference/plans/). The plan is checked at the moment of publishing, from an attestation your Layer5 Cloud organization issued after sign-in. Enforcement is off unless `BLOWHORN_ENTITLEMENT=required` is set in the environment on your Mac; it is a local switch, not an organization setting. With it on, a real run without a current attestation, or outside what the plan covers, refuses before any browser opens. The check reads no queue and no browser: the answer is already held, in the keychain, before the run starts. With enforcement off, being offline changes nothing and nothing is ever refused for it. With it on, offline use degrades by the clock, not by features: a held attestation keeps publishing for 72 hours while Cloud is unreachable; past that, publishing refuses until the connection returns. Nothing else changes offline: reading the queue, dry runs, and reports never needed the plan and never ask for it. A lapse therefore reads as "not entitled, reconnect", never as lost work: the rows wait, the ledger waits, and the next connected run carries on where the last one stopped. ## Related - [Plans and limits](/docs/reference/plans/) - what early access covers. - [Get help](/docs/reference/support/) - questions about access and billing. - [How Blowhorn avoids repeat posts](/docs/explanation/never-twice/) - why dry runs stay free. - [Where your data lives](/docs/explanation/your-data/) - the keychain half of the attestation. -------------------------------------------------------------------------------- title: "You're in control" url: https://blowhorn.ai/docs/explanation/youre-in-control/index.md description: Every worry in one place: getting banned, posting by mistake, posting twice, embarrassing yourself, and AI running away. The controls that stop each one, and where they live. -------------------------------------------------------------------------------- # You're in control Blowhorn is built for AI. Agents can drive it, queue for it and run it on autopilot. But the AI is never in control. You are. Even when you hand approval to an AI agent and let it run, the policies inside Blowhorn still decide what may happen, how fast, and when to stop. {{< major >}}You give the orders. I carry them out: one window, one profile, one platform at a time. Nobody types into two windows at once. Not in my unit.{{< /major >}} This page gathers every worry people bring to a tool that posts as them, and answers each with the control that handles it, where that control lives, and the page that documents it. ## Blowhorn limits itself to what a human could do Any given Blowhorn install only runs one automation on a given platform for a given profile at a time. You wouldn't expect a human to have two browser windows open, typing into both at once, so Blowhorn doesn't either. It acknowledges that limit and holds itself to it. Here is how that is enforced today: - **One profile, one platform, one process.** Before a run opens a LinkedIn, X, Reddit or Hacker News session for a profile, it takes a lock on that profile and platform on this Mac. A second run that wants the same profile on the same platform is refused with "profile ... is in use by another blowhorn process; wait for it to finish or stop it", and its rows stay in the queue for the next pass. The lock is the same whichever way Blowhorn reaches Chrome. - **Scheduled work runs one job at a time.** The background service runs one pass at a time, and a pass works through its jobs one after another, never side by side. - **One send per action.** The send control is clicked once. There is no retry click. Coming soon: an option to go stricter still, so that only one automation runs on a platform at a time even across different profiles. ## Will I get banned? Nobody can promise that for any tool, and we won't. Platforms change their rules, and automation can still be blocked or sessions broken, especially on X. What Blowhorn does is behave like a careful person and stop instead of guessing: - **It types and clicks like a person.** Typing arrives key by key with a computed delay between keys, actions are separated by random pauses, and clicks land at slightly varied points with natural hover and press timing. You set the pace. - **It caps itself.** At most 2 Hacker News link submissions per profile per day, and at most 400 X follows per profile a day and 15 in any 15 minutes. `--limit` caps how many people a follow or accept run takes on, and how many jobs a scheduler pass runs. - **It doesn't retry blindly.** Within a run, Blowhorn doesn't retry a failed post; it moves on to the next item. If a post goes out but Blowhorn can't record it, it stops that profile's queue rather than risk posting twice. Scheduled jobs only re-run a failure if you turn on retries, and they're off by default. - **It never solves a CAPTCHA or a 2FA prompt.** When a platform asks for one, Blowhorn stops and leaves it for you. In a visible window it can wait while you finish it; unattended, the run stops. - **It walks away from warnings.** A LinkedIn invitation flagged "Take care when connecting" is skipped, never confirmed. Read more in [How Blowhorn paces itself](/docs/explanation/reliability-and-anti-bot-design/) and [Why flagged invitations are skipped](/docs/explanation/invitation-safety/). ## Will it post something I didn't mean to? - **Nothing goes out unapproved.** A content row is published only when its `Approved?` column reads `yes` or `true`. Anything else is a draft. - **A row never widens.** A blank or unknown `Profile` is refused, never read as "everybody". A row with `Comment on` set can only ever be a comment. - **Rehearse first.** `--check-queue` lists what a run would do, and `--dry-run` walks it end to end without publishing, following or writing anything. Make dry run your default until you trust a setup. - **Stop it any time.** Pause this Mac, every Mac in your organization, or one scheduled job, each with a reason. A job already running finishes first, because killing a browser mid-post can leave a half-done action on the platform. ## Will it post twice? - **One click per run.** The send control is clicked a single time. - **"Clicked, outcome not read" is not a retry.** When Blowhorn clicked but could not read a confirmation, the row is recorded as clicked and held for you. It is neither sent again nor marked done. - **One Mac per job.** Every Mac sharing your organization's queue claims a scheduled job before it runs it, under a lease, so two Macs can't run the same job at once. - **Done is recorded per profile.** `Date Promoted` records when each profile published a row, so the next run skips it. Read more in [How Blowhorn avoids repeat posts](/docs/explanation/never-twice/). ## Will it embarrass me? - **No guessing at what landed.** A post's link is recorded only when Blowhorn read it back. It never records the page it started on as the new post. - **Hacker News is held to its own rules.** Titles over 80 characters fail the row rather than being cut, Blowhorn never votes there, and a rate-limited run waits rather than retrying. - **Your words, unchanged.** Blowhorn's actions on these platforms are scripted, not written by AI. It types the text in your queue. ## What if an AI agent is driving? An agent drives Blowhorn the same way you do: through the queue and the documented CLI. Every rule on this page applies no matter who started the run. The rate caps, the one-at-a-time lock, the single send click and the refusal to solve a CAPTCHA are built in, not flags an agent can drop. Give the agent dry run first, keep `Approved?` as your gate, and pause any scope the moment something looks off. Lifting a pause takes `blowhorn schedule resume`, or an explicit `--ignore-pause` on a single run. ## Every control, and where it lives | Control | What it does | Where it lives | Reference | |---|---|---|---| | Dry run | Walks a run without publishing, following or writing | `--dry-run` on every action command; `defaults.dry_run`; Settings in the app | [Configuration](/docs/reference/configuration/), [`blowhorn post`](/docs/reference/post/) | | Check the queue | Lists what a run would do, opening nothing | `blowhorn post --check-queue` | [`blowhorn post`](/docs/reference/post/) | | Approval gate | Only approved rows are published | The `Approved?` column of each content row | [Content queue](/docs/reference/content-queue/) | | Pause | Stops new claims on this Mac, every Mac, or one scheduled job | `blowhorn schedule pause`, with `--all` or `--row`; the pause controls in the app's popover and console | [Pause and resume posting](/docs/how-to/schedule/pause/) | | Stop the current pass | Ends the background service's pass at its next safe point; the row in progress returns to the queue | `blowhorn service cancel` | [CLI reference](/docs/reference/cli/) | | Pace | How fast Blowhorn types and how long it pauses between actions | `--pace fast`, `normal`, `slow` or a number; `defaults.pace`; Settings in the app | [Change your defaults](/docs/how-to/settings/defaults/) | | Pointer movement | Turns the pointer movement before clicks on or off | `defaults.no_mouse_move`; Settings in the app | [Configuration](/docs/reference/configuration/) | | Run size | Caps how many people a follow or accept run takes on, and how many jobs a pass runs | `--limit` on `follow`, `accept` and `schedule tick` | [CLI reference](/docs/reference/cli/) | | Excluded profiles | Leaves profiles out of `--profile all` | `--exclude`; `defaults.exclude`; Settings in the app | [Change your defaults](/docs/how-to/settings/defaults/) | | Rate caps | 2 Hacker News submissions per profile per day; 400 X follows per profile a day and 15 per 15 minutes | Built in | [Platforms](/docs/reference/platforms/) | | No blind retries | Within a run, Blowhorn doesn't retry a failed post; it moves on to the next item. If a post goes out but Blowhorn can't record it, it stops that profile's queue rather than risk posting twice. Scheduled jobs only re-run a failure if you turn on retries, and they're off by default. | Built in; scheduled-job retries are opt-in | [How Blowhorn paces itself](/docs/explanation/reliability-and-anti-bot-design/) | | One automation per platform per profile | A second run for the same profile and platform on this Mac is refused | Built in | [Chrome reference](/docs/reference/chrome/) | | One send per action | The send control is clicked once; an unread outcome is held for you | Built in | [How Blowhorn avoids repeat posts](/docs/explanation/never-twice/) | | One Mac per job | A scheduled job is claimed by one Mac at a time | Built in, through your organization's store | [How scheduling works](/docs/explanation/scheduling/) | | CAPTCHA and 2FA | Left for you; never solved | Built in | [Fix sign-in problems](/docs/how-to/troubleshoot/sign-in-problems/) | ## Your Risk Profile Think of your Risk Profile as the dials for how much risk you'll take and how fast you want things produced. Today those dials are separate settings: pace, pointer movement, run size, excluded profiles and dry run, alongside the caps Blowhorn always holds itself to. Set them cautious for a new account or an unattended run, and quicker for an established one you are watching. Coming soon: a Risk Profile setting that sets those dials together in one choice. ## Related - [How Blowhorn paces itself](/docs/explanation/reliability-and-anti-bot-design/) - human pacing and the no-retry rule. - [How Blowhorn avoids repeat posts](/docs/explanation/never-twice/) - one click per run. - [Why flagged invitations are skipped](/docs/explanation/invitation-safety/) - walking away from warnings. - [Pause and resume posting](/docs/how-to/schedule/pause/) - the three pause scopes. - [Change your defaults](/docs/how-to/settings/defaults/) - pace, exclusions and dry run. - [Configuration reference](/docs/reference/configuration/) - every option and where it is set. - [Platforms](/docs/reference/platforms/) - actions and limits per platform.