Skip to content
arrowdocs
§ 02 — getting started

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.


step 1

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.

~/projects/demo — zsh
$ 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" },› ]

step 2

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.

arrow.toml
[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" },
]

step 3

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.

~/projects/demo — first run
$ 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
~/projects/demo — second run, cached
$ 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 3
Nothing changed — no work done.

step 4

Watch 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.

terminal
$ 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.6s

step 5

Run 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.

terminal
$ 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.2s

step 6

Ship 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://….

.github/workflows/ci.yml
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.


step

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.