# Homebrew Formula Install Rankings & Change Monitor (`automa-flow/homebrew-formula-install-rankings-monitor`) Actor

Monitor Homebrew formula install-on-request rankings. Track rank, install share and new/exited formulae via the official Formulae JSON API.

- **URL**: https://apify.com/automa-flow/homebrew-formula-install-rankings-monitor.md
- **Developed by:** [Vadim Bezrukov](https://apify.com/automa-flow) (community)
- **Categories:** Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.60 / 1,000 formula results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Homebrew Formula Install Rankings & Change Monitor

See which Homebrew formulae people are installing on purpose, whether rank or
install share moved since your last successful check, and whether a watched
CLI tool entered or left the window. The Actor reads the official Formulae
JSON API. It does not scrape formulae.brew.sh HTML, download the 31 MiB
formula catalog, or collect cask analytics.

Paste the prefill below, run once, and you get the current top-20
install-on-request formulae plus one `query_status` row. Next week, switch the
same metric and period to `monitor` + `changesOnly` under a stable
`stateNamespace`.

This is **not** a claim that Homebrew Store search demand is measured, and it
is **not** a quality comparison with other Store scrapers. Homebrew does
**not** provide a native install-rank history; each row is an observation at
`scraped_at` for that analytics window.

### Choose your workflow

| Goal | Settings | What you get |
| --- | --- | --- |
| Export today's top-N on-request ranking | `snapshot` + `all` | One `formula_observation` per formula, all `BASELINE` |
| Re-check the same window next week | `monitor` + `changesOnly` | `NEW`, `RANK_UP`, `RANK_DOWN`, `EXITED`, `REAPPEARED` |
| Watch named formulae | `watchlist` | Observations only for those names; rank comes from the full window |

These are the three jobs the Actor is built for. Saved Console examples use the
same inputs; they stay unpublished until you publish each Task separately.

- **Export top formulae** for install-on-request, default 30-day window. First
  run writes `BASELINE` rows and one `query_status`.
- **Monitor rank changes** on the same query later. Keep `stateNamespace`
  stable. Later runs return `NEW`, `RANK_UP`, `RANK_DOWN`, `EXITED` or
  `REAPPEARED`. Daily install-count drift is not a change.
- **Watch named formulae** such as `gh`, `wget` and `ripgrep`. A name missing
  from a populated analytics window is delivered as an unranked `SUCCESS` row
  (`rank` null), not `NOT_FOUND`.

`BASELINE` means "this is the first successful ranking we stored for this
query". It does not mean the formula was created today.

### Quick start

```json
{
  "mode": "snapshot",
  "metric": "installOnRequest",
  "period": "30d",
  "topN": 20
}
```

On the 2026-09-11 probe, rank 1 for install-on-request 30d was `gh`. Then
schedule the same window:

```json
{
  "mode": "monitor",
  "outputMode": "changesOnly",
  "stateNamespace": "onrequest-30d",
  "metric": "installOnRequest",
  "period": "30d",
  "topN": 20
}
```

Keep `stateNamespace` stable for a given watch. Snapshot mode does not read or
write history. `snapshot` + `changesOnly` is rejected before any Homebrew
request. Changing `metric`, `period`, or (for top-N queries) `topN` starts a new
`query_key` and a new baseline. A named watchlist is keyed without `topN`, so
changing `topN` does not reset that watch.

A watchlist accepts `gh` or `https://formulae.brew.sh/formula/gh`. Other hosts
are `INVALID_INPUT` without a fetch. Rank is the analytics `number`, even when
it is greater than `topN`. A watchlist name missing from a populated JSON window
is delivered as an unranked `SUCCESS` observation (`rank` null), a candidate for
`EXITED` only after it was previously ranked, **not** `NOT_FOUND`. The query stays
`SUCCESS` when analytics returned a populated window; `NO_RESULTS` is only a
verified `items: []`. Optional per-formula metadata HTTP 404 is item-level
`NOT_FOUND` with ranking fields still filled from analytics.

### Formulae JSON API vs this Actor API

- **Formulae JSON API:** `GET https://formulae.brew.sh/api/analytics/install-on-request/{period}.json`
  and, if you turn metadata on, `GET https://formulae.brew.sh/api/formula/{name}.json`.
  Public JSON, no token.
- **Actor API:** Apify input, Dataset rows (`formula_observation`,
  `query_status`, `run_summary`), and optional last-good state in this Actor's
  key-value store.

Default metric is **install-on-request**, not install-all. The `install` metric
counts dependency installs; on 2026-09-11 its rank 1 was `ca-certificates`.

The Actor never requests `/api/formula.json`. Analytics ids may be tap-qualified
(`user/tap/formula`); those rows keep that identity and do not collapse onto a
core formula of the same short name.

### What changed, and what did not

Monitor diffs **last successful** state against **current successful** state.

- First successful monitor, including `changesOnly`, emits `BASELINE`.
- A new id after that is `NEW`. Rank moves are `RANK_UP` / `RANK_DOWN`.
- `installs` and `percent` are **not** compared. Homebrew rebuilds each rolling
  window daily, so those numbers move on almost every formula between any two
  runs (on 2026-09-12 all of the top 5 had new counts and identical ranks). They
  stay on every row as observation data; a row whose rank did not move is
  `UNCHANGED` and, in `changesOnly`, not delivered. Use `outputMode=all` if you
  want the count series itself. `UPDATED` remains in the schema for rows written
  before 2026-09-13 and is no longer emitted.
- An id missing from **two different sequential successful complete**
  windows becomes `EXITED`. The first miss is `pending_exits` only.
  A transport error, `OUTPUT_LIMIT`, or mass-removal guard does not advance
  that counter.
- `EXITED` means "not in this query's successful top-N / watchlist response".
  It does **not** mean the formula was deleted from Homebrew core.
- If a successful **top-N** window keeps under 50% of the previous ids at the
  same `topN`, the query is `PARTIAL` (mass-removal guard). Named watchlists
  skip that guard: every missing id is a pending/`EXITED` candidate. Pending
  exits are not applied on mass-removal.
- A `PARTIAL` parse window (some selected rows unparseable) still classifies
  the parsed rows against the last good state, but counts no exits and does
  not store the snapshot. The next `SUCCESS` window repeats those events with
  the same `event_id`; drop batches whose `query_status` is not `SUCCESS`.
- Broken stored state is `STATE_INCOMPATIBLE` for that query, not a silent new
  baseline. The run fails after writing `query_status` and `run_summary`.

Homebrew `count` values arrive as comma-formatted strings (`"251,930"`). A
broken count is `FAILED` for that row, never silently `0`.

### Failure semantics

`NO_RESULTS` is a verified analytics `items: []`. It is never a transport
failure, and it is not "this watchlist id is unranked in a populated window".
HTML when JSON was expected, truncated JSON, HTTP 5xx, 429 after retry, or a
body over 8 MiB (`SOURCE_LIMIT`) are `FAILED`. The Actor run then fails
(`SOURCE_FAILED`) after writing `query_status` and `run_summary`. Do not treat
Apify `SUCCEEDED` as proof the window was usable: automation should key off
`query_status`. Missing formula metadata is HTML 404 from GitHub Pages; that is
item-level `NOT_FOUND` for that formula (rank still comes from analytics), not
an empty ranking. A metadata request that fails after retries is item-level
`PARTIAL` with `error.code=METADATA_FAILED` and null `desc` / `homepage` /
`license`; the window stays `SUCCESS`, state advances, and that row is not
charged. `query_status` carries `not_found_count` and `metadata_failed_count`.
This holds for top-N and watchlist queries alike, so a formula that was removed
from core but is still in the analytics window cannot stall a watch. Invalid input writes one `query_status` (`INVALID_INPUT`) and
a `run_summary` (`INVALID_INPUT`), then fails the run before any Homebrew
request. A spend cap that cannot cover the window writes `NOT_PROCESSED` and
does not fetch Homebrew.

`maxFormulaRows` never truncates a window: if the formula rows do not fit, you
get `PARTIAL` / `OUTPUT_LIMIT` and the previous state is kept. A cap below
`topN` (or below the watchlist length) is `INVALID_INPUT` before any request.
Default is 400 so a full top-200 window plus `EXITED` rows still fits.

### Pricing

The Store **Pricing** tab is authoritative. Current pay-per-event rates:

| Event | When it is charged | FREE / BRONZE |
| --- | --- | ---: |
| `apify-actor-start` | Platform start; one event at the default 512 MB. The Actor never emits it. | $0.00005 |
| `formula-result` | Each delivered `SUCCESS` formula row in snapshot. In monitor, only `NEW` / `RANK_*` / `EXITED` / `REAPPEARED`; count drift is never charged. | $0.001 |
| `window-check` | Monitor mode, fully delivered window, including `NO_RESULTS` and zero-change. Not charged for `PARTIAL` or `FAILED`. | $0.01 |

`formula-result` is $0.0008 on SILVER and $0.0006 on GOLD+. Platform usage is
included, not passed through. `apify-default-dataset-item` is off: enabling it
would double-bill every Dataset row.

A run cap must cover start plus the events: $0.02 is not enough for the sample
top-20, and $0.01 is not enough for a quiet monitor. The minimum run cap is
$0.021 so the Store prefill can finish. Snapshot of 20 formulae →
1 × $0.00005 start + 20 × $0.001 = $0.02005. Quiet or first monitor of the
same top-20 → start + $0.01 `window-check` = $0.01005 and $0 change rows. A
weekly top-20 monitor with, say, three rank moves → $0.01005 + 3 × $0.001 =
$0.01305; the worst case is every row moving, $0.03005. Retries, `FAILED` and
`PARTIAL` are not charged as `formula-result` or `window-check` (the platform
still charges start).

Private cloud measurement on 2026-09-11 (build **0.1.4**, 512 MB): snapshot of
20 formulae → 1 × $0.00005 start + 20 × $0.001 = **$0.02005** (contribution
margin ~78%). Quiet or first monitor of the same top-20 → start + $0.01
`window-check` = **$0.01005** (~75%). A run cap must cover start plus the
events: $0.02 is not enough for the sample top-20. The minimum run cap is
$0.021 so the Store prefill can finish.

### Limitations

- Cask-install analytics are out of scope.
- There is no native Homebrew history comparable to storing yesterday's JSON
  yourself. `curl` of the same public endpoint remains free.
- Store search for this keyword family was in an EMPTY band at selection time.
  Do not read this README as evidence of measured demand.
- EXITED tombstones are kept 180 days, at most 20 000 per query. After prune, a
  very old return may classify as `NEW`.
- MVP assumes one writer per `stateNamespace` and sequential runs. There is no
  distributed lock.
- Optional metadata GETs are sequential. Metadata fields are omitted from the
  ranking fingerprint and never decide the window status. Tap-qualified analytics ids (`user/tap/formula`) have no
  Formulae `/api/formula/{name}.json` page; metadata stays null for those rows.

Legal review on 2026-09-11: `https://formulae.brew.sh/robots.txt` has a
Sitemap and no `Disallow`. Analytics are anonymous aggregates. Re-read robots
and the [Formulae API docs](https://formulae.brew.sh/docs/api/) before Store
publication; a later ban is a stop, not something to bypass.

### Use from AI agents with MCP

Connect the public Actor directly at
`https://mcp.apify.com?tools=automa-flow/homebrew-formula-install-rankings-monitor`.
Ask the agent to keep `mode: monitor` and `outputMode: changesOnly` on repeat
runs, to treat `NO_RESULTS` as a verified empty window, not a source
failure, and to treat `FAILED` / a failed run as an unusable Homebrew check.
This Actor is **not** targeted for MCP payment; do not treat that URL
as agentic billing readiness.

### Schedule and webhook example

This is a copy-paste illustration, not a hosted integration promise. Homebrew
rebuilds the window daily; weekly is a sensible cadence for rank moves.

Create the schedule (Console: Schedules, or the API; replace `<ACTOR_ID>` with
this Actor's ID and keep `stateNamespace` stable):

```json
{
  "name": "homebrew-onrequest-30d-weekly",
  "cronExpression": "0 7 * * MON",
  "timezone": "UTC",
  "isEnabled": true,
  "actions": [
    {
      "type": "RUN_ACTOR",
      "actorId": "<ACTOR_ID>",
      "runInput": {
        "body": "{\"mode\":\"monitor\",\"outputMode\":\"changesOnly\",\"stateNamespace\":\"onrequest-30d\",\"metric\":\"installOnRequest\",\"period\":\"30d\",\"topN\":20}",
        "contentType": "application/json; charset=utf-8"
      },
      "runOptions": { "build": "latest", "timeoutSecs": 180, "memoryMbytes": 512, "maxTotalChargeUsd": 0.12 }
    }
  ]
}
```

Then add a webhook on `ACTOR.RUN.SUCCEEDED` that posts the run to n8n / Make /
your endpoint:

```json
{
  "eventTypes": ["ACTOR.RUN.SUCCEEDED", "ACTOR.RUN.FAILED"],
  "condition": { "actorId": "<ACTOR_ID>" },
  "requestUrl": "https://your-endpoint.example/homebrew",
  "payloadTemplate": "{\"runId\":{{resource.id}},\"status\":{{resource.status}},\"datasetId\":{{resource.defaultDatasetId}}}"
}
```

In the receiver:

1. Fetch `GET /v2/datasets/{datasetId}/items?clean=true`.
2. Drop the batch if the `query_status` row is not `SUCCESS` (including
   `PARTIAL` parse windows) or the run status is `FAILED`.
3. Keep rows where `record_type=formula_observation` and `change_type` is one of
   `NEW`, `RANK_UP`, `RANK_DOWN`, `EXITED`, `REAPPEARED`. Each row already
   carries `formula`, `rank`, `rank_previous`, `source_url` and `event_id`, so
   it can be posted as an alert as-is; dedupe on `event_id`.
4. Open `source_url` for the human check. Do not read `EXITED` as "deleted from
   core".

### Protocol notes

User-Agent: `automa-flow-homebrew-formula-install-rankings-monitor/0.1
(+https://apify.com/automa-flow)`. Origin
is fixed to `https://formulae.brew.sh`. Redirects off that host fail the window.
Analytics timeout 30 s; optional formula metadata timeout 20 s. Uncompressed
bodies over 8 MiB are `FAILED` / `SOURCE_LIMIT`.

# Actor input Schema

## `mode` (type: `string`):

Get current rankings returns every selected formula and writes no history. Monitor changes compares this successful run with the last successful run of the same query in the same state namespace.

## `outputMode` (type: `string`):

All formulae returns every current row. Changes only omits UNCHANGED rows. Snapshot plus changesOnly is rejected before any Homebrew request.

## `stateNamespace` (type: `string`):

Isolates monitor history. Use a stable name per watch, for example onrequest-30d. Letters, numbers, '\_' and '-', 1-64 characters. Snapshot mode ignores it.

## `metric` (type: `string`):

installOnRequest counts installs people asked for. install also counts dependency installs and is inflated by packages such as ca-certificates.

## `period` (type: `string`):

Homebrew analytics window. These are rolling 30, 90 or 365-day aggregates, not a native history series.

## `topN` (type: `integer`):

How many ranked formulae to keep when watchlist is empty. 1-200, default 20. Ignored for filtering when watchlist is set; rank still comes from the full window. Watchlist history is keyed without topN, so changing topN does not reset a named watch.

## `watchlist` (type: `array`):

Optional Homebrew formula names, tap-qualified names (user/tap/formula), or https://formulae.brew.sh/formula/{name} URLs, 0-50. Empty means use topN. A name missing from a populated analytics window is delivered as an unranked SUCCESS row (rank null), not NOT\_FOUND. Duplicates keep the first name.

## `includeFormulaMetadata` (type: `boolean`):

If true, fetch each selected core formula's /api/formula/{name}.json for desc, homepage and license. Tap-qualified analytics ids skip this request. Default off. Never downloads the full formula catalog.

## `maxFormulaRows` (type: `integer`):

Cap on delivered formula\_observation rows, including EXITED. Status rows are free. Default 400 so a full top-200 window plus exits still fits. If the window does not fit, the run is PARTIAL/OUTPUT\_LIMIT and previous state is kept.

## Actor input object example

```json
{
  "mode": "snapshot",
  "outputMode": "all",
  "stateNamespace": "default",
  "metric": "installOnRequest",
  "period": "30d",
  "topN": 20,
  "includeFormulaMetadata": false,
  "maxFormulaRows": 400
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `runSummary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "mode": "snapshot",
    "metric": "installOnRequest",
    "period": "30d",
    "topN": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("automa-flow/homebrew-formula-install-rankings-monitor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "mode": "snapshot",
    "metric": "installOnRequest",
    "period": "30d",
    "topN": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("automa-flow/homebrew-formula-install-rankings-monitor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "mode": "snapshot",
  "metric": "installOnRequest",
  "period": "30d",
  "topN": 20
}' |
apify call automa-flow/homebrew-formula-install-rankings-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,automa-flow/homebrew-formula-install-rankings-monitor"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/S4bMDp0tDRiTchMC6/builds/AAkYKPd3pjmapE3Xh/openapi.json
