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:
- Package source: the versioned files rooted at
package.json.package.jsonis the source of truth. - Package config: the
minionblock ofpackage.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. - Package storage: the package's own durable storage, reached with
packageStorage()(key/value) andsql()(SQLite) fromminion:runtime. This is the durable-data primitive for every package surface: exports, subscriptions, jobs and package apps. See Package storage and SQL . - Package jobs: scheduled execution owned by the package (
package.json#minion.jobs).
When to use which:
- Durable package data:
packageStorage()orsql() - 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 ofpackage.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 inREADME.mdand export JSDoc.minion.tags: up to 10 tags, each up to 32 characters.minion.dependencies: map of direct saved package names imported through staticminion:@...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 declarationsminion.webhooks: optional inbound webhook declarations bound to package exports (mint a handle withminion.webhooks.mintand register it withminion.webhooks.apply; see Inbound webhooks )minion.app: optional web app served athttps://<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 inpackage.json#minion.dependenciesusing the imported package name, for example"dependencies": { "@your-org/my-package": "*" }inside theminionobject. 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:runtimeis 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@examplethat importsminion:@your-org/package/exportand calls it). minion.packages.getreturns 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()orsql()for the package's own data, from exports, subscriptions, jobs, workflows and package apps. - Writing ad hoc
executecode? 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 ownpackageStorage()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>/.
accessis"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()(orsql()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-pathrequest 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 charactersfilters: 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.jobsas 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 withminion.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()orsql(). 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.saveto create a package or save a new version from a complete UTF-8 text file setminion.packages.getandminion.packages.listfor 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.runChecksandrepo.publishto inspect, edit, validate and publish package source file by file ( Editing package source )minion.packages.publishto 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.updateto change mutable settings:description,hidden, andpinnedVersionto lock or unlockminion.packages.deleteto permanently remove a package the owner typed the name of (confirmNamemust 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.