# JerapahFlow plugins This file tells agents how to add a **user plugin**. Do not put personal or site-specific scripts in `packages/server/scripts/` (core, read-only). Do not put them in `examples/plugins/` (shipped examples only). ## Workflows (instance data) Live workflows are **not** product source. They live under `data/workflows//` (gitignored; override with `JFLOW_WORKFLOWS_DIR`). | Kind | In git? | Path | |---|---|---| | Live / personal YAML | No | `data/workflows//` | | Example presets | Yes | `examples/workflows/*.yaml` (copy into editor only; runner does not load them) | Do **not** add personal YAML under `packages/server/`, `examples/workflows/`, or `data/workflows/`. Prefer owner `local`. Example presets must use **core** scripts only (no `plugin/…` that requires install). ## Where things live | Kind | YAML `script` | Editable | Path | |---|---|---|---| | Core | `fetch-http.js` | No | `packages/server/scripts/` | | User plugin | `plugin/` | Yes | `plugins//` | | Example source | install → `plugin/` | After install | `examples/plugins//` | Runtime load path is repo-root `plugins/` (`PLUGINS_DIR` in `packages/server/paths.js`). Override with `JFLOW_PLUGINS_DIR` only in tests. Plugins are **outside** the pnpm workspace (`packages/*`). Do not add `plugins/*` to `pnpm-workspace.yaml`. ## When to create a plugin Create a user plugin when the script is: - Site-specific (LAN IPs, personal modems, private APIs) - A fork of a core script the user wants to edit - Anything that should stay in git but not ship as core Use native `fetch` (not `$axios`) when the URL is RFC1918 / WG (`10.x`, `192.168.x`, …). `$axios` is screened and **blocks** those hosts. ## Create a new plugin 1. Pick an id: lowercase letters, numbers, hyphens; max 64 chars; `^[a-z0-9]+(?:-[a-z0-9]+)*$`. 2. Folder name **must equal** manifest `id`. Example: `plugins/joplin-api`. 3. Id **must not** collide with a core script basename (`fetch-http`, `ntfy`, …). 4. Create three files (see below). Prefer `main: "script.js"`. 5. Point workflows at `plugin/`. 6. Restart the runner (`pnpm dev` / drain-restart under `pnpm dev:pm2`) so the plugin is picked up. Copy the layout from `plugins/joplin-api`, `plugins/send-sms`, or `examples/plugins/get-current-time`. ### `plugins//jerapah-plugin.json` ```json { "id": "my-plugin", "name": "My plugin", "version": "0.1.0", "jerapah": ">=0.1.0 <1.0.0", "main": "script.js", "description": "One-line description" } ``` - `version` must be semver (`0.1.0`). - `jerapah` must match the app (`0.1.0` in root `package.json`). Use `">=0.1.0 <1.0.0"` unless you know otherwise. - `main` is a relative path; no `..`, not absolute. ### `plugins//package.json` ```json { "name": "jflow-plugin-my-plugin", "version": "0.1.0", "private": true, "type": "module", "description": "JerapahFlow plugin: …" } ``` Add `dependencies` only if the script `require()`s extra npm packages. Then: ```bash pnpm install --dir plugins/ --ignore-scripts --prefer-offline --ignore-workspace ``` `plugins/*/node_modules/` is gitignored. Host-allowlisted modules (`axios`, `jsonata`, …) come from the server; extra deps resolve from the plugin directory. ### `plugins//script.js` Must `export default` a function. The sandbox rewrites ESM `import`/`export default` to CJS. ```js function passContext(ctx) { if (ctx?.context != null && typeof ctx.context === "object" && !Array.isArray(ctx.context)) { return { ...ctx.context }; } return {}; } async function myPlugin(ctx) { const output = { ok: true }; return { output, context: { ...passContext(ctx), ...output } }; } myPlugin.meta = { description: "What this step does", previewConfigKey: "url", tags: ["HTTP"], config: {}, input: {}, output: { ok: { type: "boolean" } }, context: { ok: { type: "boolean" } }, example: { data: {}, config: {} }, }; export default myPlugin; ``` Return `{ output, context?, skipRemaining? }`. Do not return `ctx`. Mutations of `ctx.data` / `ctx.context` are discarded unless returned. | Field | Meaning | |---|---| | `ctx.data` | Step input (trigger payload, previous `output`, or DAG `needs`) | | `ctx.context` | Run clipboard (plain object) | | `ctx.config` | YAML `config` (mustache refs like `{{ secrets.name }}` already resolved) | | `output` | Next step’s `data` | | `context` | Next clipboard. Omit to keep incoming | `fn.meta` must be JSON-serializable (UI + dry-run). Include `config` / `input` / `output` field schemas and an `example`. ## Sandbox globals (do not import these) Injected: `log` (pino), `console`, `fetch`, `require`, `$axios`, `$kv`, `$fingerprint`, `$secrets`, `$vars`, `$responses`, `$workflows`. - Use `log.info({ … }, "my-plugin: …")` — `log` is not an import. - `require("axios")` is the screened `$axios` (RFC1918 blocked). Prefer `fetch` for LAN. - Plugin `require("some-npm-dep")` uses the plugin’s `node_modules`. ## Workflow YAML ```yaml scripts: - name: Notify to channel script: plugin/my-plugin config: url: http://10.8.0.6:3030/notes token: "{{ secrets.joplin_api_token }}" ``` Optional `name` is the display title in the editor and graph (falls back to the script filename). Canonical ref is `plugin/` (`.js` suffix is optional). ## Do not - Add user plugins under `packages/server/scripts/` or `examples/plugins/`. - Use an id that matches a core script file (`ntfy`, `jsonata`, …). - Mismatch folder name and `jerapah-plugin.json` `id` (plugin is disabled). - Commit `plugins/.staging-*` or `plugins/*/node_modules/`. - Put secrets in `script.js`; use YAML `{{ secrets.name }}` / `{{ vars.name }}` (quote if the value starts with `{`).