Versions

The running jobjob_7Hq2 · accepted · document.fr-CA.pdf · $49.00 final

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.

WhatKeeps
a jobthe version it was ordered under, from intake to its end, with that version's rules, price and plan
a job minted by a subscriptionthe version live at the mint
a job's linethe rung its version named, so a line that moves reaches new jobs only: Move a step to AI
a keythe 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 subjectits job's version, until a newer job of the Service joins the subject: Timers and replay
a version anything is pinned tostays 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 flagBreaking: refused with breaking_change
a new Service, step, lens, ask or rolea Service removed, or its id changed
an optional input field, a new output fieldan input made required, or an output field removed or retyped
a new event, operation or error codea step renamed or removed while pinned jobs still have it ahead
a price change for new ordersan 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:

Run
npx services.dev check --against live
magic values: none
api.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 reviews

Thirty-eight responses are still on the old version. A repin dry run sorts each one before anything moves:

Run
npx services.dev jobs repin --from 2026-10-19.3 --to 2026-11-02.1 --dry-run
magic values: none
41 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.1

The 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.
Run
npx services.dev jobs repin --from 2026-10-19.3 --to 2026-11-02.1
magic values: none
✓ 29 matters repinned now · 3 when their reviews end · job.repinned on each: from, to, and the step it takes effect at

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

Run
npx services.dev rollback 2026-10-19.3 --dry-run
magic values: none
api.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.3
Run
npx services.dev rollback 2026-10-19.3
magic values: none
api.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 it

To move jobs back too, repin them --to the earlier version, with its dry run first. Every version still pinned:

Run
npx services.dev versions
magic values: none
api.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 Oct

The tax brand's June change, where a job keeps its rungs across a deploy, is the timer-level view.

Call (CLI)APIEventOn the consolePlan
check --against livePOST https://services.dev/v1/deploys { dry_run: true }—/console/deploysevery plan
jobs repin --from <version> --to <version> [--dry-run]POST /owner/versions/:from/repin { to, dry_run }job.repinned/console/deploysevery plan
jobs repin <job> --to <version>POST /owner/jobs/:id/repin { to }job.repinned/console/jobsevery plan
rollback <version> [--dry-run] [--breaking]POST https://services.dev/v1/deploys/:version/live { rollback: true, dry_run }version.rolled_back/console/deploysevery plan
versionsGET /versions?pinned=truenone: it reads/console/deploysevery plan