Versions
Each deploy is a version, the whole bundle under one sha256. A job keeps the version it was ordered under until it ends, a key keeps the contract of its first call, and check --against live sorts every change into additive or breaking. You move jobs in flight to a new version with a dry run first, and a rollback changes only what new orders take.
A version answers one question: which rules is this job running under? A version is the whole bundle, the Service files and every module they import, the rules, the Pools and the brand, named 2026-11-02.1 (or by --name) and fixed by one sha256. Nothing in a version changes after it is built.
| What | Keeps |
|---|---|
| a job | the version it was ordered under, from intake to its end, with that version's rules, price and plan |
| a job minted by a subscription | the version live at the mint |
| a job's line | the rung its version named, so a line that moves reaches new jobs only: Move a step to AI |
| a key | the contract of its first call: the shapes, fields and errors it was built against. Services-Version: <version> overrides it for one request |
| a clock on a subject | its job's version, until a newer job of the Service joins the subject: Timers and replay |
| a version anything is pinned to | stays runnable, and retires when its last job, clock and timer close |
So a deploy never changes a job that is already running. The new version reaches new orders, and the jobs you choose to move.
check --against live sorts every difference between your folder and live:
| Additive: ships with no flag | Breaking: refused with breaking_change |
|---|---|
| a new Service, step, lens, ask or role | a Service removed, or its id changed |
| an optional input field, a new output field | an input made required, or an output field removed or retyped |
| a new event, operation or error code | a step renamed or removed while pinned jobs still have it ahead |
| a price change for new orders | an operation or event removed, or its data narrowed |
A breaking change ships three ways: under a new Service id, beside the old one; after the jobs that need the old shape end; or with deploy --live --breaking, which keeps every pinned job and key on its own version. Each key's version is on the owner console, so you see who still calls the old contract before you retire it.
api.lawyer adds a fourth lens to its trademark response's adversarial review. It is additive:
npx services.dev check --against liveapi.lawyer · against live 2026-10-19.3 · sha256:9c02d1aa7e40b3c8…
additive services/trademark-response.ts:19 attack.lenses + 'genericness'
breaking none
pinned 41 matters on 2026-10-19.3 · 38 trademark responses, 3 contract reviewsThirty-eight responses are still on the old version. A repin dry run sorts each one before anything moves:
npx services.dev jobs repin --from 2026-10-19.3 --to 2026-11-02.1 --dry-run41 matters on 2026-10-19.3 · dry run, nothing changes
repin 29 trademark responses · each takes 2026-11-02.1 at its next step, so attack runs with 4 lenses
unchanged 6 trademark responses · past attack: nothing ahead of them differs · they stay on 2026-10-19.3
waits 3 trademark responses · a review claimed under 2026-10-19.3 · each repins after the signer's act
never 3 contract reviews · nothing in the diff touches them
run it npx services.dev jobs repin --from 2026-10-19.3 --to 2026-11-02.1The rules a repin keeps:
- It takes effect at the job's next step, never inside one. A committed step never runs again.
- A step claimed by a person finishes on the terms it was claimed under, and the job repins after their act.
- A job whose committed steps aren't all in the new graph can't repin, and the dry run says why.
- Price, plan and rules stay as ordered. A repin changes how the rest of the work runs, never what the buyer agreed to pay.
npx services.dev jobs repin --from 2026-10-19.3 --to 2026-11-02.1✓ 29 matters repinned now · 3 when their reviews end · job.repinned on each: from, to, and the step it takes effect atA rollback sends new orders to an earlier version, and it is checked the way a deploy is. rollback runs check --against live in reverse, so a key first called on the newer version keeps that version's contract, and every job keeps the version it runs on. A rollback that removes a field, step or operation a pinned key or job relies on is refused with breaking_change unless you pass --breaking. Rolling back to a retired version first re-runs its suite in test mode, from the bundle its sha256 kept.
npx services.dev rollback 2026-10-19.3 --dry-runapi.lawyer · live 2026-11-02.1 → 2026-10-19.3, for new orders · dry run, nothing changes
reverse check --against 2026-11-02.1: 1 removed lens, attack.lenses − 'genericness' · 0 breaking
keys 4 keys first called on 2026-11-02.1 keep its shapes · Services-Version tells each one apart
jobs 32 matters on 2026-11-02.1 keep it to their ends
run it npx services.dev rollback 2026-10-19.3npx services.dev rollback 2026-10-19.3api.lawyer · live 2026-11-02.1 → 2026-10-19.3, for new orders
stays every matter keeps the version it runs on · jobs repin --to 2026-10-19.3 moves them back
timers arms nothing, rewrites nothing
event version.rolled_back · 2026-11-02.1 stays runnable while anything is pinned to itTo move jobs back too, repin them --to the earlier version, with its dry run first. Every version still pinned:
npx services.dev versionsapi.lawyer · 3 versions
2026-11-02.1 live sha256:4e1b77c0 since 2 Nov 15:12 · 32 matters repinned to it
2026-10-19.3 pinned sha256:9c02d1aa 9 matters · 18 clocks · 63 timers · retires when they close
2026-10-19.2 retired sha256:51f0b6e2 nothing pinned since 30 OctThe tax brand's June change, where a job keeps its rungs across a deploy, is the timer-level view.
| Call (CLI) | API | Event | On the console | Plan |
|---|---|---|---|---|
check --against live | POST https://services.dev/v1/deploys { dry_run: true } | — | /console/deploys | every plan |
jobs repin --from <version> --to <version> [--dry-run] | POST /owner/versions/:from/repin { to, dry_run } | job.repinned | /console/deploys | every plan |
jobs repin <job> --to <version> | POST /owner/jobs/:id/repin { to } | job.repinned | /console/jobs | every plan |
rollback <version> [--dry-run] [--breaking] | POST https://services.dev/v1/deploys/:version/live { rollback: true, dry_run } | version.rolled_back | /console/deploys | every plan |
versions | GET /versions?pinned=true | none: it reads | /console/deploys | every plan |