Grants and decisions

The running jobjob_7Hq2 · working · $49.00 held

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.

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:

captured/service-grant.txt · magic values: none
→ 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_ · grant

On an ask step

KeyWrittenLeft outDoes
ask'buyer' | <party input>requiredWho acts: the buyer, or a party, party? or party[] input.
toAskTorequiredWhat the party does: 'sign', 'consent', 'answer', 'upload', 'decide', 'direct' or 'grant'.
scope{ acts: string[]; for: Within }live until revokedFor 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.
choicesstring[]noneFor decide: the choices. A decision is merits, the client's alone.
expiresWithin'7d'When the link stops working; a second expiry lapses the job.

On a step

KeyWrittenLeft outDoes
needs<step>noneA 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:

captured/service-grant.txt · 200 · ask_7Hq2 · grant · done · magic values: none
→ 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_7Hq2

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.

captured/service-grant.txt · magic values: none
→ 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:

captured/service-grant.txt · magic values: none
→ 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.
captured/service-grant.txt · magic values: none
→ 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.
captured/service-grant.txt · 200 · ask_7Hq3 · grant · done · magic values: none
→ 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 · party

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

captured/service-grant-tabs.txt · magic values: none
$ 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",
captured/service-grant-tabs.txt · magic values: none
$ 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",
captured/service-grant-tabs.txt · magic values: none
→ 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",
captured/service-grant-tabs.txt · magic values: none
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.

services/private-translation.ts
    register: { ask: 'client', to: 'decide', choices: ['formal', 'informal'] },
captured/service-consent.txt · magic values: client.email = signs@example.test, metadata.sandbox.asks = { register: informal }, client.email = declines@example.test
→ 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.16

Choices the buyer sets inside your bounds, and ones only the client sets, are in Policy.

OpAPICLIMCPSDKSiteDocsApp
job.getGET /jobs/:idjobs get <job>get_jobjobs.get()1 error pagecustomer portal
ask.revokePOST /asks/:id/revokenever: a person actsnever: a person acts3 error pageshosted link

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

Operations has every column of every row.

CodeCategoryHTTPWhy
needs_grantblocked403A grant this step needs isn't live: not given yet, expired or revoked.
needs_the_partyblocked403Only the party can do this, through their own link.
When the file won't loadThrown byWhy
needs_unknownService()Service Renewal: extend needs draft, which isn't a grant ask. needs names a step written { ask, to: 'grant' }, and this Service has none.
WrittenWhat 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: 49Type '"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.

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' },
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 runs every one of its files, captured.

Notice response runs a grant, the steps that need it and a representative's stamped response.