Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ jobs:
# Add steps to the job
# See: https://circleci.com/docs/2.0/configuration-reference/#steps
steps:
- run:
name: Install pnpm
command: sudo npm install -g corepack@latest && sudo corepack enable
- checkout
- restore_cache:
# See the configuration reference documentation for more details on using restore_cache and save_cache steps
Expand Down
183 changes: 48 additions & 135 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,165 +1,78 @@
# OpenFn Kit
# CLAUDE.md

This monorepo contains the core packages that power OpenFn's workflow automation platform. OpenFn is a Digital Public Good trusted by NGOs and governments in 40+ countries to automate data integration workflows.
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Architecture
OpenFn kit is the pnpm monorepo behind OpenFn's workflow automation: the CLI, the Runtime that executes jobs, and the Worker that runs jobs in production for Lightning (OpenFn's web platform).

The repository has three main packages: **CLI**, **Runtime**, and **Worker**. The CLI and Worker are both frontends for executing workflows - the CLI for local development, the Worker for production execution via Lightning (the web platform). Both wrap the Runtime as their execution engine. The Worker uses engine-multi to wrap the Runtime for multi-process execution.
## How the pieces fit

## Core Packages
- **@openfn/runtime** executes a workflow plan in a sandboxed Node VM. A workflow is a graph of steps (jobs) joined by conditional edges. A job is an array of operations, each a function from state to state. State is never mutated between jobs.
- **@openfn/compiler** turns job DSL into a standard ES module that imports from its adaptor (an npm package such as `@openfn/language-http`) and exports the operations array.
- **@openfn/cli** is the local frontend. It compiles jobs itself and calls the runtime in-process.
- **@openfn/ws-worker** is the production frontend. It is stateless: it claims runs from Lightning over websockets and reports events back, and Lightning owns all persistence. It never imports the compiler. It hands plans to **@openfn/engine-multi**, which compiles and runs each one in a worker thread inside a child process.
- **@openfn/project** models local projects and workspaces (`Workspace`, `Project`, YAML parsing, diffs, version hashes). The CLI's `project` handlers in `packages/cli/src/projects/` are thin layers over it, and this is the most actively developed area of the repo. Sync with Lightning uses its v2 endpoint.
- Supporting: `lexicon` (shared types), `logger`, `lightning-mock` (fake Lightning for worker tests), `deploy` and `describe-package` (both being phased out).

- **[@openfn/cli](packages/cli)** - Command-line interface for local development. Run, test, compile, and deploy workflows.
- **[@openfn/runtime](packages/runtime)** - Core execution engine. Safely executes jobs in a sandboxed VM environment.
- **[@openfn/ws-worker](packages/ws-worker)** - WebSocket worker connecting Lightning to the Runtime. Stateless server that pulls runs from Lightning's queue. See [.claude/event-processor.md](.claude/event-processor.md) for event processing details.
- **[@openfn/engine-multi](packages/engine-multi)** - Multi-process runtime wrapper used by ws-worker for concurrent workflow execution.
- **[@openfn/compiler](packages/compiler)** - Transforms OpenFn job DSL into executable JavaScript modules.
Every internal dependency is `workspace:*` and resolves to the dependency's built `dist/`, not its `src/`. engine-multi also runs its worker processes from `dist/`.

## Supporting Packages
## Commands

- **@openfn/lexicon** - Shared TypeScript types
- **@openfn/logger** - Structured logging utilities
- **@openfn/describe-package** - TypeScript analysis for adaptor docs (to be phased out)
- **@openfn/deploy** - Deployment logic for Lightning (soon to be deprecated)
- **@openfn/project** - Models and understands local OpenFn projects
- **@openfn/lightning-mock** - Mock Lightning server for testing

## AI Assistant

- Keep responses terse and do not over-explain. Users will ask for more guidance if they need it.
- Always present users a short action plan and ask for confirmation before doing it
- Keep the human in the loop at all times. Stop regularly and check for guidance.

## Key Concepts

**Workflows** are sequences of **jobs** that process data through steps. Each **job** is an array of **operations** (functions that transform state). State flows between jobs based on conditional edges.

**Adaptors** are npm packages (e.g., `@openfn/language-http`) providing operations for specific systems. The CLI auto-installs them as needed.

The **Compiler** transforms job DSL code into standard ES modules with imports and operation arrays.

## Working with Projects

A **workspace** holds one or more local **projects** that sync with Lightning. The CLI `project` subcommand (package: `@openfn/project`) is the most actively-developed area of the repo; handlers live in `packages/cli/src/projects/`. Sync uses Lightning's v2 endpoint.
Node is pinned in `.tool-versions` (use `asdf`), and pnpm switches itself to the version pinned in `packageManager` (install any pnpm with `npm install -g pnpm`; corepack no longer ships with Node).

```bash
openfn project list # List projects in the current workspace
openfn project pull [project] # Pull from Lightning to the filesystem (fetch + checkout)
openfn project fetch [project] # Download project state without expanding it
openfn project checkout <project> # Switch the active project in the workspace
openfn project deploy [project] # Deploy the checked-out project to Lightning
openfn project clean # Delete the workflows folder and re-checkout the active project
openfn project version [workflow] # Show the version hash of a workflow
pnpm install
pnpm build # all packages
pnpm --filter @openfn/cli build # one package
pnpm test # all package tests
pnpm test:types # tsc per package
pnpm format / pnpm test:format # prettier write / check (there is no eslint)
pnpm _typesync # after changing dependencies
```

New subcommands follow the existing pattern in `packages/cli/src/projects/` (`list.ts` is the simplest reference).

## Development Setup

### Prerequisites

- Node.js 24 (pinned in `.tool-versions`; `package.json` engines still allows `>=18`). Use `asdf`.
- pnpm (enable with `corepack enable`)

### Common Commands
Tests use ava with the shared root `ava.config.js`, which runs `test/**/*test.ts` straight from `src/` via `@swc-node/register`. Because workspace deps come from `dist/`, run `pnpm build` first when a test crosses packages or uses engine-multi. When unsure, build.

```bash
# Root
pnpm install # Install dependencies
pnpm build # Build all packages
pnpm test # Run all tests
pnpm format # Format all packages with Prettier
pnpm changeset # Add a changeset for your PR

# CLI
cd packages/cli
pnpm openfn test # Run from source
pnpm install:global # Install as 'openfnx' for testing

# Worker
cd packages/ws-worker
pnpm start # Connect to localhost:4000
pnpm start -l mock # Use mock Lightning
pnpm start --no-loop # Disable auto-fetch
curl -X POST http://localhost:2222/claim # Manual claim
cd packages/project
pnpm ava test/util/config.test.ts # one file
pnpm ava test/util/config.test.ts -m "name*" # tests matching a title
```

### Environment Variables

**CLI:**
- `OPENFN_REPO_DIR` - CLI adaptor storage
- `OPENFN_ADAPTORS_REPO` - Local adaptors monorepo path
- `OPENFN_API_KEY` - API key for Lightning deployment
- `OPENFN_ENDPOINT` - Lightning URL (default: app.openfn.org)

**Worker:**
- `WORKER_SECRET` - Worker authentication secret
- Many other `WORKER_*` vars (capacity, port, backoff, max payload, Sentry DSN, …) — see `packages/ws-worker/src/util/cli.ts`.

## Repository Structure

```
packages/
├── cli/ # CLI entry: cli.ts, commands.ts, projects/, options.ts
├── runtime/ # Runtime entry: index.ts, runtime.ts, modules/linker
├── ws-worker/ # Worker entry: start.ts, server.ts, api/, events/
├── compiler/ # Job DSL compiler
├── engine-multi/ # Multi-process wrapper
├── lexicon/ # Shared TypeScript types
└── logger/ # Logging utilities
```

## Testing & Releases

```bash
pnpm test # All tests
pnpm test:types # Type checking
pnpm test:integration # Integration tests
cd packages/cli && pnpm test:watch # Watch mode
```
ws-worker and engine-multi run with `--serial`; keep new tests there compatible with that.

## Testing Best Practice
### Running things locally

- Ensure tests are valuable before generating them. Focus on what's important.
- Treat tests as documentation: they should show how the function is expected to work
- Keep tests focuses: test one thing in each test
- This repo contains extensive testing: check for similar patterns in the same package before improvising
- **CLI**: `cd packages/cli && pnpm openfn <command>` runs the built `dist/`, so rebuild (or `pnpm build:watch`) after changes. `pnpm install:global` from root installs a global `openfnx` for testing; remove it with `npm uninstall -g @openfn/clix`.
- **Worker**: `cd packages/ws-worker && pnpm start` connects to Lightning at `localhost:4000`. `-l mock` uses the mock Lightning, `--mock` uses a mock engine, and `--no-loop` stops auto-claiming so you can trigger claims with `curl -X POST http://localhost:2222/claim`. Options and their `WORKER_*` env vars are in `packages/ws-worker/src/util/cli.ts`.
- CLI env vars (`OPENFN_API_KEY`, `OPENFN_ENDPOINT`, `OPENFN_REPO_DIR`, `OPENFN_ADAPTORS_REPO`, and others) are defined in `packages/cli/src/options.ts` and `packages/cli/src/projects/options.ts`.

## Additional Documentation
### Integration tests

**Changesets**: Run `pnpm changeset` when submitting PRs. Releases publish automatically to npm on merge to main.
These live in `integration-tests/` (`cli`, `execute`, `worker`, `legacy-deploy`) and run against built packages. Always `pnpm build` first.

Changeset release notes must be a **single, short, high-level sentence** - vague is fine (e.g. `added new "compile-test" command to the CLI` or `Tighten guards against shell injection vectors`). Do not list files, functions, or implementation detail. For breaking changes only, you may add a short migration guide after the sentence. See the package `CHANGELOG.md` files for the house style.
- `pnpm test:integration` from root runs them all.
- In `integration-tests/cli`, `pnpm test:dev` uses the local `packages/cli/dist`. Plain `pnpm test` expects a globally installed `openfn`, which is how CI runs it: `pnpm pack:local` then install the tarball globally.

The [.claude](.claude) folder contains detailed guides:
## Adding a CLI command

- **[event-processor.md](.claude/event-processor.md)** - Worker event processing deep-dive (ordering, batching) — companion to `packages/ws-worker/CLAUDE.md`
- **[yaml-formats.md](.claude/yaml-formats.md)** - v1 vs v2 project YAML formats: structure, detection logic, and conversion paths
- **[unit-testing-jobs.md](.claude/unit-testing-jobs.md)** - How to compile job for unit-testing
Commands are yargs modules registered in `packages/cli/src/cli.ts`. Each module calls `ensure('<command-name>', ...)` (`util/command-builders.ts`) to set `argv.command`, and `packages/cli/src/commands.ts` dispatches that string to a handler. A new `project` subcommand needs all three of:

Key packages also carry their own `CLAUDE.md` (runtime, engine-multi, ws-worker), auto-loaded when you work in them.
1. a yargs module added in `packages/cli/src/projects/command.ts`;
2. the handler exported from `packages/cli/src/projects/index.ts`;
3. an entry in the `handlers` map and the `CommandList` union in `commands.ts`.

## Code Standards
`packages/cli/src/projects/list.ts` is the simplest example to copy.

- **Formatting**: Use Prettier (`pnpm format`)
- **TypeScript**: Required for all new code
- **TypeSync**: Run `pnpm _typesync` after modifying dependencies
- **Tests**: Each package's tests run against its own `src/` via `@swc-node/register` (no build of the package under test needed). BUT workspace `@openfn/*` deps resolve to their built `dist/`, and `engine-multi` runs its worker processes from `dist/` — so run `pnpm build` first when tests cross packages or use the engine. When unsure, build.
- **Independence**: Keep packages loosely coupled where possible
## Changesets and releases

## Architecture Principles
Every PR that changes a published package needs a changeset (`pnpm changeset`). The release note is a **single, short, high-level sentence**, and vague is fine (e.g. `added new "compile-test" command to the CLI`). Leave out files, functions and implementation detail. Breaking changes may add a short migration guide after the sentence. The package `CHANGELOG.md` files show the house style.

- **Separation of Concerns**: CLI and Worker are frontends; Runtime is the shared execution backend
- **Sandboxing**: Runtime uses Node's VM module for isolation
- **State Immutability**: State cannot be mutated between jobs
- **Portability**: Compiled jobs are standard ES modules
- **Zero Persistence (Worker)**: Worker is stateless; Lightning handles persistence
- **Multi-Process Isolation**: Worker uses engine-multi for concurrent workflow execution
Releases are prepared by hand on a release branch: run `pnpm changeset version`, check the changelogs, run `pnpm install`, and commit. Merging to main then publishes to npm and builds the worker Docker image. A brand-new package must be published once first with `pnpm publish:new`; see `README.md`.

## Contributing
## Further guides

1. Make changes
2. Run `pnpm test`
3. Add changeset: `pnpm changeset`
4. Open PR at https://github.com/openfn/kit
Read these before working in the matching area:

**Resources**: [docs.openfn.org](https://docs.openfn.org) | [app.openfn.org](https://app.openfn.org) | [github.com/openfn/kit](https://github.com/openfn/kit)
- [.claude/event-processor.md](.claude/event-processor.md): how the worker orders and batches events sent to Lightning.
- [.claude/yaml-formats.md](.claude/yaml-formats.md): v1 vs v2 project YAML, how each is detected, and how they convert.
- [.claude/unit-testing-jobs.md](.claude/unit-testing-jobs.md): compiling a job so it can be unit tested.
- `packages/runtime`, `packages/engine-multi` and `packages/ws-worker` each have their own `CLAUDE.md`, loaded automatically when you work there.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
FROM node:24-alpine AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
COPY . /app
WORKDIR /app
RUN npm install -g pnpm

# TODO: use prod optimized build? ----------------------------------------------
# FROM base AS build
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ We use [`pnpm`](https://pnpm.io/installation), a fast, disk space efficient pack

## Installing

This repo uses node corepack to ensure pnpm v8 is used
Install pnpm globally. When run inside this repo, pnpm reads the `packageManager` field in `package.json` and switches itself to the pinned version.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this true? I can't see any mention of this in pnpm's (increasingly slop heavy) docs

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is true I think, but devengines.packageManager is I think the most correct way to specify the version. That's implemented on this PR but this doc note is wrong either way. Maybe I'd just remove it and note that package.json locks the version


- `$ corepack enable`
- `$ npm install -g pnpm`
- `$ pnpm install`
- `$ pnpm build`

Expand Down Expand Up @@ -127,7 +127,7 @@ This repo uses `typesync` to ensure that all packages have an appropriate `@type

This must be run MANUALLY (See https://github.com/OpenFn/kit/issues/333)

On every add, update and remove. you should do `pnpm run typesync` from the repo root.
On every add, update and remove. you should do `pnpm _typesync` from the repo root.

Note that @types packages only synchronise with the major and minor versions of a package. So for `@types/x@major.minor.patch`, `major` and `minor` refer to the versions of the corresponding package `x`, and `patch` is the version number of the actual types package.

Expand Down
15 changes: 13 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@
"version": "1.0.0",
"private": true,
"description": "Packages to support OpenFn v2",
"devEngines": {
"runtime": {
"name": "node",
"version": "26.x",
"onFail": "warn"
},
"packageManager": {
"name": "pnpm",
"version": "12.14.0",
"onFail": "download"
}
},
"scripts": {
"build": "pnpm run clean && pnpm -r --filter=./packages/* run build",
"clean:local": "rimraf dist/**/*",
Expand Down Expand Up @@ -40,6 +52,5 @@
"semver": "^7.7.4",
"tar-stream": "^3.1.8",
"yargs": "^17.7.2"
},
"packageManager": "pnpm@12.6.0+sha512.3ef68f951cb111ac204b4a5a16f0b2ddf0da56a96e0413e81d855d9f0b55ef926714709028e1cd00c405c2c5fb7b9e8ec4dc46777c805d0373c2f2ff00fd20ec"
}
}
2 changes: 1 addition & 1 deletion packages/ws-worker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ You can start a dev server (which rebuilds on save) by running:
pnpm start:watch
```

This will wrap a real runtime engine into the server. It will rebuild when the Worker Engine code changes (although you'll have to `pnpm build:watch` in `runtime-manager`). This will use the repo at `WORKER_REPO_DIR` (or a default path in /tmp)
This will wrap a real runtime engine into the server. It will rebuild when the Worker Engine code changes (although you'll have to `pnpm build:watch` in `engine-multi`). This will use the repo at `WORKER_REPO_DIR` (or a default path in /tmp)

### Disabling auto-fetch

Expand Down
2 changes: 1 addition & 1 deletion packages/ws-worker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"build:watch": "pnpm build --watch",
"start": "node --loader @swc-node/register/esm src/start.ts",
"start:prod": "node dist/start.js",
"start:watch": "nodemon -e ts,js --watch ../runtime-manager/dist --watch ./src --exec 'pnpm start'",
"start:watch": "nodemon -e ts,js --watch ../engine-multi/dist --watch ./src --exec 'pnpm start'",
"pack": "pnpm pack --pack-destination ../../dist"
},
"bin": {
Expand Down
Loading
Loading