---
title: "How a job ends"
url: https://services.dev/docs/service/ends
captured: [legal-walk.txt, sales-check.txt, service-ends.txt, tax-check.txt]
codes: [end_not_allowed, not_terminal]
---
# How a job ends

URL: https://services.dev/docs/service/ends

> Every job can be accepted, canceled, lapsed or declined, and a Service declares the rest. Each end says what it charges and who may reach it. An end is forever, and again starts a linked job.

`ends` answers one question: how can this job stop, and what is owed when it does?

## Smallest form

```ts title="services/sworn-translation.ts"
  ends: {
    withdrawn: { charge: 'acts', by: 'client' },
    unreadable: { charge: false, by: 'brand', follow: [translation] },
  },
```

## Every form

The ends every job has. 'accepted' ends the work, so the job is delivered for the buyer to accept; 'canceled' stops it at $0; 'lapsed' is the clock's, after an ask expires twice; and 'declined' is where a party's decline ends it. Both charge only the paid acts committed: a person who attested is never unpaid. Declare `declined` in `ends` to charge it otherwise.

**On the Service**

| Key | Written | Left out | Does |
|---|---|---|---|
| `ends` | `{ <end>: EndSpec }` | none | Ends beyond accepted, canceled, lapsed and declined, each with its charge, who may reach it, and what can follow. |

**Each end**

| Key | Written | Left out | Does |
|---|---|---|---|
| `charge` | `boolean \| 'acts'` | false | true: the price is owed, once its release fact is on the record. false: $0. 'acts': only the paid acts committed are owed. |
| `by` | `EndBy \| EndBy[]` | only the graph's `next` | Who can end the job this way. |
| `follow` | `AnyService[]` | none | The follow-on Services this end offers, through `POST /jobs/:id/again { service }`. |
| `note` | `string` | none | What the end means, as the manifest carries it. |

## What an end charges

`charge: true` owes the price, once its release fact is on the record. `false` owes nothing. `'acts'` owes only the paid acts already committed, so a person who did their part is never unpaid by a later stop. Here the proofreader proofed, then the client withdrew. The buyer ordered for themselves, so the record names the buyer as the client:

```txt
→ POST /jobs/job_7Hq2/end {"end":"withdrawn","reason":"The client withdrew."}
```

```txt title="200 · job_7Hq2 · ended · withdrawn · deed.fr.pdf · $60.00 final · magic values: none"
  record  translate · ai | proof · proofreader | withdrawn · ended · buyer · The client withdrew.
  ledger  +$60.00 · AI $0.42 · proofreader $60.00 · you −$0.42
```

## Who may end it

`by` names who can reach an end through `POST /jobs/:id/end`: the client, the buyer, the brand or the clock. Without `by`, only the graph's `next` reaches it. The buyer can't reach the brand's end:

```txt
→ POST /jobs/job_7Hq3/end {"end":"unreadable","reason":"The scan is blank past page 2."}
```

```txt title="409 · magic values: none"
  "code": "end_not_allowed",
  "reason": "unreadable is reached by the brand, not by the buyer.",
```

The brand can, from its console:

```txt
→ POST /owner/jobs/job_7Hq3/end {"end":"unreadable","reason":"The scan is blank past page 2."}
```

```txt title="200 · job_7Hq3 · ended · unreadable · scan.fr.pdf · $0 · magic values: none"
← 200 · job_7Hq3 · ended · unreadable · scan.fr.pdf · $0
```

## An end is forever

Nothing reopens an ended job. The same call again answers `end_not_allowed`, and its `next` is `again`:

```txt
→ POST /jobs/job_7Hq2/end {"end":"withdrawn"}
```

```txt title="409 · magic values: none"
  "code": "end_not_allowed",
  "reason": "The job already ended at withdrawn, and an end is forever.",
    "do": "again",
    "http": "POST http://localhost:8787/jobs/job_7Hq2/again"
```

## Again, and what follows

`POST /jobs/:id/again` starts a new job with `from` on its record, so the first one's history, and its loss, still count. `{ service }` orders one of the end's `follow` Services instead of the same one. The first call answers 402 with the offer: the price, the terms and the exact call that orders. Sending its `next` orders:

```txt
→ POST /jobs/job_7Hq3/again {"service":"translation"}
```

```txt title="402 · magic values: none"
  "type": "OFFER",
    "sentence": "$49 per job, within 48 hours, charged on delivery."
  "terms": "Charged on delivery. Cancel at $0 until it ships. Send it back with a note and the AI redoes it, or you pay $0. Final 7 days after delivery.",
    "do": "again",
      "max_usd": 49,
      "key": "again-job_7Hq3-translation"
    "http": "POST http://localhost:8787/jobs/job_7Hq3/again"
```

```txt
→ POST /jobs/job_7Hq3/again {"service":"translation","max_usd":49,"key":"again-job_7Hq3-translation"}
```

```txt title="202 · job_7Hq4 · working · $49.00 held · magic values: none"
  from    job_7Hq3
```

The key in `next` is derived from the ended job, so an agent that sends the offer twice still orders once.

## The ends every job has

`accepted` by the buyer, or seven days after delivery. `canceled` at $0. `lapsed` by the clock, after an ask expires twice. `declined` when a party declines an ask. Each status, and every move between them, is on [the job lifecycle](https://services.dev/docs/service/lifecycle.md).

## Seven surfaces

| Op | API | CLI | MCP | SDK | Site | Docs | App |
|---|---|---|---|---|---|---|---|
| `job.end` | `POST /jobs/:id/end`, `POST /owner/jobs/:id/end` | `jobs end <job> <end>` | `end_job` | `jobs.end()` |  | 2 error pages | customer portal, owner console |
| `job.again` | `POST /jobs/:id/again` | `jobs again <job>` | `reorder_job` | `jobs.again()` |  | 7 error pages | customer portal |

Events: `job.ended`.

[Operations](https://services.dev/docs/reference/operations.md) has every column of every row.

## Refusals

| Code | Category | HTTP | Why |
|---|---|---|---|
| [`end_not_allowed`](https://services.dev/docs/reference/errors.md#end_not_allowed) | conflict | 409 | The job can't end that way now. |
| [`not_terminal`](https://services.dev/docs/reference/errors.md#not_terminal) | conflict | 409 | Only an ended job is ordered again. |

| `check` gate | Why |
|---|---|
| [`release_unproven`](https://services.dev/docs/reference/check/release_unproven.md) | price.release names no step that runs. |

## In the example brands

The lines that use it in the example brands, as their files write them, and what `check` read aloud for each file.

```ts title="legal · services/contract-review.ts · 16 lines"
  ends: { withdrawn: { charge: 'acts', by: 'client' } },
```

```txt title="npx services.dev check · captured/legal-walk.txt"
services/contract-review.ts · 16 lines · $99 per job, within 48 hours, charged on acceptance.
  steps   consent · client · consent → flag · ai → attack · ai · adversarial → advise · attorney · client · stamped · 2-item checklist → send · ai
```

[The legal example](https://services.dev/docs/examples/legal.md) runs every one of its files, captured.

```ts title="tax · services/annual-return.ts · 45 lines"
  ends: { accepted: { follow: [amendedReturn], emit: { 'k1.issued': job => job.input.owners ?? [] } } },
  price: { usd: 1_800, within: '5d before file', late: '20%', release: 'file.accepted', lines: { state_fees: { at: 'cost', from: 'file' } } },
```

```txt title="npx services.dev check · captured/tax-check.txt"
services/annual-return.ts · 45 lines · $1,800 per job, filed 5 days before its deadline, 20% off if late, or we pay the penalty and its interest, up to $5,000, after we ask the IRS to take it off, charged when file is accepted.
  steps   consent · owner · consent · §7216, may disclose to a preparer outside the US and a lender you name → organizer · owner · upload → compute · code · cites depreciation and carryforward schedules → draft · ai · cites the workpapers and the records the organizer took → check · ai · adversarial · 3 lenses → sign · signer · * · stamped in the paid-preparer block · 3-item checklist → authorize · owner · sign · Form 8879-CORP or 8879-PE → file · IRS and states → prepare · preparer · * · 1-item checklist · routed → tentative · code · cites the year as the ledger stands, less the estimates paid · routed → pay · Treasury and states · routed → extend · IRS and states · routed
```

[The tax example](https://services.dev/docs/examples/tax.md) runs every one of its files, captured.

```ts title="sales · services/deal.ts · 42 lines"
  ends: { won: { charge: true }, lost: { charge: false, by: ['client', 'brand'] } },
```

```txt title="npx services.dev check · captured/sales-check.txt"
services/deal.ts · 42 lines · nothing on effort; 5% of closed_value at won, proven by the account's payment, held 60 days by bank, 120 by card.
  ends    won · charged · lost · $0 · by the seller or you · its reason no_fit, price, timing, competitor or dark · again starts a new deal, from: the lost one
```

[The sales example](https://services.dev/docs/examples/sales.md) runs every one of its files.

---

- Up: [Service()](https://services.dev/docs/service.md)
- Previous: [Signals](https://services.dev/docs/service/signals.md)
- Next: [The job lifecycle](https://services.dev/docs/service/lifecycle.md)
