---
title: "Grants and decisions"
url: https://services.dev/docs/service/grants
captured: [service-consent.txt, service-grant-tabs.txt, service-grant.txt, tax-check.txt]
codes: [needs_grant, needs_the_party]
---
# Grants and decisions

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

> A grant ask is a scoped, expiring permission the party gives through their own link. A step that needs it is refused needs_grant until it is live, and asked for again once it expires or the party revokes it. A decide ask is the client's choice among yours, and it is merits.

A grant answers one question: what may the brand do in the party's name, and until when?

## Smallest form

Two lines: the permission the client gives, and a step that needs it.

```ts title="services/records-request.ts"
    access: { ask: 'client', to: 'grant', scope: { acts: ['request', 'follow up'], for: '14d' }, how: 'esign', after: [] },
    request: { submit: 'https://registry.example.test/requests', to: 'the registry', receipt: 'receipt', needs: 'access', after: 'draft' },
```

Until the client grants it, the gate refuses the request and hands the link to whoever carries it to them:

```txt
→ POST /records-request {"client":{"name":"Ada Lovelace","email":"ada@example.com"},"reply_by":"2026-11-30","max_usd":79,"key":"grant","test_clock":"tc_7Hq3"}
```

```txt title="captured/service-grant.txt · magic values: none"
← job_7Hq2 · waiting · on party · $79.00 held
  record  draft · ai | request · outside · refused · needs_grant · request needs the access grant live, and the party hasn't given it yet. | review · ai · pass
  next    hand_to_person http://localhost:8787/a/ask_7Hq2?t=8FaWT5EoCj4h-uMyjlXp3_ · grant
```

## Every form

**On an ask step**

| Key | Written | Left out | Does |
|---|---|---|---|
| `ask` | `'buyer' \| <party input>` | required | Who acts: the buyer, or a `party`, `party?` or `party[]` input. |
| `to` | `AskTo` | required | What the party does: `'sign'`, `'consent'`, `'answer'`, `'upload'`, `'decide'`, `'direct'` or `'grant'`. |
| `scope` | `{ acts: string[]; for: Within }` | live until revoked | For `grant`: the acts it permits, and how long it stays live once given. Its state (live, expired, revoked) comes from the record; the party revokes it through their own link. |
| `how` | `'click' \| 'esign' \| 'id-verified' \| 'notarized'` | `'click'` | How the act is proven, recorded with it. |
| `choices` | `string[]` | none | For `decide`: the choices. A decision is merits, the client's alone. |
| `expires` | `Within` | `'7d'` | When the link stops working; a second expiry lapses the job. |

**On a step**

| Key | Written | Left out | Does |
|---|---|---|---|
| `needs` | `<step>` | none | A `grant` ask whose permission must be live when this step runs, or a submit sends. Until the party grants it, and again once it expires or they revoke it, the gate refuses the step `needs_grant` and the party is asked again. |

## Granted, the step runs

The client grants it through their own link. The grant records the acts it covers and when it stops being live, and the refused step runs:

```txt
→ POST /asks/ask_7Hq2?t=8FaWT5EoCj4h-uMyjlXp3_ {}
```

```txt title="captured/service-grant.txt · 200 · ask_7Hq2 · grant · done · magic values: none"
  grant   ask_7Hq2 · access · request, follow up · live · until 2026-11-08T14:02:11.000Z
← job_7Hq2 · waiting · on person · $79.00 charged
  record  … access · client · grant · esign | request · the registry · accepted · rcp_7Hq2
```

## Asked again

A grant runs out at its `for`. A step that needs it after that is refused `needs_grant`, and the client is asked again. When a clock's rung runs that step, the rung is held: it escalates to the owner, stays armed and proposes again each day, so the work goes on the moment the client grants it again.

```txt
→ POST /test_clocks/tc_7Hq3/advance {"to":"2026-11-21T00:00:00Z"}
```

```txt title="captured/service-grant.txt · magic values: none"
  2026-11-08 14:02:11  grant.expired · access · until 2026-11-08
  2026-11-20 00:00:00  clock reply_by · rung run follow_up 10d → follow_up proposed
  2026-11-20 00:00:00  clock reply_by · rung run follow_up 10d · held: refused:needs_grant, proposes again 2026-11-21
  2026-11-20 00:00:00  clock reply_by · rung run follow_up 10d · escalated to the owner: follow_up is held: refused:needs_grant · owner buyer
  2026-11-20 00:00:00  ask access · ada@example.com · open
  2026-11-20 00:00:00  waits on party at follow_up
  2026-11-21 00:00:00  clock reply_by · rung run follow_up 10d → follow_up proposed
  2026-11-21 00:00:00  clock reply_by · rung run follow_up 10d · held: refused:needs_grant, proposes again 2026-11-22
  2026-11-21 00:00:00  waits on party at follow_up
← job_7Hq2 · waiting · on party · $79.00 charged
  record  … access · grant expired | reply_by clock · run follow_up 10d · goto follow_up | follow_up · ai · refused · needs_grant · follow_up needs the access grant live, and it has expired. | reply_by clock · run follow_up 10d · held: refused:needs_grant | reply_by clock · run follow_up 10d · goto follow_up | follow_up · ai · refused · needs_grant · follow_up needs the access grant live, and it has expired. | reply_by clock · run follow_up 10d · held: refused:needs_grant
  next    hand_to_person http://localhost:8787/a/ask_7Hq3?t=rE5VINsEvPPB93bDQLooDc · grant
```

## Only the party revokes

No key revokes a grant for the party, delegated or not. Their own link does, and it stops being live at once:

```txt
→ POST /asks/ask_7Hq3/revoke   # x-services-test-dlg: dlg_test_4Rb1
```

```txt title="captured/service-grant.txt · magic values: none"
← 403 needs_the_party: Only Ada Lovelace can revoke the access grant, through their own link. Hand them the link in next.
```

```txt
→ POST /asks/ask_7Hq3/revoke
```

```txt title="captured/service-grant.txt · magic values: none"
← 403 needs_the_party: Only Ada Lovelace can revoke the access grant, through their own link. Hand them the link in next.
```

```txt
→ POST /asks/ask_7Hq3/revoke?t=rE5VINsEvPPB93bDQLooDc {}
```

```txt title="captured/service-grant.txt · 200 · ask_7Hq3 · grant · done · magic values: none"
  grant   ask_7Hq3 · access · request, follow up · revoked · until 2026-12-05T00:00:00.000Z · revoked 2026-11-21T00:00:00.000Z
← job_7Hq2 · waiting · on person · $79.00 charged
  record  … access · grant revoked · party
```

Every grant on a job, and its state, is on the job, on every surface:

**CLI**

```bash title="CLI"
npx services.dev jobs get job_7Hq2 --json   # its status, wait, grants and first next
```

```txt title="captured/service-grant-tabs.txt · magic values: none"
  "status": "waiting",
    "step": "follow_up",
    "clock": "paused"
      "step": "access",
      "until": "2026-11-08T14:02:11.000Z",
      "status": "expired"
    "do": "hand_to_person",
    "url": "http://localhost:8787/a/ask_7Hq3?t=rE5VINsEvPPB93bDQLooDc",
```

**API**

```bash title="API"
curl http://localhost:8787/jobs/job_7Hq2   # its status, wait, grants and first next
```

```txt title="captured/service-grant-tabs.txt · magic values: none"
  "status": "waiting",
    "step": "follow_up",
    "clock": "paused"
      "step": "access",
      "until": "2026-11-08T14:02:11.000Z",
      "status": "expired"
    "do": "hand_to_person",
    "url": "http://localhost:8787/a/ask_7Hq3?t=rE5VINsEvPPB93bDQLooDc",
```

**MCP**

```txt title="MCP"
→ tools/call get_job {"id":"job_7Hq2"}   # its status, wait, grants and first next
```

```txt title="captured/service-grant-tabs.txt · magic values: none"
  "status": "waiting",
    "step": "follow_up",
    "clock": "paused"
      "step": "access",
      "until": "2026-11-08T14:02:11.000Z",
      "status": "expired"
    "do": "hand_to_person",
    "url": "http://localhost:8787/a/ask_7Hq3?t=rE5VINsEvPPB93bDQLooDc",
```

**SDK**

```ts title="SDK"
await lingua.jobs.get("job_7Hq2")   // its status, wait, grants and first next
```

```txt title="captured/service-grant-tabs.txt · magic values: none"
  "status": "waiting",
    "step": "follow_up",
    "clock": "paused"
      "step": "access",
      "until": "2026-11-08T14:02:11.000Z",
      "status": "expired"
    "do": "hand_to_person",
    "url": "http://localhost:8787/a/ask_7Hq3?t=rE5VINsEvPPB93bDQLooDc",
```

An agent never grants: over MCP the job's next is `hand_to_person`, with the link ([What an agent can never do](https://services.dev/docs/mcp/never.md)). Acting before a regulator in someone's name is often licensed work. A grant to a step a credentialed role performs is how a Service meets that by design: the client authorizes, the member acts under their credential, and the record holds both.

## Decisions

`to: 'decide'` asks the client to choose among `choices`. A decision is merits: once committed, [the gate](https://services.dev/docs/service/gate.md) never undoes it on mechanics, and only the client makes it.

```ts title="services/private-translation.ts"
    register: { ask: 'client', to: 'decide', choices: ['formal', 'informal'] },
```

```txt
→ POST /private-translation {"document":"memo.pdf","to":"de","client":{"name":"Ada Lovelace","email":"signs@example.test"},"max_usd":59,"key":"consents","metadata":{"sandbox":{"asks":{"register":"informal"}}}}
```

```txt title="captured/service-consent.txt · magic values: client.email = signs@example.test, metadata.sandbox.asks = { register: informal }, client.email = declines@example.test"
← job_7Hq2 · delivered · memo.de.pdf · $59.00 charged
  record  consent · client · consent | register · client · decide · decided informal | translate · ai | review · ai · pass
  ledger  +$59.00 · AI $0.84 · you $58.16
```

Choices the buyer sets inside your bounds, and ones only the client sets, are in [Policy](https://services.dev/docs/service/policy.md).

## Seven surfaces

| Op | API | CLI | MCP | SDK | Site | Docs | App |
|---|---|---|---|---|---|---|---|
| `job.get` | `GET /jobs/:id` | `jobs get <job>` | `get_job` | `jobs.get()` |  | 1 error page | customer portal |
| `ask.revoke` | `POST /asks/:id/revoke` |  | never: a person acts | never: a person acts |  | 3 error pages | hosted link |

Events: `grant.given`, `grant.expired`, `grant.revoked`.

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

## Refusals

| Code | Category | HTTP | Why |
|---|---|---|---|
| [`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. |
| [`needs_the_party`](https://services.dev/docs/reference/errors.md#needs_the_party) | blocked | 403 | Only the party can do this, through their own link. |

| When the file won't load | Thrown by | Why |
|---|---|---|
| [`needs_unknown`](https://services.dev/docs/reference/definition-errors.md#needs_unknown) | `Service()` | Service Renewal: extend needs draft, which isn't a grant ask. needs names a step written &#123; ask, to: 'grant' &#125;, and this Service has none. |

| Written | What TypeScript says |
|---|---|
| `input: { client: 'party' }, output: { document: 'file' }, deliver: { authorize: { ask: 'client', to: 'grant', scope: { acts: ['file'], for: '30d' } }, file: { by: 'ai', needs: 'authorise' } }, price: 49` | `Type '"authorise"' is not assignable to type '"authorize" \| "file" \| undefined'. Did you mean '"authorize"'?` |

## 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="tax · services/notice-response.ts · 19 lines"
    poa: { ask: 'owner', to: 'grant', after: 'respond.claim', what: 'Form 2848', names: { claimant: 'caf' }, scope: job => ({ acts: ['represent', 'send'], matters: [{ tax: 'income', form: '1120', periods: job.input.tax_years }], for: '365d' }), how: 'esign', capacity: 'officer', withdrawn: ['accepted', 'for'] },
    respond: { by: 'representative', act: 'represent', needs: 'poa', stamp: true, est: '1h', drafts: { misapplied: 'a transfer request citing the receipt', penalty: 'a request to abate', extend: 'a Form 872 limited to the issues and a date' } },
    send: { submit: 'https://correspondence.example.test/replies', to: 'the agency', receipt: 'receipt', needs: 'poa' },
```

```txt title="npx services.dev check · captured/tax-check.txt"
services/notice-response.ts · 19 lines · $299 per job, or $99 if you answer it yourself, or 25% of a penalty abated, $99 at least, within 48 hours, charged on send's receipt.
  steps   read · ai · cites source → match · code · cites each payment's receipt, and the period it was for → classify · code · agree to pay, the rest to respond → review · ai · adversarial → respond · representative · * · stamped → poa · owner · grant · Form 2848, after respond.claim → send · the agency → pay · Treasury · routed
```

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

[Notice response](https://services.dev/docs/examples/tax/notice-response.md) runs a grant, the steps that need it and a representative's stamped response.

---

- Up: [Service()](https://services.dev/docs/service.md)
- Previous: [Asks: a ceremony without an account](https://services.dev/docs/service/ask.md)
- Next: [Intake and request lists](https://services.dev/docs/service/intake.md)
