---
title: "In CI"
url: https://services.dev/docs/cli/ci
codes: [not_running]
---
# In CI

URL: https://services.dev/docs/cli/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 every push

`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:

**Text**

```txt
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 console
  ✗ A step no edge reaches never runs. Fix its after, or route there with next. file is routed, and no step has a next, a rejection or a fallback that can send the job there.
    https://services.dev/docs/reference/check/step_unreachable
  ✗ price.release names no step that runs. Name a step that runs. price.release is 'file.accepted', and file never runs.
    https://services.dev/docs/reference/check/release_unproven
  ✗ 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

mcp     14 buyer tools · 5,097 of 8,000 tokens · 9 more with ?toolset=sandbox
✗ 3 to fix
  next    Add the row, or render it. cli listen is service.order's command, and the CLI has none.

AI and money are simulated until you wire them. $0.42 a call is an example.
$ echo $?
1
```

**JSON**

```txt
magic values: none

mcp     14 buyer tools · 5,097 of 8,000 tokens · 9 more with ?toolset=sandbox
$ npx services.dev check --json
{
  "ai": {
    "cents_per_call": 42,
    "simulated": true
  },
  "mcp": {
    "tools": 14,
    "tokens": 5097,
    "sandbox": 9,
    "budget": 8000
  },
  "services": [
    {
      "path": "services/listen.ts",
      "lines": 5,
      "steps": "draft · ai → review · ai · adversarial",
      "ok": false,
      "price": "$19 per job, within 48 hours, charged on delivery.",
      "who": "AI delivers every job.",
      "agent": {
        "tool": "order_listen",
        "words": 22,
        "job": "job_7Hq2",
        "status": "accepted",
        "on": null,
        "charged": 1900,
        "person": false,
        "why": ""
      },
      "ledger": "+$19.00 · AI $0.84 (example) · you $18.16",
      "approved_ledger": null,
      "outcome": null,
      "margin": {
        "floor": {
          "cents": 1520,
          "pct": 80
        },
        "own": 1520,
        "stop": 380,
        "nested": []
      },
      "apps": [
        "customer portal",
        "owner console"
      ],
      "failures": [
        {
          "code": "surface_drift",
          "reason": "A surface serves a call with no row, or lacks a live row.",
          "next": "Add the row, or render it. cli listen is service.order's command, and the CLI has none.",
          "doc_url": "https://services.dev/docs/reference/check/surface_drift"
        }
      ]
    },
    {
      "path": "services/translation.ts",
      "lines": 22,
      "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",
      "ok": false,
      "price": "$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.",
      "agent": {
        "tool": "order_translation",
        "words": 30,
        "job": "job_7Hq2",
        "status": "waiting",
        "on": "party",
        "charged": null,
        "person": true,
        "why": ""
      },
      "ledger": "+$89.00 · AI $1.26 (example) · sworn translator $30.00 · you $57.74",
      "approved_ledger": null,
      "outcome": null,
      "margin": {
        "floor": {
          "cents": 4120,
          "pct": 46
        },
        "own": 4120,
        "stop": 4780,
        "nested": []
      },
      "apps": [
        "customer portal",
        "delivery portal",
        "owner console"
      ],
      "failures": [
        {
          "code": "step_unreachable",
          "reason": "A step no edge reaches never runs.",
          "next": "Fix its after, or route there with next. file is routed, and no step has a next, a rejection or a fallback that can send the job there.",
          "doc_url": "https://services.dev/docs/reference/check/step_unreachable"
        },
        {
          "code": "release_unproven",
          "reason": "price.release names no step that runs.",
          "next": "Name a step that runs. price.release is 'file.accepted', and file never runs.",
          "doc_url": "https://services.dev/docs/reference/check/release_unproven"
        },
        {
          "code": "surface_drift",
          "reason": "A surface serves a call with no row, or lacks a live row.",
          "next": "Add the row, or render it. cli listen is service.order's command, and the CLI has none.",
          "doc_url": "https://services.dev/docs/reference/check/surface_drift"
        }
      ]
    }
  ]
}
$ echo $?
1
```

Only `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`](https://services.dev/docs/reference/check/step_unreachable.md), [`release_unproven`](https://services.dev/docs/reference/check/release_unproven.md) and [`surface_drift`](https://services.dev/docs/reference/check/surface_drift.md).

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 |

## Fix it, and check again

```diff
$ 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
+++ b/services/translation.ts
@@ -19,7 +19,6 @@ export default Service({
       rejected: { missing_certificate: 'correct', '*': 'translate' },
       perfect: '2d',
       fallback: 'hand_off',
-      routed: true,
     },
     correct: { ask: 'buyer', to: 'upload', routed: true },
     hand_off: { ask: 'buyer', to: 'answer', routed: true },
```

```txt
$ 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

mcp     14 buyer tools · 5,098 of 8,000 tokens · 9 more with ?toolset=sandbox
✓ ready to sell · 2 Services · AI delivers 1 alone · each keeps at least 46% of every job
  next    npx services.dev dev

AI and money are simulated until you wire them. $0.42 a call is an example.
$ echo $?
0
```

## Exit codes

Every command's exit code is its failure's `category`, so a pipeline branches without parsing words:

```txt
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:

```txt
$ 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 $?
10
```

## Every branch, with no person and no wait

A 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](https://services.dev/docs/reference/magic-values.md) 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:

```txt
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 expiry
  2026-11-08 14:02:11  job_7Hq2  ask.expired · sign
  2026-11-08 14:02:11  job_7Hq2  job.ended · lapsed
  2026-11-08 14:02:11  job_7Hq2  charge.voided · $120.00

$ npx services.dev get job_7Hq2
job_7Hq2 · ended · lapsed · lease.fr.pdf · $0
  record  translate · ai | harden · ai · adversarial · 2 lenses · pass | sign · reminded 1d before expiry | sign · expired | sign · reminded 1d before expiry | sign · expired | lapsed · ended · clock · sign expired twice.
  ledger  AI $1.26 · you −$1.26
```

The 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](https://services.dev/docs/api/verify.md).

## Against what's live

`check --against live` compares your change with the deployed brand and fails `breaking_change` by [the rule Versions sets](https://services.dev/docs/operate/versions.md#what-counts-as-breaking).

---

- Up: [The CLIs](https://services.dev/docs/cli.md)
- Previous: [Install and log in](https://services.dev/docs/cli/install.md)
- Next: [Listening locally](https://services.dev/docs/cli/listen.md)
