skip to content
$registry / executables

registry-cli

(alias: workers-cli)v0.3.0

Command-line tool to publish workers and kits to the iii registry (workers.iii.dev).

executable
iiiverified
106 installs29 in 7d0 today
install
$curl -fsSL https://workers.iii.dev/install/registry-cli/0.3.0.sh | sh
executablelicense: Apache-2.0clipublishregistryreleasekits
  • macOS: arm64 · x64
  • Linux: arm64 · armv7 · x64
  • Windows: arm64 · x64

exact versions are immutable; the install script pins every archive url and verifies its sha-256 before installing.

standalone executable
installed on the host by the script above (default ~/.local/bin, override with INSTALL_DIR); not a worker, so it cannot be added to a compose graph.
README.md

workers-cli

workers is the command-line tool for publishing workers to the iii worker registry. Run it in a worker project: it uploads the artifacts you have already built, registers a new immutable version with its README, metadata, skills and API reference (the functions and triggers the worker registers, read from a running iii engine), and makes it installable by anyone running iii. It never compiles, packages or archives anything for you.

workers login

# a worker is published from a running iii engine it is connected to (see "Connect the worker to a running engine")
iii &                                   # the engine, on ws://localhost:49134
III_WORKER_NAME=bridge ./bridge &       # the worker, connected to it under its registry name

# binary worker: a folder of pre-built <bin>-<target-triple>.tar.gz archives
workers publish --artifacts ./dist --skills ./skills

# Python or Node.js worker (bundle): one .tar.gz you built, with iii.worker.yaml at its root
workers publish --artifacts ./build/hello.tar.gz --skills ./skills

Publishing a worker needs a running iii engine with the worker connected to it. The functions and triggers on the worker's API reference tab, with their schemas, are read from that engine: without it, or with the worker not connected, workers publish stops before uploading anything. See Connect the worker to a running engine. Executables need no engine.

Publishing a Python or Node.js worker? Read Python and Node.js workers first: the archive must contain iii.worker.yaml and everything the worker needs at runtime.

Install

curl -fsSL https://workers.iii.dev/install/workers-cli/latest.sh | sh

The installer detects your OS and CPU architecture, downloads the matching archive, verifies its SHA-256 checksum and places the workers binary in ~/.local/bin. If that directory is not on your PATH, it tells you how to add it.

  • Custom location: set INSTALL_DIR, e.g. curl -fsSL … | INSTALL_DIR="$HOME/bin" sh.
  • Pin a version: replace latest with a release tag (next) or an exact version, e.g. https://workers.iii.dev/install/workers-cli/0.1.0.sh.

Supported platforms:

OS Targets
macOS aarch64-apple-darwin (Apple Silicon), x86_64-apple-darwin (Intel)
Linux x86_64-unknown-linux-musl (static, any distribution), x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, armv7-unknown-linux-gnueabihf
Windows x86_64-pc-windows-msvc, aarch64-pc-windows-msvc (run the installer from Git Bash, MSYS2 or Cygwin)

Check the installation with workers --version. Run workers --help or workers publish --help for the built-in reference. What changed in each version: the changelog, also published as the GitHub release of every cli-v* tag.

Build from source

With a stable Rust toolchain, from the CLI's source directory:

cargo install --path .    # installs `workers` into ~/.cargo/bin

How it works

The registry hosts your files on its own CDN and records their SHA-256 digests. Every workers publish run goes through the same steps:

  1. Reads your project. iii.worker.yaml provides the worker's name, type and metadata; README.md becomes the worker's page; the version comes from --version or from your language manifest.
  2. Checks the artifacts you built, and computes the SHA-256 and size of each one locally, before anything leaves your machine. For a binary worker these are the per-target archives found in the --artifacts folder; for a bundle, the .tar.gz file itself, which the CLI opens to check that iii will be able to install and start it (see What workers checks). Nothing is built or archived for you.
  3. Reads the API reference from a running iii engine: every function the worker registers and every trigger type it provides, with their schemas. The worker must be connected to that engine; otherwise the run stops here, before anything is uploaded (see Connect the worker to a running engine).
  4. Uploads the artifacts to the registry, which serves them from its own CDN (cdn.iii.cloud).
  5. Publishes the version. The registry records the name, version, release tag, README, metadata, API reference and, for each artifact, its URL and digest.
  6. Attaches your skills (optional): Markdown documentation for that version, sent right after the version is published.

Once published, the worker appears on workers.iii.dev and anyone can add it to their project:

iii trigger compose::add worker=<name>

Versions are immutable. Re-running a publish is always safe: if the version already exists with identical artifacts, the run is a no-op (already published with identical contents); if the artifacts differ, the CLI stops and asks you to bump the version. A CI job that failed halfway can simply be retried (with the engine and the worker running again: step 3 runs on every attempt).

Authenticate

workers login

Prompts for your registry API key (input is hidden), checks it with the registry and prints whose it is, then saves it to ~/.iii/workers-api-key (%USERPROFILE%\.iii\workers-api-key on Windows), with 0600 permissions on macOS and Linux:

$ workers login
Registry API key:
Logged in as acme (key "laptop")
Saved to /home/me/.iii/workers-api-key

A mistyped, revoked or expired key fails right there and is not saved, and so does a key that cannot be checked because the registry is unreachable. workers whoami tells you, at any time, who the key publish would use belongs to and where it comes from (WORKERS_API_KEY or the saved file).

Without a terminal (a script, docker run without -t, an agent), pipe the key in instead. workers login reads the first line of stdin whenever stdin is not a terminal; --stdin asks for that explicitly:

printf '%s\n' "$KEY" | workers login --stdin

In CI, skip login and set WORKERS_API_KEY instead; it takes precedence over the saved key. The key is only needed to talk to the registry, so --dry-run (which still needs the running engine) and workers check work without it.

Prepare your worker

A typical project looks like this:

my-worker/
├── iii.worker.yaml     # worker manifest
├── Cargo.toml          # or package.json / pyproject.toml, provides the version
├── README.md           # shown on the worker's registry page
├── skills/             # optional Markdown skills
│   └── SKILL.md
└── dist/               # binary/executable: the pre-built per-target archives

Bundles (Python and Node.js) are laid out differently: see Python and Node.js workers.

iii.worker.yaml

iii: v1
name: bridge                 # worker name on the registry
language: rust
deploy: binary               # binary | bundle | executable
manifest: Cargo.toml         # file the version is read from
bin: bridge                  # artifact file name prefix (defaults to `name`)
description: Bridge two engines.
license: Apache-2.0
repo: https://github.com/acme/bridge
tags: [bridge, remote]
experimental: false
config:                      # default configuration, shown on the worker's page
  url: ws://0.0.0.0:49134
dependencies:                # other workers, by semver range or release tag
  configuration: "latest"
Field Required Description
name yes The worker's name on the registry.
deploy yes binary, bundle or executable; see Artifacts.
manifest no Language manifest, next to iii.worker.yaml, that holds the version: Cargo.toml, package.json or pyproject.toml.
bin no Prefix of the binary artifact file names. Defaults to name.
repo no Link to the source repository. When omitted, it is derived from --github-repo if given.
tags no Search tags. When omitted, the tags already set on the registry are kept.
description, license, config, dependencies, experimental no Sent to the registry as-is.
scripts, runtime, env, resources bundles: scripts.start Not sent to the registry: iii reads them from the copy of iii.worker.yaml inside a bundle archive. See the fields iii reads.

Version

The version is resolved in this order:

  1. --version X.Y.Z;
  2. the file named by manifest:: the version field of a JSON file (package.json), or [package].version / [project].version of a TOML file (Cargo.toml, pyproject.toml).

If neither is available, publish stops and asks for --version.

Artifacts

--artifacts is required, and what it points to depends on deploy:

deploy --artifacts points to What gets published
binary a folder One archive per platform, named -.tar.gz (or .zip, typically for Windows). Files that don't follow this pattern are ignored.
bundle a .tar.gz file you built, never a folder A single archive for every platform, e.g. a Node.js or Python worker. It must hold iii.worker.yaml at its root plus everything the worker needs at runtime; a folder is refused, not archived. See What goes in the archive.
executable a folder Same rules as binary. For standalone programs, like this CLI, that users install with curl -fsSL https://workers.iii.dev/install//latest.sh | sh instead of adding them to a project.

Example dist/ folder for a binary worker named bridge:

dist/
├── bridge-aarch64-apple-darwin.tar.gz
├── bridge-x86_64-apple-darwin.tar.gz
├── bridge-x86_64-unknown-linux-gnu.tar.gz
└── bridge-x86_64-pc-windows-msvc.zip

Skills

--skills points to a folder of Markdown files, read recursively. Only non-empty .md files are published. SKILL.md at the root is the overview; every other file keeps its relative path under skills/:

Local file Published as
skills/SKILL.md SKILL.md
skills/guides/setup.md skills/guides/setup.md

publish (and --dry-run) lists every file before anything is uploaded, including what it leaves out and why:

skill:     SKILL.md  (from skills/SKILL.md, 1 link adjusted to the published layout)
skill:     skills/guides/setup.md  (from skills/guides/setup.md)
skipped:   skills/empty.md (empty)
skipped:   skills/diagram.png (not a Markdown .md file)

and ends with the paths it published (Published 2 skill file(s) to bridge@1.2.0: SKILL.md, skills/guides/setup.md). A folder with nothing to publish, or a file name the registry would refuse (only letters, digits, ., _, - and /), stops the run before anything is uploaded.

Write links for the folder as it is on disk: [setup](guides/setup.md) in skills/SKILL.md, [overview](../SKILL.md) in skills/guides/setup.md. SKILL.md moves up one level when published, so workers rewrites relative links between skill files to keep them working on the registry (the two above are published as skills/guides/setup.md and ../../SKILL.md). URLs, anchors and links already written for the published layout are left alone. A relative link to something that is not published (an image, a folder, a file outside --skills) is reported as a warning, because it is broken once published.

Connect the worker to a running engine

The worker page's API reference tab lists every function the worker registers, with its request and response schemas, and every trigger type it provides, with what a trigger is configured with and what the bound function receives. None of that is in the artifacts: the worker registers it with the engine when it connects. So workers publish reads it from a running iii engine that the worker is connected to, and a binary or bundle worker is never published without it. Executables are not workers and skip this step.

  1. Start an engine. iii runs one with the config.yaml of the current folder (creating an empty one when there is none) and listens on ws://localhost:49134. Install it with curl -fsSL https://install.iii.dev/iii/main/install.sh | sh.

  2. Start the worker you are publishing, connected to that engine, under its registry name. The engine knows a worker by the name it registers with, and workers looks for the name: of iii.worker.yaml. The iii SDKs (Rust, Node.js, Python) register under III_WORKER_NAME when it is set, and iii sets it for the workers it starts itself (iii worker add ./my-worker). To run the worker yourself, the way you do in development:

    III_WORKER_NAME=bridge ./target/release/bridge     # binary worker
    III_WORKER_NAME=hello node dist/index.js           # Node.js bundle
  3. Publish, or preview with --dry-run, which reads the engine too:

    $ workers publish --artifacts ./dist --dry-run
    worker:    bridge (binary)
    version:   1.2.0 (tag: latest)
    hosting:   https://api.workers.iii.dev (workers/bridge/1.2.0/)
    artifact:  bridge-x86_64-unknown-linux-gnu.tar.gz  sha256:…  (… bytes)
    engine:    ws://localhost:49134 (2 function(s), 1 trigger type(s))
    function:  bridge::send
    function:  bridge::status
    trigger:   bridge::on-message

What ends up on the API reference tab:

Entry Read from the engine
Each function: name, description, request and response schemas, metadata engine::workers::info lists the worker's functions and engine::functions::info gives their schemas. The name is the function id, or metadata.registry_name / metadata.name when the worker sets one.
Each trigger type: name, description, invocation and return schemas engine::workers::info lists the worker's trigger types and engine::triggers::info gives their schemas: the configuration a trigger takes (shown as the invocation schema) and what the bound function receives (shown as the return schema).
  • --engine ws://host:port (or III_URL) points at another engine. Only ws:// works: iii engines do not serve wss://.
  • --engine-wait waits for the engine to come up and for the worker to connect and finish registering (it must report the same functions and triggers twice in a row, a second apart). Use it in CI, where both start in the background right before publish.
  • A function without a typed request or response schema, or a trigger type without a typed configuration schema, is published anyway and shown as unknown; one warning: line lists the functions, another the trigger types. Register handlers with typed formats (in Rust, a struct deriving schemars::JsonSchema) to fix it.
  • The run stops, before anything is uploaded, when the engine cannot be reached, the worker is not connected (the error lists the workers that are), it registered no function and no trigger type, two functions would publish under the same name, or a trigger type's name is not one the registry accepts (lowercase letters, digits and -, optionally namespaced with : or ::).
  • The engine is only read: workers calls engine::workers::info, engine::workers::list, engine::functions::info and engine::triggers::info, nothing else. On an engine with RBAC, those must be exposed to the connection.

Publish

Where the artifacts are hosted

workers publish --artifacts ./dist --skills ./skills   # bundle: --artifacts ./build/hello.tar.gz

The files are always uploaded straight to the registry and served from https://cdn.iii.cloud/workers///. Only your API key is needed.

  • The CLI opens a release for the version, uploads each artifact to a short-lived signed URL and then publishes. Transient upload failures are retried automatically, and an interrupted run resumes the same release the next time.
  • Nothing becomes public until the publish succeeds: the registry first checks the presence, size and digest of every upload.
  • Binary archives keep their - names; a bundle is always published as -.tar.gz, whatever its local file name.
  • --github-repo is optional; when given, it only fills the repo link if iii.worker.yaml has none.

Release tags

By default, a version is published under the latest tag: it is stable, installed by default and matched by semver ranges such as ^1.0.0. Any other tag (next, beta, rc, …) publishes a candidate, which is only installed when requested explicitly by tag or exact version; semver ranges ignore it.

workers publish --artifacts ./dist --tag next

# users opt in explicitly:
iii trigger compose::add worker=my-worker@next

The worker page always shows the stable version, so for a candidate publish prints the page link pinned to it (https://workers.iii.dev/workers/my-worker?version=1.1.0) and how to promote it.

Promote, move or roll back a tag

Tags are pointers: move them to any version you already published, without publishing again.

workers tag acts on one worker, found the same way publish finds it: name: in the iii.worker.yaml of the current folder. Run it from the worker's folder (or the CI job that published it), or name the worker with --name from anywhere else. Every line it prints names the worker, so you can always see which one changed.

From the worker's folder, where iii.worker.yaml says name: my-worker:

workers tag ls                                   # latest  my-worker@1.0.0 / next  my-worker@1.1.0
workers tag promote                              # latest -> the version `next` points to (1.1.0)
workers tag set next 1.2.0                       # point any tag at an existing version
workers tag set latest 1.0.0 --expect-current 1.1.0   # roll back, only if latest is still 1.1.0

From anywhere else, the same commands with the worker named:

workers tag ls --name my-worker
workers tag promote --name my-worker
workers tag set next 1.2.0 --name my-worker
workers tag set latest 1.0.0 --expect-current 1.1.0 --name my-worker

Without --name and without an iii.worker.yaml, the command stops before doing anything (no iii.worker.yaml to read the worker name from; pass --name). promote fails instead of overwriting when latest or next moved in the meantime. Moving a tag needs the same API key as publishing.

Preview with --dry-run

--dry-run prints the worker, version, hosting target, every artifact with its digest and size, every skill file (published or skipped), the functions and triggers read from the engine, and the exact JSON payload that would be sent, without uploading or publishing anything and without an API key. It reads the engine like a real publish, so it needs the engine running with the worker connected (Connect the worker to a running engine). Use it to check the manifest, the version, the artifact names, the skills and the API reference before spending a version number. For a bundle it also runs the archive checks, so an archive iii could not install fails here instead of after it is published.

workers publish --artifacts ./dist --dry-run                # binary
workers publish --artifacts ./build/hello.tar.gz --dry-run  # bundle

Flags

Flag Env Default Description
--manifest iii.worker.yaml Worker manifest.
--readme README.md Markdown shown on the worker's page. Must exist.
--skills Folder of Markdown skills.
--artifacts required Folder of pre-built archives (binary, executable) or the .tar.gz file you built (bundle, with iii.worker.yaml at its root). Nothing is archived for you.
--github-repo GITHUB_REPOSITORY Repository linked as the worker's repo when iii.worker.yaml has none. Metadata only.
--engine III_URL ws://localhost:49134 The running iii engine the worker is connected to; its functions and triggers are read from it. Required for binary and bundle workers; see Connect the worker to a running engine.
--engine-wait 0 (ask once) Wait this long for the engine to answer and the worker to connect and register its functions and triggers.
--version read from manifest: Version to publish.
--tag latest Release tag. Anything other than latest publishes a candidate.
--registry WORKERS_REGISTRY_URL https://api.workers.iii.dev Registry API base URL.
--dry-run Print the payload; upload and publish nothing. Still reads the engine.

--engine and --engine-wait are refused with an executable, which needs no engine.

Python and Node.js workers (bundles)

Python and Node.js workers are published with deploy: bundle. Instead of one archive per platform, a bundle is a single .tar.gz that serves every platform. Login, hosting, release tags and --dry-run work as described above; this section covers everything that is different.

workers never builds or archives anything. --artifacts must be a .tar.gz file you built yourself: a folder such as ./dist is refused, not archived for you. The CLI only opens the archive to check it (see What workers checks), then uploads it byte for byte.

What goes in the archive

The archive is the worker as iii installs it. iii trigger compose::add worker= downloads it, extracts it into a folder and starts the worker in a sandbox, with that folder as the working directory (/workspace), by running the scripts.start command of the iii.worker.yaml it finds there. Nothing from your project reaches the sandbox unless it is inside the archive.

The archive must contain, at its root (not under a folder such as dist/):

Entry Required Why
iii.worker.yaml yes iii reads it from the extracted archive to know how to start the worker. Without it, the install fails with the archive for '' held no executable. Use the same file you publish with; see the fields iii reads.
The code scripts.start runs yes For example dist/index.js or hello/__main__.py. Paths in scripts.start are relative to the archive root.
Dependencies yes, unless scripts.install installs them or your code is bundled into a single file iii does not run npm install or pip install for you. For Node.js, ship the production node_modules/.
package.json, pyproject.toml, requirements.txt, … when the code or scripts.install needs them For example, "type": "module" in package.json changes how Node.js loads .js files.

A Node.js worker named hello, as it should look inside hello.tar.gz:

./
├── iii.worker.yaml      # name: hello, scripts.start: node dist/index.js
├── package.json
├── dist/
│   ├── index.js
│   └── greet.js
└── node_modules/        # production dependencies (leave out if dist/ is a single bundled file)

An archive of the build folder, tar -czf hello.tar.gz dist/, only holds dist/index.js, dist/greet.js, …: no iii.worker.yaml at its root and no dependencies. iii can never install it, so workers refuses to publish it.

Things the archive should not contain: README.md and skills/ (they are published separately), tests, .git/, development dependencies, and anything built for your own machine, such as a Python virtualenv (.venv/) or __pycache__/ folders: they do not work inside iii's Linux sandbox.

1. Lay out the project

hello/
├── iii.worker.yaml     # deploy: bundle + scripts.start; published AND copied into the archive
├── package.json        # or pyproject.toml; holds the version
├── README.md           # shown on the worker's registry page (not part of the archive)
├── skills/             # optional Markdown skills (not part of the archive)
├── dist/               # your build output
├── out/                # staging folder: exactly what goes in the archive
└── build/
    └── hello.tar.gz    # the bundle: --artifacts points at this file

2. Declare the worker and how it starts

The same iii.worker.yaml is read twice: by workers when you publish (name, version, metadata), and by iii, from inside the archive, when someone installs the worker (how to start it). Keep a single file and copy it into the archive.

Node.js:

iii: v1
name: hello
language: node
deploy: bundle
manifest: package.json      # version read from its "version" field
description: Say hello.
license: Apache-2.0
scripts:
  install: ""               # dependencies ship in the archive: nothing to install
  start: node dist/index.js # run from the archive root

Python:

iii: v1
name: hello
language: python
deploy: bundle
manifest: pyproject.toml    # version read from [project].version
description: Say hello.
license: Apache-2.0
runtime:
  base_image: docker.io/iiidev/python:latest  # required: without it the worker runs on the Node.js image
scripts:
  install: pip install -r requirements.txt    # runs once, inside the sandbox, before the first start
  start: python -m hello

Fields iii reads from the manifest inside the archive (workers does not send them to the registry):

Field Required Description
name yes Exactly the worker's name on the registry; otherwise iii refuses to install it.
scripts.start yes Shell command that starts the worker, run from the archive root.
scripts.install no Shell command run once, inside the sandbox, before the first start. Use it for what cannot ship in the archive, such as Python packages with native extensions. When omitted, iii prints a warning at every start; set it to "" when the dependencies are in the archive.
scripts.setup not allowed Refused in bundles: ship the archive ready to run.
runtime.base_image no OCI image the sandbox runs on. Defaults to docker.io/iiidev/node:latest; Python workers need docker.io/iiidev/python:latest (or another image with Python). language: does not select the image.
env no Extra environment variables for the worker. III_URL and III_ENGINE_URL are reserved and ignored.
resources.cpus, resources.memory no CPUs and memory (MB) for the sandbox. Defaults: 2 and 2048; capped at 4 and 4096.

The manifest must be a regular file (not a link) of at most 64 KiB.

  • bin: is not used by bundles.
  • Only a literal version string is read: version in package.json, [project].version (or [package].version) in pyproject.toml. Poetry's [tool.poetry].version and dynamic = ["version"] are not read: add a [project].version or pass --version.

3. Build the archive

Build it in your own build step: put exactly what the worker needs at runtime in a staging folder, then archive the folder's contents with -C ., so that they land at the archive root.

Node.js:

npm ci && npm run build                 # produces dist/
rm -rf out && mkdir -p out build
cp iii.worker.yaml package.json package-lock.json out/
cp -R dist out/dist
(cd out && npm ci --omit=dev)           # production node_modules/; skip if dist/ is a single bundled file
COPYFILE_DISABLE=1 tar -czf build/hello.tar.gz -C out .

Python:

rm -rf out && mkdir -p out build
cp iii.worker.yaml pyproject.toml requirements.txt out/
cp -R hello out/hello                   # the package `scripts.start` runs
COPYFILE_DISABLE=1 tar -czf build/hello.tar.gz -C out .

If the worker folder holds nothing else, you can archive it directly instead of staging. Write the archive outside the folder, so it does not end up inside itself, and exclude what only works on your machine:

# from the worker folder, the one holding iii.worker.yaml
COPYFILE_DISABLE=1 tar -czf ../hello.tar.gz --exclude .venv --exclude __pycache__ --exclude .git --exclude '*.tar.gz' .

Check the result before publishing: iii.worker.yaml must be at the root, not under a folder. workers check build/hello.tar.gz runs the same checks as publish (see What workers checks).

$ tar -tzf build/hello.tar.gz
./
./iii.worker.yaml
./package.json
./dist/index.js
./dist/greet.js
./node_modules/…

A quick smoke test is to extract it into an empty folder and run scripts.start from there: a file missing from the archive shows up immediately.

mkdir -p /tmp/hello-check && tar -xzf build/hello.tar.gz -C /tmp/hello-check
cd /tmp/hello-check && node dist/index.js

Keep in mind:

  • tar -czf x.tar.gz dist/ is wrong: it archives the folder itself (every path starts with dist/) and leaves the manifest out. Archive a folder's contents (-C out ., or . from inside the worker folder), with iii.worker.yaml among them.
  • One archive runs everywhere iii runs: inside a Linux sandbox, on x86_64 or arm64 depending on the machine. Pure JavaScript and pure Python dependencies can ship as they are; native modules built on your machine (Node.js .node addons, Python wheels with C extensions) will not load there. For Python, install them with scripts.install; for Node.js, prefer dependencies without native addons.
  • COPYFILE_DISABLE=1 stops macOS tar from adding ._* metadata files to the archive; elsewhere it does nothing.

What workers checks

Before uploading anything, and also with --dry-run, workers opens the archive and refuses it when iii would refuse to install or start it:

  • --artifacts must be a file whose name ends in .tar.gz. Folders are refused (they are never archived for you), and so are .tgz and .zip.
  • The file must be a valid gzip-compressed tar archive.
  • It must have an iii.worker.yaml at its root: a regular file of at most 64 KiB, in valid YAML. When the manifest is missing, the error says what the archive holds instead (for example, everything under dist/) and how to fix it.
  • The manifest's name must be the name being published, scripts.start must be set, scripts.setup must not be, and runtime.base_image, if set, must be a valid image reference.
  • A Python worker (language: python, or manifest: pyproject.toml) must set runtime.base_image: without it, iii runs it on the Node.js image and it exits with python: command not found.

It also warns, without refusing the archive, about what was built for or by your own machine: Python virtualenvs (.venv/, or any folder holding pyvenv.cfg), __pycache__/, .git/ and macOS metadata (._* AppleDouble files, .DS_Store).

workers does not run scripts.start or check that the files and dependencies it needs are in the archive: use the smoke test above for that.

To run these checks on an archive alone, without a README, a version or an API key, use workers check:

workers check build/hello.tar.gz                 # the worker name comes from ./iii.worker.yaml
workers check hello-0.3.0.tar.gz --name hello    # or give it

The local file name is free: the archive is always published as -.tar.gz (hello-0.3.0.tar.gz).

4. Publish

Start the engine and the worker first, as in Connect the worker to a running engine: the worker's functions and triggers are read from it.

iii &                                        # the engine, on ws://localhost:49134
III_WORKER_NAME=hello node dist/index.js &   # the worker, connected to it as `hello`

# check the archive, the name, the version, the digest and the API reference first (no API key needed)
workers publish --artifacts ./build/hello.tar.gz --dry-run

# served from https://cdn.iii.cloud/workers/hello/0.3.0/hello-0.3.0.tar.gz
workers publish --artifacts ./build/hello.tar.gz --skills ./skills

Once published, it installs like any other worker: iii trigger compose::add worker=hello. publish ends with the worker page's URL. Pages are cached, so the new version can take about a minute to show there; iii can install it right away. A candidate (--tag next, …) never becomes the page's version: the link printed pins it with ?version=.

In GitHub Actions

- name: Build the bundle
  run: |
    npm ci && npm run build
    mkdir -p out build
    cp iii.worker.yaml package.json package-lock.json out/
    cp -R dist out/dist
    (cd out && npm ci --omit=dev)
    tar -czf build/hello.tar.gz -C out .
- name: Publish to the iii registry
  env:
    WORKERS_API_KEY: ${{ secrets.WORKERS_API_KEY }}
  run: |
    curl -fsSL https://install.iii.dev/iii/main/install.sh | sh
    curl -fsSL https://workers.iii.dev/install/workers-cli/latest.sh | sh
    export PATH="$HOME/.local/bin:$PATH"
    iii --no-update-check &                                  # the engine the API reference is read from
    (cd out && III_WORKER_NAME=hello node dist/index.js) &   # the worker, as it ships, connected to it
    workers publish --artifacts build/hello.tar.gz --skills skills --engine-wait 60

Common bundle errors

Message Fix
bundle workers need --artifacts to be the .tar.gz archive, and ./dist is a folder: … workers does not archive folders. Build the archive as in step 3 and pass the .tar.gz file.
./build/hello.tar.gz does not exist: bundle workers need --artifacts to be the .tar.gz archive … The archive was not built yet, or the path is wrong. Build it as in step 3 and point at it.
… is not a .tar.gz file: bundle workers need --artifacts to be the .tar.gz archive … --artifacts is a .tgz, a .zip or an uncompressed .tar. Build a .tar.gz and point at it.
… is not a valid .tar.gz archive The file is not a gzip-compressed tar (e.g. a renamed .zip or an uncompressed .tar). Rebuild it with tar -czf.
… has no iii.worker.yaml at its root, so iii cannot install it: everything in it is under `dist/` … The build folder was archived instead of a staging folder with the manifest. Stage as in step 3 and archive with tar -czf … -C out ..
… it has `out/iii.worker.yaml`, but iii only reads the one at the archive root The staging folder was archived as a folder (tar -czf x.tar.gz out). Archive its contents: tar -czf x.tar.gz -C out ..
iii.worker.yaml inside … is named `x`, but the worker being published is `y` … The archived manifest is stale or belongs to another worker. Copy the current iii.worker.yaml into the staging folder.
iii.worker.yaml inside … has no `scripts.start` … Add scripts.start to iii.worker.yaml (e.g. python src/main.py for a Python worker, node dist/index.js for Node.js) and rebuild the archive.
iii.worker.yaml inside … declares `scripts.setup` … Remove it: ship dependencies in the archive, or install them with scripts.install.
iii.worker.yaml inside … describes a Python worker (`language: python`) but sets no `runtime.base_image` … language: does not select the sandbox image. Add runtime.base_image: docker.io/iiidev/python:latest to iii.worker.yaml and rebuild the archive.
warning: … contains `.venv/`, a Python virtualenv built on this machine … (also __pycache__/, .git/) Not fatal, but leave those folders out: stage the archive, or archive with --exclude as in step 3.
warning: … contains `._x` and possibly other `._*` files, the AppleDouble metadata macOS tar adds … (also .DS_Store) Not fatal, but leave them out: archive with COPYFILE_DISABLE=1 tar -czf … as in step 3.
iii.worker.yaml at the root of … is N bytes; iii refuses bundle manifests over 64 KiB Trim the manifest below 64 KiB and rebuild the archive.
no string `version` in …/pyproject.toml; pass --version The version is under [tool.poetry] or is dynamic. Add [project].version or pass --version.
iii.worker.yaml has no `manifest:` to read the version from; pass --version Add manifest: package.json or manifest: pyproject.toml.

GitHub Actions

GitHub Actions sets GITHUB_REPOSITORY, which fills the worker's repo link when iii.worker.yaml has none, so --github-repo can be omitted. The job runs an engine and the worker it publishes in the background, so that publish can read the worker's functions and triggers (Connect the worker to a running engine); --engine-wait gives both time to start:

- name: Publish to the iii registry
  env:
    WORKERS_API_KEY: ${{ secrets.WORKERS_API_KEY }}
  run: |
    curl -fsSL https://install.iii.dev/iii/main/install.sh | sh
    curl -fsSL https://workers.iii.dev/install/workers-cli/latest.sh | sh
    export PATH="$HOME/.local/bin:$PATH"
    iii --no-update-check &                                  # the engine, on ws://localhost:49134
    III_WORKER_NAME=bridge ./target/release/bridge &         # the worker being published, connected to it
    workers publish --artifacts dist --skills skills --engine-wait 60   # binary worker

Only WORKERS_API_KEY is needed. For a bundle worker, --artifacts must be the .tar.gz file, not a folder, and the job must build that archive first; see In GitHub Actions.

Troubleshooting

Message Fix
no API key: run `workers login` or set WORKERS_API_KEY Run workers login or export WORKERS_API_KEY.
could not connect to the iii engine at ws://localhost:49134: … No engine there. Start one (iii) with the worker connected to it, or point --engine / III_URL at the engine it is connected to. In CI, add --engine-wait . Nothing was uploaded. See Connect the worker to a running engine.
worker `x` is not connected to the iii engine at … Connected workers: … Start the worker against that engine under its registry name (III_WORKER_NAME=x), or point --engine at the engine it is connected to.
worker `x` is connected … but has registered no functions and no trigger types The worker connected but registered nothing yet: publish once it has (--engine-wait waits for it).
warning: N function(s) have no typed request or response schema … / warning: N trigger type(s) have no typed configuration schema … Not fatal: the API reference shows those schemas as unknown. Register the handlers (or trigger types) with typed formats.
two of the worker's functions publish as `x` … / trigger type `x` cannot be published … Rename the function (or its metadata.registry_name) or the trigger type so the registry accepts it.
engine URL … must start with ws:// / … wss:// is not supported Pass --engine ws://host:port: iii engines serve plain ws:// only.
no `-.tar.gz|.zip` files in The files in --artifacts don't follow the naming pattern, or bin: doesn't match their prefix.
does not exist: bundle workers need --artifacts to be the .tar.gz archive … The archive was not built yet, or the path is wrong: build it first (step 3) and point --artifacts at it.
bundle workers need --artifacts to be the .tar.gz archive, and is a folder … / is not a .tar.gz file: … With deploy: bundle, point --artifacts at the .tar.gz you built: folders are never archived for you, and .tgz/.zip are refused.
no API key on stdin: pipe it in … workers login found no terminal and nothing on stdin. Pipe the key (printf '%s\n' "$KEY" | workers login --stdin) or, in CI, set WORKERS_API_KEY.
the registry at … refused this API key (Invalid API key), so it was not saved: … The key is mistyped, revoked or expired: paste the whole key, or ask for a new one. API key expired means the same.
could not reach the registry at … to check the API key … workers login saves only a key it could check. Check your connection and --registry / WORKERS_REGISTRY_URL.
--skills … holds no non-empty Markdown (.md) file to publish … Add a SKILL.md, or drop --skills. Nothing was uploaded.
warning: … links to `…`, which is not a published skill file … Only non-empty .md files inside --skills are published: link to one of them, or to a URL.
could not move latest of …: 409 Conflict: … The tag moved since it was read, or --expect-current is stale. Check with workers tag ls and run it again.
The worker page does not show the version just published Pages are cached for about a minute; iii can install the version right away. The Page: line publish prints is where it shows up. A candidate (--tag next) never becomes the page's version: open the ?version= link printed, or promote it with workers tag promote.
… has no iii.worker.yaml at its root, so iii cannot install it: … The bundle archive is missing its manifest, usually because the build folder was archived. See Common bundle errors.
deploy `x` is not supported (only binary, bundle and executable) Fix deploy: in iii.worker.yaml.
iii.worker.yaml has no `manifest:` to read the version from; pass --version Add manifest: to iii.worker.yaml or pass --version.
no string `version` in ; pass --version Add a version to your language manifest or pass --version.
… is already published with different contents; bump the version instead of overwriting Versions are immutable: bump the version and publish again.

License

Apache-2.0