---
title: "Propose, then the gate commits"
url: https://services.dev/docs/service/gate
captured: [ask.txt, brand-credential.txt, fallback.txt, legal-decline.txt, legal-region.txt, service-ends.txt, service-guard.txt, service-policy.txt, terminal.txt]
codes: [brand_unverified, end_not_allowed, guard_refused, job_ended, merits_not_yours, needs_a_person, needs_direction, needs_grant, not_waiting, policy_invalid, region_not_ratified, reserved]
---
# Propose, then the gate commits

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

> AI, your code, a person and an outside system all propose. One gate runs seven checks in order and commits or refuses, the same way for everyone.

The gate answers one question: may this output become part of the job?

No step writes to a job directly. Every doer hands the gate a proposal: the AI, your code, a URL, a nested Service, a person and an outside system's answer. The gate is the only code that writes a step's output to the record. On a pass it writes `step.committed`. On a refusal it writes `step.refused` with a code, and the doer's work stays open.

## Seven checks, in order

| # | Check | Holds when | On the record | A person gets | Explained in |
|---|---|---|---|---|---|
| 1 | terminal | The job is not at an end. | `job_ended` | `job_ended` | [The job lifecycle](https://services.dev/docs/service/lifecycle.md) |
| 2 | direction | No decline awaits the client's direction. | `needs_direction` | `needs_direction` | [Declines and direction](https://services.dev/docs/people/direction.md) |
| 3 | graph | The graph allows the step, and any grant it needs is live. | `step_not_ready`, `needs_grant` | `not_waiting`, `needs_grant` | [The step graph](https://services.dev/docs/service/graph.md) |
| 4 | mechanics | Every policy value within its bounds. A client's decide is never undone here (5b). | `policy_invalid` | `policy_invalid` | [Policy](https://services.dev/docs/service/policy.md) |
| 5 | merits | Merits come only from the client, and the guard checks the output against them. | `merits_not_yours`, `guard_refused` | `merits_not_yours`, `guard_refused` | [Guards](https://services.dev/docs/service/guards.md) |
| 6 | authority | The rules row, never the sign line, under the job's pinned version. A reserved act commits only from a holder of its credential, in the region it derives and where the row is ratified, with every brand credential it names verified. It carries its attestation, every checklist item affirmed, over the output as it stands now. | `unattested`, `reserved`, `region_not_ratified`, `brand_unverified` | `needs_a_person`, `reserved`, `region_not_ratified`, `brand_unverified` | [Rules](https://services.dev/docs/people/rules.md) |
| 7 | proof | An end that charges has its release fact on the record. | `release_unproven` | `end_not_allowed` | [How a job ends](https://services.dev/docs/service/ends.md) |

Any check can refuse, and the first that does decides the answer.

**1. Terminal.** An end is forever. Ending the job again answers `end_not_allowed`, and any other act on it answers `job_ended`:

```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.",
```

```txt
→ POST /jobs/job_7Hq2/messages {"text":"Can you still change page 2?"}
```

```txt title="409 · magic values: none"
  "code": "job_ended",
  "reason": "The job has ended, for good.",
```

**2. Direction.** A member's decline holds the job until the client directs it, to another member or to an end. While it waits, nothing else moves the job, from your console or the buyer's key:

```txt
→ POST /owner/matters/job_7Hq2/approve   # the owner console
```

```txt title="magic values: none"
← 409 needs_direction: A signer declined, and only the client's direction moves the matter on.
```

[Declines and direction](https://services.dev/docs/people/direction.md) walks the decline, the client's link and the member they name.

**3. The graph.** A step runs only when every step and ask it follows is on the record. Translation waits for the buyer's glossary before it starts:

```txt
→ POST /translation {"document":"lease.pdf","to":"fr-CA","max_usd":49,"key":"glossary"}
```

```txt title="magic values: none"
← job_7Hq2 · waiting · on party · $49.00 held
  waiting {"on":"party","since_at":"2026-10-25T14:02:11.000Z","step":"glossary","clock":"paused"}
```

**4. Mechanics.** Every policy value sits inside its bounds. An order that breaks them hears every violation at once:

```txt
→ POST /checked-translation {"document":"manual.pdf","to":"de","policy":{"match":1.5,"turnaround":"fast"},"max_usd":79,"key":"bad"}
```

```txt title="422 · magic values: none"
  "code": "policy_invalid",
  "reason": "The policy breaks 2 bounds: match must be at most 1; turnaround must be a number.",
```

**5a. Merits.** Merits come only from the client, and a step's guard checks the output against them. The brand can't set one, and an output below one is refused, whoever proposed it:

```txt
→ POST /owner/jobs/job_7Hq2/policy {"policy":{"match":0.7}}
```

```txt title="403 · magic values: none"
  "code": "merits_not_yours",
  "reason": "match binds the merits, and only the client sets them.",
```

**5b.** A client's committed decision is never undone on mechanics.

**6. Authority.** The rules row the job pinned decides, never the sign line. Where the row isn't ratified, the order is refused at $0:

```txt
→ POST /matters {"service":"contract-review","contract":{"url":"data:text/plain,A%20lease.","name":"lease.pdf"},"client":{"name":"Ada Lovelace","email":"ada@example.com"},"client_region":"US-FL","max_usd":99,"key":"lease-fl"}
```

```txt title="403 · magic values: none"
  "code": "region_not_ratified",
  "reason": "advise is ratified in US-CA, US-NY, US-TX under rules 2026-10, and this order's region is US-FL.",
```

So is an act under a brand credential that isn't verified and fresh:

```txt
→ POST /jobs {"service":"extension","year":2025,"max_usd":25,"key":"stale","metadata":{"sandbox":{"brand":"stale"}}}
```

```txt title="403 · magic values: metadata.sandbox.brand = verified, metadata.sandbox.brand = stale"
  "code": "brand_unverified",
  "reason": "file is performed under the brand's efin, which isn't verified and fresh.",
```

**7. Proof.** An end that charges has its release fact on the record. With no receipt, the filing that fell back to the buyer charges nothing when they accept it:

```txt
→ POST /jobs/job_7Hq2/accept
```

```txt title="magic values: metadata.sandbox.outside = never"
← job_7Hq2 · accepted · lease.fr-CA.pdf · $0
  ledger  AI $0.84 · you −$0.84
```

## The same answer for AI and people

The AI's refused proposal is redone inside its budget. A person's gets 422 `guard_refused`, and the step stays theirs:

```txt
→ POST /proofreaders/jobs/job_7Hq2/approve {"output":{"match":0.8}}
```

```txt title="422 · magic values: metadata.sandbox.propose = { match: [0.7, 0.95] }"
  "code": "guard_refused",
  "reason": "match: below the match the client set (0.8 < 0.9)",
```

## Two sovereigns

The gate holds both: a member's decline at check 2, and the client's yes on their merits at 5a and 5b. [Two sovereigns](https://services.dev/docs/people/sovereignty.md) explains why neither can be overridden.

## Seven surfaces

| Op | API | CLI | MCP | SDK | Site | Docs | App |
|---|---|---|---|---|---|---|---|
| `job.approve` | `POST /:portal/jobs/:id/approve`, `POST /jobs/:id/approve`, `POST /jobs/:id/sign`, `POST /owner/jobs/:id/approve` | `jobs approve <job>` | never: a person acts | never: a person acts |  | 8 error pages | owner console, delivery portal |

Events: `step.refused`, `job.paused`.

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

## Refusals

| Code | Category | HTTP | Why |
|---|---|---|---|
| [`job_ended`](https://services.dev/docs/reference/errors.md#job_ended) | conflict | 409 | The job has ended, for good. |
| [`needs_direction`](https://services.dev/docs/reference/errors.md#needs_direction) | conflict | 409 | A member declined, and only the client's direction moves the job on: another member, or an end. |
| [`not_waiting`](https://services.dev/docs/reference/errors.md#not_waiting) | conflict | 409 | The job isn't waiting on a person. |
| [`needs_grant`](https://services.dev/docs/reference/errors.md#needs_grant) | blocked | 403 | A grant this step needs isn't live: not given yet, expired or revoked. |
| [`policy_invalid`](https://services.dev/docs/reference/errors.md#policy_invalid) | invalid_request | 422 | A policy value is out of bounds; the reason lists each. |
| [`merits_not_yours`](https://services.dev/docs/reference/errors.md#merits_not_yours) | blocked | 403 | Only the client sets the merits. |
| [`guard_refused`](https://services.dev/docs/reference/errors.md#guard_refused) | guard | 422 | A guard refused the output, and says why. |
| [`needs_a_person`](https://services.dev/docs/reference/errors.md#needs_a_person) | blocked | 403 | A person approves and signs, in the app. A key or a tool never can. |
| [`reserved`](https://services.dev/docs/reference/errors.md#reserved) | blocked | 403 | The rules reserve this act to a credential the person who attested doesn't hold. Nothing was charged. |
| [`region_not_ratified`](https://services.dev/docs/reference/errors.md#region_not_ratified) | unavailable | 403 | The rules don't ratify this act in the job's region yet. Nothing was charged. |
| [`brand_unverified`](https://services.dev/docs/reference/errors.md#brand_unverified) | unavailable | 403 | A credential the brand itself holds is unverified or stale, so the act it covers waits. Nothing was charged. |
| [`end_not_allowed`](https://services.dev/docs/reference/errors.md#end_not_allowed) | conflict | 409 | The job can't end that way now. |

---

- Up: [Service()](https://services.dev/docs/service.md)
- Previous: [Another Service as a step](https://services.dev/docs/service/nested.md)
- Next: [Guards](https://services.dev/docs/service/guards.md)
