Skip to content
mj-devingPublic

About

A unit of work you can place. Write a step once in TypeScript, declare what it needs, and run the same file on a laptop, a small server, or an edge isolate.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Waybill. A unit of work you can place.

CI Latest release MIT licence

You write a step once in TypeScript, declare what it needs, and the same file runs on a laptop, on a small server, or in an edge isolate. Nothing about the step changes between those, and no build flag selects between them.

Waybill is a library and a file layout. It is not a platform, it has no server, no UI, and no scheduler. Whatever already decides when your jobs run keeps deciding.

Is this the right tool

Reach for it when a job needs to remember what it has already seen, retry without doing the work twice, and leave a record of what it did. Those three needs are what the state capabilities exist for, and they are what a cron one-liner starts failing at.

Reach for something else when the job is a one-shot script, when it needs long or heavy compute, when it has to sleep for days waiting on a human, or when you want a product with a UI and credential management. docs/ARCHITECTURE.md names the neighbours it is deliberately not, and the ancestors it comes from.

The model

flowchart LR
  in(["{ url }"]) --> A1["A_EXTRACT_CONTENT<br/><i>requires: fetch</i>"]
  A1 -->|"+ content"| A2["A_LABEL_AND_RATE<br/><i>requires: llm</i>"]
  A2 -->|"+ labels, score"| A3["A_ROUTE<br/><i>requires: nothing</i>"]
  A3 -->|"+ destination"| out(["{ url, content, labels, score, destination }"])
Loading

One action per box, and what each one adds on the arrow leaving it. The chain is a pipeline; a flow is what puts items into a chain on a schedule and stores what comes out.

Primitive Is State Handles
Action one unit of work: fetch, parse, an LLM call, a routing decision none one envelope
Pipeline an ordered chain of actions none one envelope
Flow a producer (schedule to queue) or a consumer (queue to pipeline to store) kv, queue, db many

The two arrows differ. Action to pipeline is pure composition. Pipeline to flow adds state and cardinality, which is why a flow is the only place a store appears.

The envelope accumulates. Every action destructures what it needs and returns the rest alongside its own output:

const { content, ...upstream } = input;
return { ...upstream, summary, labels };

The last action in a chain therefore sees every field every earlier action added. This is the invariant that makes chains composable, and it is why provenance is a property of the data rather than something logged next to it.

Capabilities are injected, never imported. An action declares what it needs in its manifest:

{ "name": "A_EXTRACT_ARTICLE", "requires": ["fetch"] }

The runner hands it a fetch, and an action that declared nothing is handed nothing. The declaration is doing two jobs at once: it is what the runner will give the step, and it is the placement constraint. shell cannot be satisfied by an edge isolate, so a unit that asks for it is not deployable there, and the checker says so before a deploy rather than after.

This is a declaration, not a sandbox. It constrains what the runner hands an action, not what an action's own module can import, so an action you add runs with your privileges and you should treat adding one exactly like adding a dependency. SECURITY.md says where that line falls.

flowchart TD
  M["<b>requires</b> in the manifest"] --> C{"waybill check"}
  C -->|"nothing, fetch, llm"| E["laptop &nbsp;·&nbsp; server &nbsp;·&nbsp; edge"]
  C -->|"shell"| S["laptop &nbsp;·&nbsp; server"]
  C -->|"readFile"| D["wherever the files are"]
Loading

The same declaration answers three questions before anything runs: what the step may touch, where it may be deployed, and what a reviewer has to read to know both.

Quickstart

Bun 1.2 or newer, and nothing else. Clone, then:

bun bin/waybill list
bun bin/waybill run --local A_ROUTE --input '{"quality_score":92,"labels":["ai"]}'
bun bin/waybill check

All three run in a fresh clone with no configuration and no credential: A_ROUTE is a pure decision over its input, and list and check only read manifests. check prints what each unit needs and where it may run, which is the command worth reading the output of first.

To type waybill rather than bun bin/waybill, run bun link once in the repo.

Steps that declare llm are the ones that need a provider. Copy .env.example to .env and set ANTHROPIC_API_KEY, or point WAYBILL_INFERENCE_PATH at a local command that answers the same string-in, string-out contract.

Layout

Actions/     A_*/action.json + action.ts    one unit of work each
Pipelines/   P_*/pipeline.json              an ordered list of action names
Flows/       F_*/flow.json + flow.ts        producer or consumer, the only stateful layer
Workers/     one directory per deployed edge worker
lib/         the runner, the types, the checker
shared/      capability implementations and the local store
bin/waybill  the CLI

The reference workload

The repository ships the pipeline it was built for, a feed: poll sources, drop what has been seen, fetch and summarise and rate each new item, route it, store it. It is worth reading because it exercises every part of the model at once. A producer flow enqueues, a consumer flow drains, a pipeline does the per-item work, and dedup happens on both sides because a queue that delivers at least once will eventually deliver twice.

The Cloudflare configuration under Workers/ is real and deployable, with account-specific values replaced by YOUR_* placeholders. Substitute your own resources before deploying.

Documentation

  • docs/ARCHITECTURE.md — why it is shaped this way. The four decisions, what a capability maps onto per substrate, the lineage, and where the model stops.
  • docs/BUILDING_AUTOMATIONS.md — how to build a new automation on it. Start here if you want to write something rather than understand something.
  • docs/USE-CASES.md — what the primitives reach, pattern by pattern, each marked as running, buildable, or needing a named extension.
  • CONTRIBUTING.md — the gates a change has to pass, what 0.x means here, and how a release is cut.
  • SECURITY.md — what the capability declaration protects, what it does not, and how to report something privately.

Limits, stated up front

  • No branching. A pipeline is a linear list. No conditionals, no fan-out, no join.
  • No durable waiting. Nothing survives a process death mid-run and resumes days later.
  • No rollback. A failed step leaves earlier side effects in place.
  • No shell in the cloud, and no long or heavy compute at the edge, where CPU and time are capped.

Failures are data rather than exceptions: a step that fails returns a result saying so, with the state of the world at that moment intact, and the caller decides whether that is a retry or a dead letter.

Status

0.x, which here means what SemVer says it means: the interfaces still move, and a minor bump may break them. Every move that matters is in CHANGELOG.md, and each release on the releases page carries that file's section for it, extracted rather than retyped. bun bin/waybill --version reports what you have. MIT licensed.

About

A unit of work you can place. Write a step once in TypeScript, declare what it needs, and run the same file on a laptop, a small server, or an edge isolate.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages