Skip to docs content
Browse documentation
On this page

Package authoring

START HERE when creating or materially changing a Minion package: package shape, required README.md (human) and AGENTS.md (agent), README Intent section, per-export JSDoc (search Purpose), personal-details hygiene before going public, secret-using package approval checklist, and scope-update guidance without adding new primitives.

Use this guide when creating a new Minion package or materially changing an existing one.

A package is not done until root README.md is present and says what the package does and what it needs (secrets, hosts), every public export has JSDoc as specified in Export JSDoc , and the smoke tests in Verify your publish pass (or the user explicitly skips a surface). Publish checks fail when README.md is missing or empty.

Unique Dynamic Worker days, and how a stable module graph reuses one isolate per UTC day, are documented once in Platform efficiency . Package READMEs do not repeat that cost model.

Choose how to write the source

Packages are sets of UTF-8 text files rooted at package.json. There are two ways to write one, and neither uses git:

  • A new package, or replacing everything. Call minion.packages.save({ name, description, files }) with the complete file set: package.json, README.md and your source files. Owners and admins can pass publish: true. Binary files aren't supported.
  • Changing an existing package. Open a repo session with minion.repo.openSession({ package }), read and edit files (repo.readFile, repo.editFiles, repo.applyPatch), run repo.runChecks, then publish with repo.publish. Members hand the sessionId to an owner or admin. See Editing package source .

If a request needs binary assets or a substantial local build and test loop, tell the user it fits a coding agent better and keep the Minion package a thin orchestrator. See Offload work that does not fit a Worker isolate .

Package docs (README.md)

Publish requires a non-empty root README.md. Write it for people and agents alike: what the package does, what it needs (secrets, hosts, integrations), how to use its exports, and how someone knows it is working. Search indexes the README, so it is how other agents find the package later.

README Intent section

Package intent is human-authored guidance, not a Minion primitive. Keep it in the root README.md so people (and search) see why the package exists. It is a convention, not something Minion checks.

When you create or materially change a package:

  1. Include or maintain a ## Intent section in README.md.
  2. Capture the user's goal in a few concrete sentences.
  3. Ask the user when the intent is unclear or underspecified.
  4. Update the intent only when you are confident the goal changed.
  5. If the user expands the package scope, update the section with the new scope.

Do not add a package manifest field, runtime object, saved value, or other Minion primitive solely to track intent.

README Intent is package-level. It does not replace per-export JSDoc.

Minimal shape

# Package Name

## Intent

This package exists to ...

## Needs

The githubToken secret (api.github.com).

Keep the Intent section concise. It should explain why the package exists and what success means for the user, not duplicate every implementation detail.

Export JSDoc

Package search returns one entry per export, built from the comment at the top of the export's file. Agents skim those entries first when choosing among sibling exports.

TypeScript types and the export name give call shape when present. They do not say when or why to pick one export over another. README ## Intent is package-scoped and often does not name every export. Neither replaces per-export JSDoc.

Do not add a package manifest field, runtime object, saved value, or other Minion primitive solely to track export purpose. Put it in JSDoc on the exported function.

When you create or materially change a public export:

  1. Write JSDoc immediately above the exported function (or above export default for a local binding in the same file).
  2. Start with one line that states what the export does and when to call it.
  3. Add @param for each input.
  4. Add @returns.
  5. Add @example that imports minion:@your-org/package/export and calls it.

Implement the function in the export file (or a local binding in that file) so the comment sits on the exported symbol. A comment on an imported re-export isn't picked up.

/**
 * Format a Discord moderation report for a channel.
 * Use when a human or job needs a readable summary of recent flags.
 *
 * @param input - Channel id and optional lookback window
 * @returns Markdown report body
 *
 * @example
 * import formatReport from 'minion:@your-org/discord/format-report'
 *
 * const report = await formatReport({ channelId: '123' })
 */
export default async function formatReport(input: {
	channelId: string
	lookbackHours?: number
}): Promise<{ markdown: string }> {
	return { markdown: '' }
}

Treat a missing export comment as unfinished work, the same as a missing README ## Intent section.

Package app routing

A package app is served at https://<org slug>.runminion.run/<package>/<path>. Minion strips the mount (/<package>), so the app receives /<path>, and root-relative links such as /audio/123 escape the app. Build every in-app link, form action and redirect from the x-minion-base-path request header (the mount, for example /guestbook):

import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => {
	const base = c.req.header('x-minion-base-path') ?? ''
	return c.html(`<a href="${base}/audio/123">Play</a>`)
})

export default app

See Package apps for the manifest block, limits and security model.

minion.description (short tagline)

package.json#minion.description is a short tagline, not a feature dump. Aim for about 80 to 120 characters (hard max 200). Prefer outcome phrasing such as “Send transactional email via Resend” over inventory lists of exports, auth, or APIs.

Put feature lists, API surface, auth notes, and longer guidance in README.md and export JSDoc , not in minion.description.

Package visibility

Every package belongs to one organization and is visible only inside it. There is no public catalog and no per-package access: every member sees every package. To keep a package out of ordinary search results, hide it with minion.packages.update({ name, changes: { hidden: true } }). That is a discovery preference. The package stays saved, executable and editable.

Because every member can read a package's source, treat it like code in a shared repo. Never put secrets, tokens, home addresses, private emails or other personal details in the files. Parameterize identifiers, and use secrets or integrations instead of hardcoded credentials.

Work that does not fit a Worker isolate

Package checks and publish rebuilds bundle every import inside a short-lived isolate. Only zod and hono (with subpaths such as hono/html) are allowed from npm; any other npm package fails the bundle check with an isn't on the sandbox allowlist error.

Do not vendor a large library or skip checks. Keep the Minion package as a thin orchestrator and run the heavy work in a process the owner operates. See Offload heavy work .

Secret-using packages

When a package will use a secret (a {{secret:name}} placeholder in an outbound fetch):

  1. Make sure the secret exists and lists the hosts it may be sent to. A person adds it on the secure entry page or at /app/secrets; never paste a value into chat. See Secrets and host approval .
  2. Only the approved hosts ever receive the value. A request to any other host is refused until an owner or admin approves it.
  3. A secret used to verify webhooks (verification.secretName, or a challenge's secretName) must also be allowed for webhook use by an owner or admin on /app/secrets. Until then deliveries fail closed.
  4. Smoke-test from execute with a static minion:@your-org/package/export import. Use a read-only export or a package-supported dry-run input that actually reads the secret (for example an authenticated read-only API call), so the test proves secret access without external side effects.
  5. Only then treat the package as ready to run.

For OAuth logins, use an integration instead of a secret: createAuthenticatedFetch('github') from minion:runtime. See OAuth .

Cross-package composition

To call another package's export, import it and call it. Use a static import. When the call site is package source, also list the package in package.json#minion.dependencies:

import sendMail from 'minion:@your-org/mailer/send'
await sendMail({ to, subject, text })

Imports only resolve inside your own organization. External HTTP clients use inbound webhooks instead.

README.md and export @example blocks follow the same rule: import, then call.

Verify your publish

After publish succeeds, and after any secret host approvals, confirm every export has a real JSDoc comment (see Export JSDoc ), then run smoke tests for every declared surface before calling the package complete. These are real runs with real side effects; use a deliberately visible irreversible-side-effect guard when a smoke test should stay safe.

  1. Exports and secrets: statically import minion:@your-org/package/export from execute against a read-only export or package-supported dry-run input that exercises the secrets it uses (see Secret-using packages ).
  2. Jobs: run minion.jobs.runNow({ id }), read the run with minion.runs.get, then enable the job. A job can't be enabled until one run has succeeded.
  3. Package apps: request the app's URL (https://<org slug>.runminion.run/<package>/...) with each method your handler supports, then check minion.runs.list({ source: 'app' }) for errors. That works for a public app; for a members app, use Open app from the package page in a signed-in browser. See Package apps .
  4. Subscriptions: call minion.events.dispatchSynthetic({ package, topic, params }) for each declared topic, then check minion.events.deliveriesList({}). See Synthetic event dispatch and the package subscriptions guide .
  5. Webhooks: mint the webhook and send a synthetic request with minion.webhooks.dispatchSynthetic. See Inbound webhooks .
  6. Optional UI checks: open the app in a browser. A request from curl doesn't replace checking layout, links and forms.

Only after README.md, per-export JSDoc, and these checks pass (or the user explicitly skips a surface) treat the package as ready to run.