npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@worker-protocol/client

v0.3.1

Published

Read a Worker, and take work from it. The consumer half of worker-protocol

Readme

@worker-protocol/client

Read a Worker, and take work from it. The consumer half of worker-protocol.

worker-protocol is an open specification for Workers that can be seen, operated and given work by people who did not build them. HTTP and JSON Schema, no runtime. A Worker describes itself in a Descriptor served at /.well-known/worker-protocol — the one address this protocol fixes — and declares there which of its Capabilities it implements and at which addresses.

consume(url) reads that document once, resolves every address the Worker declared, and answers an object with one member per Capability that Worker implements — and nothing for the ones it does not. A console, a Control Tower, a teams app or a Worker that answers another Worker's Tasks is built on this.

It depends on @worker-protocol/schemas and on fetch, and on nothing else. A consumer is not a server, so nothing here asks you to install a web framework in order to make HTTP requests.

Install

npm i @worker-protocol/client zod

zod is a peer dependency (^4.5.4), shared with @worker-protocol/schemas.

Reading a Worker

import { consume } from "@worker-protocol/client";

const worker = await consume("https://fleet.example.com", { credential: process.env.TOKEN });

worker.descriptor; // the document itself, validated
worker.edition; // the edition this Worker declares it speaks

// Each of these is present only where the Worker declared the Capability.
const health = await worker.health?.();
const alerts = await worker.alerts?.();
const activity = await worker.activity?.();

const buckets = await worker.metrics?.read("vehicles.quiet", {
  granularity: "day",
  from: new Date("2026-09-01T00:00:00Z"),
  by: ["region"],
});

// What the Worker recorded, most recent first, from the window it still holds.
const records = await worker.logs?.read({ level: "warn", from: new Date("2026-09-23T00:00:00Z") });

Paging, the cursor, the ordering, the retry that must not happen on a refusal the caller cannot fix, and the classification of an answer that cannot be read are all carried here. Nine of the rules this package implements oblige a consumer rather than a Worker, and a consumer that wrote this itself would be deriving every one of them again.

One page at a time

Every list above reads to the end of the collection. Beside each is page(), which answers one page and the cursor for the next — for a caller whose work is cut into invocations, a scheduled function or a serverless action, and which keeps the cursor in its own store between them:

const first = await worker.logs?.page({ level: "warn" });
// ... store first.nextCursor, and in a later invocation:
const next = await worker.logs?.page({ level: "warn", cursor: storedCursor });

worker.alerts?.page(), worker.activity?.page(), worker.tasks?.page({ type }) and worker.metrics?.page(metric, options) are the same call. nextCursor is absent at the end. The cursor goes back exactly as it arrived and is never built here (ENDP-21), and a Worker that will not honour it — a deploy since, or simply one that never promised to — answers a refusal that throws Refused with kind: "reject". Start the reading again from the top.

A cursor continues one reading. It does not find what is new since the last poll, and nothing in this package does. The specification gives no cursor that points forward and gives a log record no identity, so that is the caller's to work out:

  • logs is answered most recent first (LOG-3), so a cursor walks towards older records and a cursor kept from the last poll reaches nothing written since. Read again from the head with from set to the newest instant you hold (LOG-8, inclusive), and drop the records at that instant you already have. A record carries an instant, a level, a message and perhaps fields, and nothing else (LOG-4), so two identical lines recorded at the same instant cannot be told apart.
  • tasks, alerts and activity are what holds now, derived on every read: each ends when its condition stops holding, and a new one can sort anywhere in the order. Read the whole list and compare it by id with what you stored last time.

A cursor has no lifetime this protocol states, and ENDP-21 is what keeps it opaque, so none of that can be done by holding on to one.

Taking work

A Task is work a Worker needs somebody to do. Nobody is assigned one and nobody closes one: it exists while its condition holds and it is gone when that stops being true.

const open = await worker.tasks?.list();

for (const task of open ?? []) {
  // How to answer this type: the Action to post, and the JSON Schema of what it takes. Both are
  // read out of the Descriptor you already hold — the Task itself says nothing about how to answer
  // it, because that belongs to the Worker that raised it.
  const answer = worker.tasks?.answers(task.type);
  if (answer === undefined) continue;

  const input = doTheWork(task.payload);
  await worker.actions?.perform(answer.action, input, { idempotencyKey: crypto.randomUUID() });
}

The schema comes back as it travels, so a console can render a form from it and an agent can build the document, neither having been told anything about this Worker in advance. Where a Task can end several ways, that schema is a discriminated union and each ending is a variant.

worker.nudges?.(type) tells a Worker there is work of a type it answers. It buys latency and nothing else — a consumer reading on its own schedule is slower and never wrong — so it answers nothing and is safe to lose.

When a call does not succeed

| Thrown | What happened | |---|---| | Refused | The Worker answered a refusal, and it carries the code. A reject is not repeated; a retry already was, with backoff, before you saw this. | | Malformed | The Worker answered something this protocol does not admit, naming the rule it broke. | | Unserved | The Worker declared an address that serves nothing there. |

Retries and their backoff are retries and backoffMs on the options, and default to three attempts from 200ms. A credential is presented as Authorization: Bearer <token> and is never sent to an origin the Descriptor did not record as the Worker's own.

Before any work changes hands

import { canAnswer } from "@worker-protocol/client";

const { verdict, why } = canAnswer(owner.descriptor, answerer.descriptor, taskType);

A Worker declares its Skills: the Task types it can answer, what it needs to be handed to answer one, and what it produces. canAnswer compares that against what the owner sends and what the owner's answering Action takes, in both directions, and answers compatible, incompatible or unknown with a reason. unknown is not false: an answerer that declares the Skill and states no requirement has claimed the capability and said nothing about what it needs, which the protocol allows, and reporting that as a refusal would be inventing an obligation.

Strict about what this protocol fixes, blind to what it does not

Every document the protocol's schemas describe is validated, and a Worker that answers something else raises Malformed naming the rule. A Task's payload, an Action's input and result, an event's data are the Worker's own — this protocol has no data model — and nothing here looks inside them.

Related packages

  • @worker-protocol/schemas — the Zod objects that generate the normative JSON Schemas.
  • @worker-protocol/hono — the other half: mount(), for building a Worker on Hono.
  • @worker-protocol/conformance — point it at a Worker's base URL, get a report of what it complies with.

License and name

Apache-2.0, patent grant included — implement the protocol in any product, commercial or not, without asking anyone. The name is not part of that grant (Apache-2.0 §6): a claim that something speaks worker-protocol is one this project vouches for, and the conformance tool is how it is earned.