In CI
Run check on every push. It exits 1 on any failure and names each fix, as text for you or JSON for a pipeline, and magic values and test clocks reach every branch without a person or a wait.
check runs every gate on every Service, and has an agent buy a job of each, in-process. It needs no key, no network and no running server, and it exits 1 on any failure.
Here a teammate's push added one line, routed: true on the filing step, and a second Service called Listen. TypeScript passes both files; check doesn't:
magic values: none
$ npx services.dev check
services/listen.ts · 5 lines · $19 per job, within 48 hours, charged on delivery.
who AI delivers every job.
steps draft · ai → review · ai · adversarial
agent order_listen · 22 of 30 words · bought job_7Hq2 · $19.00 · no person
ledger +$19.00 · AI $0.84 (example) · you $18.16
margin at least $15.20 a job (80%) · a job that stops costs you at most $3.80
apps customer portal · owner console
✗ A surface serves a call with no row, or lacks a live row. Add the row, or render it. cli listen is service.order's command, and the CLI has none.
https://services.dev/docs/reference/check/surface_drift
services/translation.ts · 22 lines · $89 per job, within 48 hours, charged when file is accepted.
who A sworn translator signs before the file step. The buyer answers at glossary.
steps glossary · buyer · answer → translate · ai → harden · ai · adversarial · 2 lenses · then a person signs → file · the registry · routed → correct · buyer · upload · routed → hand_off · buyer · answer · routed
agent order_translation · 30 of 30 words · job_7Hq2 waits on party
ledger +$89.00 · AI $1.26 (example) · sworn translator $30.00 · you $57.74
margin at least $41.20 a job (46%) · a job that stops costs you at most $47.80
apps customer portal · delivery portal · owner consoleOnly check reads the graph: a routed step that nothing routes to never runs, so the charge its receipt releases never comes. And listen is already a command, so the CLI can't order a Service by that name. Drift belongs to the whole brand, so it fails on every Service. Each failure's page says why the gate exists: step_unreachable, release_unproven and surface_drift.
A pipeline reads these fields of check --json:
| Field | What it holds |
|---|---|
services[].ok | false when any gate fails on that Service |
services[].failures[] | each failure's code, reason, next (the fix) and doc_url (its gate's page) |
services[].agent | the job an agent bought in-process: its tool, job, status, charged in cents, and whether it needed a person |
services[].margin.floor | the least you keep of a job, in cents and pct |
mcp | the buyer tools, and their tokens against the budget |
ai.simulated | true until you wire a model |
$ git diff --staged
diff --git a/services/listen.ts b/services/transcription.ts
similarity index 94%
rename from services/listen.ts
rename to services/transcription.ts
index d70002e..a100384 100644
--- a/services/listen.ts
+++ b/services/transcription.ts
@@ -3,7 +3,7 @@
import { Service } from 'services.dev'
export default Service({
- name: 'Listen',
+ name: 'Transcription',
description: 'A recording, transcribed in the language it was spoken.',
input: { recording: 'file' },
output: { transcript: 'file' },
diff --git a/services/translation.ts b/services/translation.ts
index effb319..1f92c79 100644
--- a/services/translation.ts$ npx services.dev check
mcp 14 buyer tools · 5,097 of 8,000 tokens · 9 more with ?toolset=sandbox
"tokens": 5097,
"sandbox": 9,
services/transcription.ts · 5 lines · $19 per job, within 48 hours, charged on delivery.
who AI delivers every job.
steps draft · ai → review · ai · adversarial
agent order_transcription · 22 of 30 words · bought job_7Hq2 · $19.00 · no person
ledger +$19.00 · AI $0.84 (example) · you $18.16
margin at least $15.20 a job (80%) · a job that stops costs you at most $3.80
apps customer portal · owner console
services/translation.ts · 21 lines · $89 per job, within 48 hours, charged when file is accepted.
who A sworn translator signs before the file step. The buyer answers at glossary.
steps glossary · buyer · answer → translate · ai → harden · ai · adversarial · 2 lenses · then a person signs → file · the registry → correct · buyer · upload · routed → hand_off · buyer · answer · routed
agent order_translation · 30 of 30 words · job_7Hq2 waits on party
ledger +$89.00 · AI $1.26 (example) · sworn translator $30.00 · you $57.74
margin at least $41.20 a job (46%) · a job that stops costs you at most $47.80
apps customer portal · delivery portal · owner console
Every command's exit code is its failure's category, so a pipeline branches without parsing words:
Exit codes
0 ok · 1 check failed · 2 usage · 3 invalid request · 4 authentication · 5 blocked: a person must act · 6 conflict or guard · 10 retry: api error, rate limited, nothing running| Exit | category | Means |
|---|---|---|
| 0 | it worked | |
| 1 | check found something to fix | |
| 2 | invalid_request | the command is missing something, has no such name, or the folder has no Services |
| 3 | invalid_request | the brand refused the request, such as a signal the Service doesn't take |
| 4 | authentication | a key that isn't the brand's |
| 5 | blocked | a person must act first |
| 6 | conflict, guard | the job's state or a guard refused it |
| 10 | api_error, rate_limited | retry: nothing running, rate limited, or an API error |
A command that calls the brand needs dev running. Without it, the answer is still the one error shape, and next is the command that fixes it:
$ npx services.dev jobs get job_7Hq2 --json
{
"type": "BLOCKED",
"category": "api_error",
"code": "not_running",
"reason": "Nothing is running at http://localhost:8787.",
"next": {
"do": "run",
"cli": "npx services.dev dev"
},
"doc_url": "https://services.dev/docs/reference/errors#not_running",
"request_id": null
}
$ echo $?
10A test never waits for a person or an outside system. Magic values pick the branch:
| Where | Value | Branch |
|---|---|---|
| a party's email | signs@example.test | acts at once: signs, consents, answers |
| a party's email | declines@example.test | declines, and the job ends at $0 unless a step routes it |
| a party's email | silent@example.test | never answers, so the ask reminds, expires and lapses on the clock |
metadata.sandbox.outside | reject_once | the outside system rejects the first submission, so the repair runs |
metadata.sandbox.outside | never | the outside system never answers, so the fallback runs |
metadata.sandbox.review | flag_once | the first review round flags the work back, so the redo runs |
Magic values lists every value and its branch. Test clocks move time, so a silent client's two weeks run in a second. A day before the link expires it reminds, a second ask opens, and when that one expires too the job lapses and the hold is voided:
magic values: client.email = silent@example.test
$ npx services.dev signed-translation document=lease.pdf to=fr "client=Ada Lovelace <silent@example.test>" --max 120
job_7Hq2 · working · $120.00 held
job_7Hq2 · waiting · on party · $120.00 held
record translate · ai | harden · ai · adversarial · 2 lenses · pass
next hand http://localhost:8787/a/ask_7Hq2?t=8FaWT5EoCj4h-uMyjlXp3_ to the person it asks
$ npx services.dev tick 6d
tc_7Hq2 · 2026-10-25 14:02:11 → 2026-10-31 14:02:11 · 1 fired
2026-10-31 14:02:11 job_7Hq2 ask.reminded · sign · 1d before expiry
$ npx services.dev tick 1d
tc_7Hq2 · 2026-10-31 14:02:11 → 2026-11-01 14:02:11 · 2 fired
2026-11-01 14:02:11 job_7Hq2 ask.expired · sign
2026-11-01 14:02:11 job_7Hq2 ask sign · silent@example.test · sha256:0f61… · open
$ npx services.dev tick 7d
tc_7Hq2 · 2026-11-01 14:02:11 → 2026-11-08 14:02:11 · 4 fired
2026-11-07 14:02:11 job_7Hq2 ask.reminded · sign · 1d before expiryThe buyer pays $0, and the AI the job ran is yours to bear. To run your brand's whole conformance suite in the same CI job, see Your conformance suite.
check --against live compares your change with the deployed brand and fails breaking_change by the rule Versions sets.