Skip to docs content
Browse documentation
On this page

Packages

A package is Minion's unit of saved, versioned code: a set of text files rooted at package.json, owned by one organization. Every connected agent in the organization can find it with search, import its exports, and rely on its jobs, subscriptions, webhooks and app.

A package's source is a list of numbered versions. A version doesn't run until it is published; see Save, edit and publish .

Packages are not integrations or MCP servers. See Packages, integrations, and MCP servers when those three look interchangeable.

Mental model

Think in terms of:

  • packages
  • package exports
  • package apps
  • package subscriptions
  • package-owned jobs
  • package-owned webhooks

Packages are the saved-entity unit across search, execute, repo editing and UI hosting. The package page in the app (/app/packages/<name>) lists the files of each version, its check status, the Publish, Lock and Unlock buttons, and, for a package with an app, the app's address and an Open app link. Inbound HTTP uses webhooks . Sharing a package with another organization isn't available yet; see Sharing packages .

Package state model

Four concepts make up a package's state:

  1. Package source: the versioned files rooted at package.json. package.json is the source of truth.
  2. Package config: the minion block of package.json (description, tags, dependencies, jobs, subscriptions, emits, webhooks, app). Secrets are not package config: a package uses an organization secret by name, and the secret says which hosts may receive it.
  3. Package storage: the package's own durable storage, reached with packageStorage() (key/value) and sql() (SQLite) from minion:runtime. This is the durable-data primitive for every package surface: exports, subscriptions, jobs and package apps. See Package storage and SQL .
  4. Package jobs: scheduled execution owned by the package (package.json#minion.jobs).

When to use which:

  • Durable package data: packageStorage() or sql()
  • Credentials: organization secrets, used by name as {{secret:name}}; non-secret knobs: packageStorage()
  • Scheduled work: a package job

package.json

Use package.json as the source of truth. Every field, with the rules each save is checked against, is in Package manifest .

Important fields:

  • name: a scoped package name, @<org slug>/<leaf>. The scope must be your organization's slug. Look up a package by that name (or its leaf).
  • exports: the import and export map, at the top level of package.json. Either a single path string or an object such as { ".": "./src/index.ts", "./digest": "./src/digest.ts" }.
  • minion.description: a short tagline for search and package listings (max 200 characters). Prefer outcome phrasing (“Send transactional email via Resend”) over feature lists; put the detail in README.md and export JSDoc.
  • minion.tags: up to 10 tags, each up to 32 characters.
  • minion.dependencies: map of direct saved package names imported through static minion:@... imports ({ "@your-org/package": "*" }).
  • minion.jobs: optional package-owned schedules, an array of { name, cron, export?, timezone?, params? } (up to 20)
  • minion.subscriptions: optional event-topic subscriptions, keyed by topic (see Package subscriptions and events )
  • minion.emits: optional package-emitted event topic declarations
  • minion.webhooks: optional inbound webhook declarations bound to package exports (mint a handle with minion.webhooks.mint and register it with minion.webhooks.apply; see Inbound webhooks )
  • minion.app: optional web app served at https://<org slug>.runminion.run/<package>/ (see Package apps )

Publishing validates the manifest and reports every problem as a plain sentence.

npm dependencies

Package code can import two npm packages: zod and hono (including subpaths such as hono/html). Minion bundles them from its own copies. Importing any other npm package fails the publish bundle check with an isn't on the sandbox allowlist error.

Keep a package that needs a large library as a thin orchestrator and run the heavy work somewhere you operate; see Offload heavy work .

Package exports

package.json exports is the package's callable and importable surface.

  • Cross-package imports use the full package name, such as minion:@your-org/my-package/export-name. A static import is the default for package reuse, from execute and from other packages. Static imports are typed, publish-verified and visible in the dependency graph.
  • Imports only resolve inside your own organization (@<org slug>/...). They always use the imported package's current runnable version.
  • Every direct static minion:@... import in saved package code must be declared in package.json#minion.dependencies using the imported package name, for example "dependencies": { "@your-org/my-package": "*" } inside the minion object. Publish checks fail when static imports and declarations differ, when a declared dependency is never imported, when a dependency isn't runnable, or when the graph loops back to the package.
  • minion:runtime is always host-owned and request-scoped: import { minion, packageStorage, sql } from 'minion:runtime'.
  • Exports are normal modules. They may expose a default export, named exports, or both. A job, subscription or webhook runs the default export of the entry it names.
  • Add JSDoc to every public export. Package search builds each export's entry from the comment at the top of its file. See Export JSDoc for the done criteria (@param, @returns, and an @example that imports minion:@your-org/package/export and calls it).
  • minion.packages.get returns the description, README, export names and files, jobs, dependencies and version list. It does not return the other source files. To read them, open a repo session (minion.repo.openSession + minion.repo.readFile) or browse them at /app/packages/<name>.

Package reuse

Import an export and call it. Use a static import from execute and from other packages:

import handleEvent from 'minion:@your-org/event-subscriber/handle-event'
import profile from 'minion:@your-org/google/profile'

const result = await handleEvent({ event })
const account = await profile({})

Declare every static minion:@ import in package.json#minion.dependencies. Static imports from ad hoc execute bundle per call and always see the current runnable version. Static imports in saved package code are bundled when the package is checked and published. Exactly-once work uses workflows .

There is no author-facing packages.invoke. External clients that must call a named export over HTTP use inbound webhooks: declare one webhook per export, mint a handle with minion.webhooks.mint, and POST JSON. See Inbound webhooks .

Package storage

Every saved package owns its own durable storage, reached with packageStorage() and sql() from minion:runtime. One rule per context:

  • Writing a saved package? Use packageStorage() or sql() for the package's own data, from exports, subscriptions, jobs, workflows and package apps.
  • Writing ad hoc execute code? It has no package storage. Persist durable state from a saved package, or statically import the owning package's export.
  • Touching another package's data? Statically import that package's export (import fn from 'minion:@your-org/package/export') so its own packageStorage() does the reading and writing.
import { sql } from 'minion:runtime'

export default async function listItems() {
	const { rows } = await sql('select name from items order by name asc')
	return rows.map((row) => row.name)
}

sql() runs one SQLite statement in the package's own database and returns { rows, rowCount }, at most 1,000 rows. Page large reads with LIMIT/OFFSET (or a keyset). Limits, the Postgres migration notes and how storage counts against your plan are in Package storage and SQL .

Which package's storage a module touches is decided by the bundler, not by source code. Each module is stamped with the saved package it came from, and the host grants storage access only from that record. When package A's export statically imports package B, B's code keeps B's storage. Hand-written code can't claim another package's id to read its data, and inline execute code has no package at all.

Package apps

A package app is optional. When package.json#minion.app names one of the package's exports, Minion serves that export, a Hono app or any fetch(request) handler, at https://<org slug>.runminion.run/<package>/.

  • access is "members" (signed-in members of your organization, the default) or "public".
  • Members open a members app through Minion (the package page's Open app link, https://app.minion.codes/a/<org slug>/<package>/), which signs them in on the app's domain. A public app is opened at its address.
  • Each request runs in a fresh sandbox, so durable data goes through packageStorage() (or sql() in a members app), the same storage the package's exports and jobs use.
  • The app sees paths with the mount stripped; build links from the x-minion-base-path request header.

The manifest block, a Hono example, limits and the security model are in Package apps .

Attached MCP servers

Remote MCP servers added at /app/mcp-servers are available as minion.mcp["name"] in execute and in package runtimes: package apps, subscription handlers, jobs, workflows and webhook deliveries. Only approved tools can be called. See Connect remote MCP servers .

Package subscriptions

Package subscriptions let a saved package react to Minion event topics. Define them under package.json#minion.subscriptions as a record keyed by topic:

{
	"minion": {
		"subscriptions": {
			"email.message.received": {
				"export": "./on-email",
				"description": "Process stored inbound mail.",
				"filters": {
					"tag": "invoices"
				}
			}
		}
	}
}

Each subscription has:

  • export: optional package export to run (defaults to ".")
  • description: optional human-readable purpose, up to 200 characters
  • filters: optional top-level payload keys that must match exactly

The handler gets the event envelope as its params and runs as an ordinary package run with packageStorage(), sql(), secrets and the minion capabilities.

Platform topics today are run.error.recorded, job.failed, integration.auth.succeeded, integration.auth.failed, mcp.server.disconnected, mcp.server.reconnected, webhook.received, webhook.delivery.failed, email.message.received and email.message.quarantined. Packages can also emit their own topics, scoped to the organization. Payload shapes, filters, the hop limit and testing are in Package subscriptions and events . After publishing, smoke-test a handler with minion.events.dispatchSynthetic ( Synthetic event dispatch ).

List which packages subscribe to a topic with minion.events.subscriptionsList({ topic }), and see recent handler runs with minion.events.deliveriesList({}).

Package webhooks

Inbound HTTP webhooks are declared under package.json#minion.webhooks and bound to a package export. Declaring a webhook does not open ingress. Mint a handle with minion.webhooks.mint first, then use minion.webhooks.apply to register a destination such as a GitHub repository hook. Full contract, signature examples, and payload shape: Inbound webhooks .

Package-owned jobs

Recurring schedules belong on a saved package:

  • Define them under package.json#minion.jobs as an array of { name, cron, export?, timezone?, params? }.
  • Each job runs the default export of the entry it names ("." if omitted).
  • A new job starts disabled. It can be enabled once a run of it has succeeded, so run it first with minion.jobs.runNow({ id }), then enable it with minion.jobs.update({ id, changes: { enabled: true } }).
  • Changing a job's schedule or export in a new version disables it again until it is re-verified.
  • Shared durable data goes through packageStorage() or sql(). Jobs can't run more often than your plan's minimum interval.

Jobs are part of the package definition. Deferred one-shot work uses workflows.create({ runAt }) from execute or package runtime; see Workflows .

minion.jobs.update adjusts an existing job's state, such as enabling or disabling it. Name, schedule and source stay in the package: change the job entry there and publish. minion.jobs.runNow triggers an existing job immediately for debugging. Runs show up on /app/jobs and /app/activity.

Save, edit and publish

When creating a package or publishing a new version, include a non-empty root README.md. It says what the package does, what it needs (secrets, hosts, integrations) and how someone knows it is working, and it is what search indexes. Publish checks fail if it is missing or empty. A concise ## Intent section in the README is a recommended convention.

Use:

  • minion.packages.save to create a package or save a new version from a complete UTF-8 text file set
  • minion.packages.get and minion.packages.list for saved package metadata (full export list, jobs, versions, app URL). They don't return source files.
  • minion.repo.openSession, repo.readFile, repo.editFiles, repo.applyPatch, repo.runChecks and repo.publish to inspect, edit, validate and publish package source file by file ( Editing package source )
  • minion.packages.publish to run the checks on a saved version and make it the one that runs (owners and admins; members can save but not publish)
  • minion.packages.update to change mutable settings: description, hidden, and pinnedVersion to lock or unlock
  • minion.packages.delete to permanently remove a package the owner typed the name of (confirmName must match)

A package can have up to 200 files, 256 KB per file and 2 MB in total. Publishing runs the checks: manifest, README.md, declared dependencies, bundling every export, TypeScript and job limits.

Hidden packages

Use minion.packages.update({ name, changes: { hidden: true } }) to hide a package from ordinary search. Set hidden: false to show it again. packages.list and packages.get return a hidden boolean on each package, and packages.list({ includeHidden: true }) includes hidden ones.

Hiding is a discovery preference, not deletion. The package stays saved, executable and editable.

Delete a package

Deleting a package removes it from the organization. It is permanent.

Call minion.packages.delete({ name, confirmName }). Show the owner the package name and what will be destroyed, wait for them to type that name, then pass it as confirmName. Owners and admins only.

Delete stops the package's jobs and clears its storage and package-scoped secrets. Hiding a package is not deletion; use that when the package should stay saved.

Locking a version

Locking pins what runs to one version that passed the publish checks. A locked package keeps serving that version: newer saves and publishes are stored but don't go live until an owner or admin changes the lock. Dependencies are not locked along with it; each runs its own runnable version.

Lock from the package page in the app (the Lock button on a version that passed its checks), or with minion.packages.update({ name, changes: { pinnedVersion: 3 } }). Pass pinnedVersion: null to unlock. Locking and unlocking are for owners and admins.

When a provider token is broader than the operation, Gmail's gmail.compose allows managing drafts and sending email. A Minion package lock pins the reviewed version of this package; it does not reduce Google's token permissions. See Gmail drafts and send permissions .

A lock doesn't shrink an OAuth token or an MCP connector. To keep a connected MCP server off execute and other packages, lock the server to this package instead: Lock an MCP server to a package .

Search and discovery

Search returns packages as the saved-entity unit. Each package has one entry (description, README excerpt, export import specifiers, jobs) and one per export, so agents find saved code by what it does. Open one export's contract with entities: ["package:<name>#<export>"]. Hidden packages are left out.