From an empty folder to a green CI run.
A ten-minute tour. We will init a project, write a small arrow.toml, run a job, and watch the cache do its job.
Initialize a project
From any directory, run arrow init. It detects the project kind from the files it finds, drafts a starter arrow.toml, and leaves it for you to edit.
$ arrow init › detected pnpm + vite› writing arrow.toml› writing .gitignore entry (/.arrow/) $ ls arrow.toml› arrow.toml # read what was generated$ cat arrow.toml› [project]› name = "demo"› kind = "web"› version = "0.3.2"› › [jobs.build]› steps = [› { name = "lint", run = "pnpm lint" },› { name = "compile", run = "pnpm tsc --noEmit" },› { name = "bundle", run = "pnpm vite build" },› ]Add a real job
Replace the generated file with something a little more deliberate. A test job that runs after the build, with a cache key bound to the dist/ directory so reruns are free when nothing changed.
[project]
name = "demo"
version = "0.3.2"
[env]
NODE_ENV = "production"
CI = "true"
[jobs.build]
description = "lint, type-check, and bundle"
cache = "dist/**"
steps = [
{ name = "lint", run = "pnpm lint", cache = ".cache/lint/**" },
{ name = "typecheck", run = "pnpm tsc --noEmit" },
{ name = "bundle", run = "pnpm vite build", cache = "dist/**" },
]
[jobs.test]
description = "run unit tests against the build"
depends = ["build"]
steps = [
{ name = "vitest", run = "pnpm vitest run" },
{ name = "report", run = "pnpm report write --out=./reports/test.json" },
]Run the first job
With arrow.toml in place, run a job by name. The first invocation will execute every step; the second will reuse cached outputs wherever the inputs match.
$ arrow run build [build/1] lint starting[build/1] lint done (1.4s)[build/2] compile starting[build/2] compile done (3.1s)[build/3] bundle starting[build/3] bundle done (4.6s) ✓ build completed in 9.2s — 3 steps — cache miss on 3 / cache hit on 0$ arrow run build [build/1] lint cached (key a91f3c)[build/2] bundle cached (key 5d11a4)[build/3] compile cached (key 78b32e) ✓ build completed in 0.4s — 3 steps — cache miss on 0 / cache hit on 3Watch the DAG get built
arrow explain prints the dependency graph for a job as a plain-text tree. It is great for sanity-checks during code review - no rendering dependencies, no browser plugins.
$ arrow explain test test├── build (depends)│ ├── lint cache: .cache/lint/**│ ├── compile cache: -│ └── bundle cache: dist/**├── vitest└── report 4 nodes · 3 leaves · 1 dependency edgeestimated critical path: 9.6sRun a job in parallel
Jobs that don't depend on each other run in parallel by default. arrow run job-a job-b job-c schedules them as soon as their inputs are ready, with bounded concurrency so a laptop fan doesn't go full lift-off.
$ arrow run lint test bundle --parallel=4 [1] lint starting[2] test starting[3] bundle starting[1] lint done (1.4s)[2] test done (5.6s)[3] bundle done (4.2s) ✓ 3 jobs in 5.6s — wall clock — total work 11.2sShip the same config to CI
arrow is small enough to use as a bootstrap step in CI. The same arrow.toml drives your laptop and your pipeline - so you get the same cache keys in both places with --remote-cache=s3://….
name: ci
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: curl -fsSL https://arrow.run/install | sh
- run: arrow run build test --remote-cache=s3://${{ secrets.CACHE_BUCKET }}
env:
ARROW_TOKEN: ${{ secrets.ARROW_TOKEN }}Pushing to a remote cache is opt-in. Without a bucket, runs are purely local - and a fresh clone of your repo will rebuild incrementally because the cache keys are content-hashed from your committed files.
Where you are, what's next
You now have an arrow.toml that other humans can read and a CI pipeline that runs the same jobs as your laptop. From here, the API reference is the next useful thing - it lists every command and every flag, plus the full shape of the config file.
- Skim the API reference - the command surface is small, and you can read it in five minutes.
- Add a remote cache - cut CI time roughly in half with
--remote-cache=s3://your-bucket. - Write your first plugin - a plugin is just a binary that reads a step and runs it. The reference has a worked example.