Plugins

Install a schema-2 flow extension onto a base flow — flows-plugin.json, flows add, flows plugin, and the trust tiers the gallery displays.

A flow plugin is a directory in a public GitHub repository whose flows-plugin.json says what it contributes to a base flow. Helper plugins (kind absent or "helper") still install from npm as @flows/helper-* and extend Ctx with verbs; that v1 path is unchanged. Schema 2 adds a second kind on the same file:

{ "schema": 2, "kind": "flow-extension" }

The entry default-exports flow() and declares handlers, hooks, triggers, permissions, compat, and the same mandatory preflight. Browse the vendored catalog on the plugin gallery. An available card's Install plugin badge opens Cloud with the base flow and the plugin source:

/cloud/flows/deploy?flow=<base url>&plugin=<plugin url>

plugin may repeat. The badge helper appends each one so a second plugin does not overwrite the first.

See the plugin gallery — including Babysitter's pinned artifact and release gate.

flows-plugin.json schema 2

kind absent or "helper" keeps today's v1 semantics byte-for-byte. "schema": 2, "kind": "flow-extension" is additive: it requires entry, compat, and source (filled by flows add); it forbids verbs (a flow extension that also wants verbs ships a helper plugin beside it). Unknown top-level keys are refused (plugin_manifest_invalid). preflight is mandatory.

Worked example — Babysitter on Software Garden:

{
  "schema": 2,
  "kind": "flow-extension",
  "name": "babysitter",
  "version": "0.2.0",
  "description": "Native Babysitter for Software Factory: turns a verified PR delivery into one cloud:babysitter-turn request.",
  "compat": {
    "surface": "^2.0.26",
    "sdk": "^2.0.26",
    "base": [{ "name": "software-factory", "version": "^2.0.0" }]
  },
  "entry": "babysitter.flow.ts",
  "extends": { "handlers": true, "hooks": [] },
  "triggers": [
    { "provider": "github", "event": "pull_request", "actions": ["opened", "synchronize", "reopened", "ready_for_review", "closed", "labeled", "unlabeled"] },
    { "provider": "github", "event": "pull_request_review", "actions": ["submitted", "dismissed"] },
    { "provider": "github", "event": "check_run", "actions": ["completed"] },
    { "provider": "github", "event": "issue_comment", "actions": ["created"] }
  ],
  "permissions": {
    "integrations": ["github"],
    "harnesses": ["codex"],
    "mcp": [],
    "writes": ["cloud:babysitter-turn"],
    "budget": { "dollars": 1, "wallclock": "5m" }
  },
  "preflight": { "credentials": [], "servers": [] }
}

Rules that matter at review time:

  • name is kebab-case (^[a-z0-9]+(-[a-z0-9]+)*$).
  • compat.surface / compat.sdk are semver ranges checked against the pinned runtime. Mismatch is plugin_incompatible, never a warning.
  • compat.base[] names the base flow() and a version range. A base without version matches only "*".
  • permissions.writes is a declaration for review, labelled UNENFORCED until gate 8 (#442) turns it into scope enforcement. The honest statement until then: the plugin can do anything the deployment can.
  • Manifest triggers are validated against the surface registry providerEventTypes. An event the registry cannot route is plugin_event_unroutable.

Relayflows SDK 2.0.31 carries the isolated hosted runtime for the reviewed Babysitter artifact, but no Cloud capability adapter or Relay native existing-session delivery release exists yet. The catalog therefore remains metadata-only and exposes no install control. Cloud may enable Babysitter only after both dependencies are merged and deployed; an arbitrary labelled pull request is not execution authority.

flows add

Input forms accepted (ref may be a tag, branch, or sha at input only):

flows add github:<owner>/<repo>@<ref>#<path>
flows add https://github.com/<owner>/<repo>/tree/<ref>/<path>
flows add <owner>/<repo>@<ref>#<path>
flows add <helper-name|@flows/helper-name>

The GitHub path is public, unauthenticated, https only. Private repositories are out of scope and answer 404 (plugin_source_unresolved). Resolution:

  1. Ref → 40-hex sha.
  2. Tree at that commit, refusing symlinks, submodules, traversal, files over 256 KB, or a plugin over 2 MB.
  3. Content digest = sha256 of the canonical [{bytes,path,sha256}] payload (the same routine sealed bundles use).
  4. Bytes land at .flows/plugins/<name>@sha256:<digest>/, never in node_modules.
  5. flows.json.plugins records the canonical github:<owner>/<repo>@<sha>#<path> — a branch or tag is never persisted.
  6. flows.lock.json (version 2) records name, version, source, digest, manifest hash, and declaration order. That order is composition order.

A later re-fetch that yields a different digest for the same sha is plugin_source_drift.

flows plugin

flows plugin list [--json]
flows plugin verify [--json] [--offline]
flows plugin remove [--json] <name>
flows plugin update [--json] [--yes] [--to <ref>] [<name>]
  • list reads flows.lock.json.
  • verify re-hashes the store against the lock and, unless --offline, re-fetches the pinned commit. Drift is exit 2.
  • remove drops the name from flows.json.plugins and the lock (order is rebuilt) and deletes the store directory only if nothing else references it.
  • update re-resolves, shows the permissions / events / budget diff, and requires --yes (refuse otherwise, exit 2).

flows check composes base + plugins, runs plugin preflight, compat, and event routability, and prints one EXTENSION line per composed extension. flows deploy … [--plugin <github ref>]… is send-only: it does not rewrite the working tree's flows.json.

Trust tiers

The gallery and the deploy wizard display a tier. The label is never used to skip a check.

TierHow it is assigned
first-partyAgentWorkforce/* at a sha reachable from main
verifiedbundle identity.json keyid matches a publisher key registered in Cloud
communityanything else

Babysitter in catalog v3 is first-party: its reviewed v2.0.26 artifact commit is reachable from AgentWorkforce/flows main, and the hosted runtime provenance is the published SDK v2.0.31 release. The label does not widen runtime authority.

permissions.writes stays a reviewed declaration labelled UNENFORCED until gate 8. Displaying a first-party or verified badge does not enforce write scope, skip the digest, or route an event the registry does not carry.

Install badge

flowPluginBadgeMarkdown() renders the README form for an extension whose activation gate is ready. plugin is appended so repeats survive:

Babysitter's gate requires both cloud-babysitter-capability-adapter and relay-native-existing-session-delivery to carry a pull request, merge commit, merge time, deployment receipt, and deployment time. Until both proofs are recorded and the gate state is ready, flowPluginInstallHref() returns null and the gallery shows catalog status plus source only.

The catalog itself is versioned JSON, {version:3, plugins:[{name, description, source:{owner,repo,path}, ref, digest, manifestSha256, compat, runtime, activation, tier, base}]}. Any public repo with a flows-plugin.json remains installable by an explicit source URL; the curated catalog does not advertise an activation route until its dependency gate is satisfied.