Skip to docs content
Browse documentation
On this page

Flake Hunter

A worked cron example: a package-owned daily job that scans CI for flaky end-to-end runs, stays quiet on a clean day, and only emails you when a flake signal is present. Load this when someone asks how to hang a schedule on a package they own.

This is the cron example. A schedule is optional, not the product. The product is a package you own that can run while your laptop is closed, with no model in the loop and no tokens spent.

The package below is a pattern you build, not a package that ships with Minion. Ask your agent to write it for your repo. The primitive is documented in Jobs, workflows, and webhooks .

The job

Every day at 4am America/Denver, the published package scans the last 24 hours of GitHub Actions jobs on your-org/your-repo. If any job indicates e2e flakiness, it emails you once. A clean day is silent.

That is the factory loop from How Minion works aimed at CI noise: ask once, save the scan, trigger it on a cron.

The pattern stops at "scan and notify". If you want something to investigate the flake for you, have the mail link to the failing run and hand that to an agent yourself.

Package shape

Declare the schedule in package.json#minion.jobs so it travels with the code it runs. It needs a connected github integration, see Connect GitHub :

{
	"name": "@your-org/e2e-flake-hunter",
	"exports": {
		".": "./src/scan.ts",
		"./daily": "./src/daily.ts"
	},
	"minion": {
		"jobs": [
			{
				"name": "daily",
				"export": "./daily",
				"cron": "0 4 * * *",
				"timezone": "America/Denver"
			}
		]
	}
}

Jobs start disabled. Publish, run the job once with minion.jobs.runNow, then enable it with minion.jobs.update. The job runs the default export of the entry it names. The usual shape is a thin module that calls a callable export and notifies only when there is news:

import { minion, packageStorage, createAuthenticatedFetch } from 'minion:runtime'

const gh = createAuthenticatedFetch('github')

export default async function daily() {
	const store = packageStorage()
	const since = (await store.get('lastRunId')) ?? 0
	const res = await gh('https://api.github.com/repos/your-org/your-repo/actions/runs?status=failure&per_page=50')
	const { workflow_runs } = await res.json()
	const fresh = workflow_runs.filter((r) => r.id > since)
	if (fresh.length === 0) return { flakes: 0 }
	await store.set('lastRunId', Math.max(...fresh.map((r) => r.id)))
	await minion.email.send({
		subject: `${fresh.length} CI failure(s) to look at`,
		text: fresh.map((r) => `${r.name}: ${r.html_url}`).join('\n'),
	})
	return { flakes: fresh.length }
}

Keep the scan itself callable from execute so you can prove it before the clock does:

import scan from 'minion:@your-org/e2e-flake-hunter'

export default async function main() {
	return await scan()
}
  • Keep the scan read-only in one export, so you can run it from chat without sending mail.
  • Record the newest run id in packageStorage() before you notify, so a retry doesn't mail you twice.
  • minion.email.send with no to only mails your own verified address. Skip it when there is nothing to report. An empty digest every morning trains people to ignore the real one.
  • The cursor belongs in packageStorage() (or sql() if you want history), not in a local variable.

jobs.runNow fires one run for debugging. jobs.update flips enabled after that run looks right. Schedule, timezone and source live in the package: change them there and publish, which disables the job until you enable it again.

Example prompts

Paste one of these into an agent already connected to your Minion account.

Build one

Search Minion for the job and package guides. I want a daily cron that scans the last 24 hours of GitHub Actions jobs on this repo for e2e flakes. Stay quiet when the day is clean. If a flake signal is present, record it and mail me once. Publish with the job disabled, run it once, then enable it only after that run looks right.

Operate an existing copy

Import my flake hunter package and tell me whether today's sweep ran, and what the last outcome was. If I want it quiet for a bit, disable the job with jobs.update rather than deleting it.

What you see

  • Chat (execute): Invoke the scan or the daily export and read a structured result
  • /app/jobs: The daily row, enable/disable, timezone, and "run now"
  • /app/runs: Failures and recent runs for that job
  • Your inbox: Mail only when the sweep found something worth saying

Where to go next