Skip to content

The FlowRunner CLI

@flowrunner/cli is the tool you author and deploy custom extensions with. It lives in your project as a development dependency rather than on your machine, so everyone working on the repository runs the same version and an upgrade is a commit like any other.

Installing it

For a new project, start with npx, which downloads the package and runs its init:

npx @flowrunner/cli@latest init -d my-extensions
cd my-extensions

For a repository you already have, add the package and set it up in place:

npm install --save-dev @flowrunner/cli
npx flowrunner init

Either way the CLI lives in the project's node_modules, and these pages run every command as npx flowrunner <command>, which finds that copy. It needs Node 20 or newer.

The CLI's own hints, such as the Next steps list init prints, use npx flowrunner <command>, or the bare flowrunner form once the command is installed globally (below).

Typing flowrunner without npx. Install the package once per machine:

npm install -g @flowrunner/cli

The global copy is only a launcher. Inside a project it finds the project's own copy and hands the command to it, so the version pinned in the project's package.json is the one that runs - a global 0.1.4 in a project pinned to 0.1.1 runs 0.1.1. A global 0.1.0 is the exception: it only recognises the package's old name, so update it once with npm install -g @flowrunner/cli. Outside a project, as with a first flowrunner init in an empty directory, it runs itself. init offers to install the global copy at the end of a new project.

To move to another CLI version, update the project's copy with npm install -D @flowrunner/cli@<version> and commit the change. Inside a project, a global copy that has fallen behind changes nothing.

Moving a project from flowrunner-cli. The package used to be called flowrunner-cli. The command is still flowrunner, and nothing in flowrunner.json, services/ or your deployed extensions changes. Swap the dependency:

npm uninstall flowrunner-cli
npm install -D @flowrunner/cli

Then change the one line in sandbox/service.js that names the package, or npm test fails with Cannot find module 'flowrunner-cli/runtime':

} = require('@flowrunner/cli/runtime')

If you use the Claude Code agents, run npx flowrunner init-claude again so they point at the new location of the authoring reference. A global install moves the same way: npm uninstall -g flowrunner-cli, then npm install -g @flowrunner/cli.

The project on disk

init produces this, and every later command reads it:

my-extensions/
  package.json             the project's own: the CLI and the test harness
  flowrunner.json          serverUrl, workspaceId, workspaceName - commit this
  .flowrunner/token        the deploy token - gitignored for you
  jsconfig.json            tells your code editor where FlowRunner's types are
  sandbox/                 the local test harness
  jest.config.js
  services/
    tmdb/                  one extension, and one npm package
      package.json         the service's OWN dependencies
      node_modules/        installed here, and shipped with the deploy
      src/index.js
      public/icon.svg

Each service is its own npm package. Run npm install inside services/<id>/, not at the project root - only services/<id>/node_modules is packed, so a dependency installed at the root resolves on your machine and then fails with MODULE_NOT_FOUND the first time the block runs.

Editing workspaceId by hand produces a 403 at deploy time, because the token is issued for one workspace. Use npx flowrunner login to change it.

The commands

Command What it does
init [template] Creates the project and optionally scaffolds a first service
create-service / cs [template] Scaffolds another service into services/
use [target] Points the project at a server; with no argument, lists them
login Authorizes the CLI in your browser and stores the deploy token
logout Revokes the token and deletes it locally
deploy Builds, packages and uploads services to the connected workspace
pull / sync-services Downloads the workspace's services into services/ so you can edit them
init-claude Installs the FlowRunner agents into .claude/ for Claude Code

Creating the project with init

init writes the project files, installs dependencies, and creates a git repository with a first commit:

Project directory:

  /Users/you/my-extensions
  a new directory

  + package.json
  + .gitignore
  + flowrunner.json
  + README.md
  + jsconfig.json
  + jest.config.js
  + jest.setup.js
  + sandbox/index.js
  + sandbox/service.js
  + sandbox/nock-mock.js
  + sandbox/files-mock.js

  Server: https://app.flowrunner.ai

It never overwrites. Run it inside a repository that already exists and it adds only what is missing, reporting everything it left alone:

  already an npm project — its package.json will be kept, with the CLI added as a devDependency
  already has services/
  already tracked by git — no repository will be created and nothing will be committed

    package.json — already there, left alone
    .gitignore — already there, left alone
    flowrunner.json — already there, left alone

It is also how a project created before a harness file existed picks one up. It does not refresh a harness file that is already there, so after upgrading the CLI copy the current sandbox/ over yours - see Testing.

Useful options:

  • -d, --dir <path> - the project root, created if missing. Defaults to the current directory.
  • -s, --server <target> - the server to bind to, written into flowrunner.json: prod for the FlowRunner cloud, or a full URL for a server of your own. Defaults to prod.
  • -y, --yes - accept the target directory and every template default without asking.
  • --no-install / --no-git / --no-tests - skip the npm install, the git repository, or the test harness.

If git cannot commit because user.name and user.email are unset, init warns and leaves the files staged rather than failing.

Scaffolding a service with cs

create-service, or cs, adds a service folder. --list shows what is available:

Available templates:

  demo-todo-list  Demo To-do List (default)
                  A working to-do list service — lists, tasks, completion, plus a generic HTTP Request action.
  blank           Blank
                  A minimal service — one action, ready to build on. Optionally with OAuth2 and file storage wired up.
  qa-test         QA Test Harness
                  Exercises every custom-extension capability so QA can verify a deployment end to end.

Pass the answers as flags to skip the prompts:

npx flowrunner cs blank --id tmdb --name "TMDB" -y
Creating a service from the "Blank" template.

  Created services/

  Created services/tmdb/
    package.json
    src/index.js
    README.md
    public/icon.svg

  Edit services/tmdb/src/index.js to make it yours.

Would you like to deploy "tmdb" to the workspace Acme Production? (Y/n) n

When you are ready:
  flowrunner deploy -s tmdb   — logs you in first if you have not already

Without flags, cs asks for the display name first and suggests the service id from it. Press Enter to accept the suggestion:

Display name (My Service): Token Generator
  Suggested from the display name — press Enter to accept it. Becomes the services/<id>/ folder and is frozen once deployed.
Service id (token-generator):

The service id becomes the folder name under services/ and the id saved into every flow that places one of the extension's blocks, which is why it is frozen once deployed. Check it before your first deploy. The suggestion is lowercase, drops accents, turns spaces and punctuation into single dashes, and drops leading digits (3D Printer becomes d-printer). If you type an id yourself, the CLI tidies it the same way and prints the result on the next line:

Service id (token-generator): My Cool_Tool!
  → my-cool-tool

If a folder under services/ already has the suggested id, the CLI suggests the next free one, such as token-generator-2. An id you type that is already taken is refused with Service id is already taken — services/token-generator exists in this project.

-y answers the template's own prompts - for blank, the description and the OAuth2 and Files API questions. The offer to deploy is separate: on a terminal it is asked either way and defaults to yes, and without a terminal it is skipped.

With flags, --name alone also sets the id: npx flowrunner cs blank --name "Token Generator" -y creates services/token-generator. An id given with --id is used exactly as typed, so one that breaks the rule (a lowercase letter first, then only lowercase letters, digits and dashes) is refused:

  Service id must start with a lowercase letter and contain only lowercase letters, digits and dashes —
  it becomes the folder name and the service id, which is frozen once deployed

Choosing a server with use

With no argument, use lists the servers the CLI knows and marks the one this project is bound to. The listing is trimmed here to the row that applies to a FlowRunner cloud workspace:

Servers:

  prod   https://app.flowrunner.ai   ← current, logged in to Acme Production

  flowrunner use <name>   — switch, detaching the current login

prod is the FlowRunner cloud. You can also pass a full URL for a server of your own.

Switching servers clears the stored workspace and the token, because both only mean something on the server that issued them. Running use with the server already selected changes nothing.

Connecting to a workspace with login

login starts a callback server on a local port, opens your browser, and waits:

  Connecting this project to a Flowrunner workspace on https://app.flowrunner.ai.

  Authorize the CLI in your browser and pick the workspace to deploy to:

    https://app.flowrunner.ai/developer/cli/authorize?callback=http%3A%2F%2Flocalhost%3A56888%2Fcallback&clientType=cloud-code-deploy

  Waiting for authorization...

The browser round trip times out after two minutes, and the URL is printed so you can open it by hand if no browser launches.

On the authorize page you pick the workspace this project will deploy to. Authorize stays disabled until one is chosen, and the token it issues expires after 30 days.

The workspace list is sorted by name. With more than five workspaces it opens with a Recent group, the three you opened last in the console in the same browser, followed by All workspaces, and typing in its search box shows one list of matches.

The Authorize Cloud Code CLI page with the signed-in account at the top, the Workspace picker showing the selected workspace and its id, the Deploy Custom extensions permission, and the Deny and Authorize buttons above a note that the token expires in 30 days

The choice is written back to flowrunner.json:

{
  "serverUrl": "https://app.flowrunner.ai",
  "workspaceId": "3F2A9C41-7B10-4E52-9D64-0C81A7F5B2E3",
  "workspaceName": "Acme Production"
}

Commit that file. The token goes to .flowrunner/token, which init has already gitignored.

Once a workspaceId is recorded, later logins show that workspace already selected, and Authorize is enabled straight away. To move the project somewhere else, choose Use another workspace and the full list comes back.

The authorize page on a later login, with the connected workspace shown as a fixed entry, a Use another workspace link beneath it, and the Authorize button enabled

logout revokes the token on the server and deletes the local copy. It leaves workspaceId in place, so logging in again returns you to the same workspace.

Deploying

A deploy carries one service at a time, so several projects, repositories or people can deploy into the same workspace and coexist.

With one service in the project, deploy sends it. With several it asks which:

Which services should be deployed?

❯ All services [tmdb, invoicing]
  tmdb (TMDB) — modified
  invoicing (Invoicing) — new

The output says what actually changed:

Deploying custom extensions...

  Connecting to Flowrunner Prod cluster (https://app.flowrunner.ai)
  Workspace "Acme Production" (3F2A9C41-7B10-4E52-9D64-0C81A7F5B2E3)
  Found 1 service under services/.

  Deploying 1 service:
    - modified service: tmdb (TMDB) - [1258d5f80c44] - packaged 11.4 KB

  Deployed 1 service to workspace "Acme Production" (3F2A9C41-7B10-4E52-9D64-0C81A7F5B2E3) — you can use it in a flow.
  Open Custom Extensions screen in the Flowrunner Console to configure it: https://app.flowrunner.ai/app/Acme%20Production/custom-extensions

Every service is compared against the workspace before anything is packaged, and labelled new, modified or same. A same service matches what is already live, so it is skipped rather than republished. The twelve-character hash is the version id: it covers every file under services/<id>/ except node_modules/, which is why the same code produces the same version in anybody's checkout. A change made only inside node_modules/ keeps the hash, so deploy that with --force.

The definition is built on your machine rather than on the server, so a service whose module throws on load fails at deploy time rather than silently in the workspace afterwards. One that fails to build is skipped with its reason and the rest still deploy; if nothing builds, the deploy stops and reports every reason.

  • -s, --service <id...> - deploy named services, repeatable and variadic.
  • --all - every service, though still only the changed ones are uploaded.
  • --force - upload even a service whose hash matches what the workspace already has.

The uploaded archive is capped at 50 MB, and it carries the service's node_modules, so a service with heavy dependencies can reach it.

Service ids are unique per workspace. Two projects that both define tmdb overwrite each other's versions with no warning, since nothing records which project a version came from. Agree ids across teams before two repositories deploy into one workspace. The id is both the folder name under services/ and the id in createExtension, and the two must match. It is also frozen once deployed: renaming both deploys a different extension and leaves the old one live under its old id, and renaming only the folder makes the deploy skip the service with its entry point does not define a service.

A failed --all is not rolled back

Services deploy one after another and independently. If the third fails, the first two stay deployed. Re-running is safe, because an unchanged service produces the same version and is skipped.

Removing a service

Deleting services/<id>/ and deploying again does not remove the extension. The deploy only touches what it carries, so the service stays live in the workspace on its last deployed version.

To remove one, open it under Custom Extensions in the console and click Delete extension, the trash icon at the top. It asks to confirm, and it removes the service and every version of it - there is no undo and no rollback afterwards.

Remove the blocks from your flows first

Nothing checks which flows use an extension before deleting it; the confirmation only says that flows that use it will stop working. Any flow still holding one of its blocks is left referring to something that no longer exists. Take the extension's blocks out of every flow that uses them before you delete it.

The TMDB extension page: the breadcrumb with the extension and its version, the Reload extension, Custom extensions documentation and Delete extension icons (the pointer on Delete extension, with its tooltip), the header with the name, Active, the id tmdb, the version and 1 action, and the Methods, Configuration, OAuth, Versions, Activity and Develop tabs

Pulling services back down

pull (also sync-services) copies the workspace's services into services/. It is how a fresh checkout picks up what is live, including extensions somebody else deployed from a different project. In a fresh checkout, install the project's packages first, because npx flowrunner runs the copy in node_modules:

npm install && npx flowrunner login && npx flowrunner pull --all
cd services/tmdb && npm install

To pull into a new project instead, create it with npx @flowrunner/cli@latest init -d my-extensions and run login and pull inside it.

Each service arrives whole, its own package.json included, but without node_modules - the CLI names the folders that need an npm install rather than running one for you.

pull takes the same -s and --all as deploy. Its --force behaves the opposite way round, on purpose: a local copy that differs is skipped unless you force it, and a file you added counts as "differs", so a pull cannot quietly delete your work.

Running without a terminal

Every command works non-interactively. Prompts appear only on a TTY, and a question that cannot be answered becomes an error that names the flag that answers it, rather than hanging. -y accepts every default, and --id, --name and --set name=value answer template prompts directly, which is what a CI job uses.

When a command fails it prints the reason and what to do about it, and exits with status 1:

It prints What to do
Deploying needs a connection to a Flowrunner workspace, and this project has none yet. Run npx flowrunner login - it needs a browser
This project has a login but no workspace attached Run npx flowrunner login to pick one
Session expired. Log in again and repeat the command
There is no flowrunner.json here, so this is not a Flowrunner project. Run npx flowrunner init to create one
No services/ directory in this project. Run npx flowrunner cs to create a service first
No services found under services/. Each service needs a src/index.js (or dist/index.js) calling Flowrunner.createExtension
Pass --yes to accept the defaults, or supply the values as flags. Pass --yes, or supply the value as a flag

Working with Claude Code

init-claude installs the FlowRunner agents into the project and adds a block to CLAUDE.md, so an assistant working in the project already knows the authoring format:

Agents in .claude/agents:

  + flowrunner-service-code-engineer.md
  + flowrunner-service-test-engineer.md

  Created the FlowRunner instructions block in CLAUDE.md.

Files under .claude/agents/ named flowrunner-* belong to the CLI and are overwritten on every run, so npm install -D @flowrunner/cli@latest followed by npx flowrunner init-claude moves the agents to the version matching the installed CLI. Your own agents, under your own names, are left alone. The command replaces only the region between its and markers and leaves the rest of CLAUDE.md byte for byte.

What the agents do with all that, and how to work with them, is Quick Start: Your First Extension (with AI).