Grants and decisions
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?
Two lines: the permission the client gives, and a step that needs it.
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:
→ 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"}
← 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_ · grantOn 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. |
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:
→ POST /asks/ask_7Hq2?t=8FaWT5EoCj4h-uMyjlXp3_ {}
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_7Hq2A 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.
→ POST /test_clocks/tc_7Hq3/advance {"to":"2026-11-21T00:00:00Z"}
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
No key revokes a grant for the party, delegated or not. Their own link does, and it stops being live at once:
→ POST /asks/ask_7Hq3/revoke # x-services-test-dlg: dlg_test_4Rb1
← 403 needs_the_party: Only Ada Lovelace can revoke the access grant, through their own link. Hand them the link in next.→ POST /asks/ask_7Hq3/revoke
← 403 needs_the_party: Only Ada Lovelace can revoke the access grant, through their own link. Hand them the link in next.→ POST /asks/ask_7Hq3/revoke?t=rE5VINsEvPPB93bDQLooDc {}
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 · partyEvery grant on a job, and its state, is on the job, on every surface:
$ npx services.dev jobs get job_7Hq2 --json # its status, wait, grants and first next
"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",$ curl http://localhost:8787/jobs/job_7Hq2 # its status, wait, grants and first next
"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",→ tools/call get_job {"id":"job_7Hq2"} # its status, wait, grants and first next
"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",await lingua.jobs.get("job_7Hq2") // its status, wait, grants and first next
"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). 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.
to: 'decide' asks the client to choose among choices. A decision is merits: once committed, the gate never undoes it on mechanics, and only the client makes it.
register: { ask: 'client', to: 'decide', choices: ['formal', 'informal'] },→ 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"}}}}
← 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.16Choices the buyer sets inside your bounds, and ones only the client sets, are in Policy.
| 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 has every column of every row.
| Code | Category | HTTP | Why |
|---|---|---|---|
needs_grant | blocked | 403 | A grant this step needs isn't live: not given yet, expired or revoked. |
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 | Service() | Service Renewal: extend needs draft, which isn't a grant ask. needs names a step written { ask, to: 'grant' }, 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"'? |
The lines that use it in the example brands, as their files write them, and what check read aloud for each file.
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' },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 · routedThe tax example runs every one of its files, captured.
Notice response runs a grant, the steps that need it and a representative's stamped response.