Ask the buyer

The running jobjob_7Hq2 · accepted · document.fr-CA.pdf · $49.00 final

Add an ask step. The buyer answers one question through a link that needs no account, and the job's clock pauses until they do.

Some work needs something only a person on the buyer's side can give: an answer, a file, a signature, consent or a decision. An ask step puts it to them through a hosted link that needs no account, and the job's deadline pauses until they act. Here the buyer says which terms stay in English. This is Grow 2.

check counts 9 lines:

grow/ask.ts
import { Service } from 'services.dev'

// Grow 2, ask the buyer: before the AI starts, the buyer says which terms stay in English, through a
// link that needs no account. The job waits on them, and its clock pauses until they answer.
export default Service({
  name: 'Translation',
  input: { document: 'file', to: 'language: a BCP 47 tag, such as fr-CA' },
  output: { document: 'file' },
  deliver: {
    glossary: { ask: 'buyer', to: 'answer', items: () => [{ id: 'terms', label: 'Which terms stay in English?' }] },
    translate: 'ai',
    harden: { review: 'adversarial', lenses: ['meaning', 'terms'], rounds: 2 },
  },
  price: 49,
})

glossary is an ask. It's written first, so it runs first, and translate waits for it.

KeyWhat it doesDefault
askWho acts: 'buyer', or a party input such as a co-signer (party? when optional, party[] for several)required
toWhat they do: answer, upload, sign, consent, decide, direct, or grant, a scoped and expiring permission a later step needs (Grants)required
itemsA request list, a function of the job, so the questions can follow the input. Each item is answered, waived with a reason, or substituted; a step can start on one with after: 'glossary.terms'. Request lists derives one from the job and its subject deliver.itemsnone
expiresWhen the link stops working. The ask then opens once more, with a new link; a second expiry lapses the job, which charges only the acts already committed7d
remindReminders, each this long before the link expires['1d']
choicesFor decide: the options the party picks from. A decision is theirs alonenone
howHow the act is proven: 'click', 'esign', 'id-verified' or 'notarized', kept on the record'click'

Asks lists the rest: what a signature binds, all for several parties, capacity and void.

Run
npx services.dev check
magic values: none
services/translation.ts · 9 lines · $49 per job, within 48 hours, charged on delivery.
  who     AI delivers every job. The buyer answers at glossary.
  steps   glossary · buyer · answer → translate · ai → harden · ai · adversarial · 2 lenses
  agent   order_translation · 29 of 30 words · job_7Hq2 waits on party
  ledger  +$49.00 · AI $1.26 (example) · you $47.74
  margin  at least $39.20 a job (80%) · a job that stops costs you at most $9.80
  apps    customer portal · owner console

mcp     13 buyer tools · 4,494 of 8,000 tokens · 9 more with ?toolset=sandbox
✓ ready to sell · 1 Service · it keeps at least 80% of every job
  next    npx services.dev dev

AI and money are simulated until you wire them. $0.42 a call is an example.

The who line names the step where the buyer answers, and the agent line says an order now waits on a party. The margin doesn't move: asking costs nothing.

No magic value is needed. In this capture the buyer answers two hours after the order; on localhost, dev serves the link, so open the one the CLI prints and answer it yourself.

npx services.dev translation document=https://example.com/lease.pdf to=fr-CA --max 49 --key grow
job_7Hq2 · working · $49.00 held
job_7Hq2 · waiting · on party · $49.00 held
  record  
  next    hand http://localhost:8787/a/ask_7Hq2?t=8FaWT5EoCj4h-uMyjlXp3_ to the person it asks

# 2 hours later, the buyer answers through the link
→ POST /asks/ask_7Hq2?t=8FaWT5EoCj4h-uMyjlXp3_ {"items":{"terms":{"answer":"Lessee, Lessor, Premises"}}}

$ npx services.dev jobs wait job_7Hq2
job_7Hq2 · delivered · lease.fr-CA.pdf · $49.00 charged
curl -X POST http://localhost:8787/jobs -H 'content-type: application/json' -d '{"service":"translation","document":{"url":"https://example.com/lease.pdf","name":"lease.pdf"},"to":"fr-CA","key":"grow","max_usd":49}'
  next    get_job {"id":"job_7Hq2"}

$ curl http://localhost:8787/jobs/job_7Hq2?wait=50
← 200 · job_7Hq2 · waiting · on party · $49.00 held
  next    hand_to_person {"ask":"ask_7Hq2"}

# 2 hours later, the buyer answers through the link
→ POST /asks/ask_7Hq2?t=8FaWT5EoCj4h-uMyjlXp3_ {"items":{"terms":{"answer":"Lessee, Lessor, Premises"}}}

$ curl http://localhost:8787/jobs/job_7Hq2?wait=50
← 200 · job_7Hq2 · delivered · lease.fr-CA.pdf · $49.00 charged
→ tools/call order_translation {"document":{"url":"https://example.com/lease.pdf","name":"lease.pdf"},"to":"fr-CA","key":"grow","max_usd":49}
← job_7Hq2 · working · $49.00 held
  next    get_job {"id":"job_7Hq2"}

→ resources/read job://job_7Hq2?wait=50
← job_7Hq2 · waiting · on party · $49.00 held
  next    hand_to_person {"ask":"ask_7Hq2"}

# 2 hours later, the buyer answers through the link
→ POST /asks/ask_7Hq2?t=8FaWT5EoCj4h-uMyjlXp3_ {"items":{"terms":{"answer":"Lessee, Lessor, Premises"}}}

→ resources/read job://job_7Hq2?wait=50
← job_7Hq2 · delivered · lease.fr-CA.pdf · $49.00 charged
await lingua.translation.order({"document":{"url":"https://example.com/lease.pdf","name":"lease.pdf"},"to":"fr-CA","key":"grow","max_usd":49})
← job_7Hq2 · working · $49.00 held
  next    get_job {"id":"job_7Hq2"}

await lingua.jobs.wait("job_7Hq2")
← job_7Hq2 · waiting · on party · $49.00 held
  next    hand_to_person {"ask":"ask_7Hq2"}

# 2 hours later, the buyer answers through the link
→ POST /asks/ask_7Hq2?t=8FaWT5EoCj4h-uMyjlXp3_ {"items":{"terms":{"answer":"Lessee, Lessor, Premises"}}}

await lingua.jobs.wait("job_7Hq2")
← job_7Hq2 · delivered · lease.fr-CA.pdf · $49.00 charged

Every surface answers the same way: the job waits on: party, and its next step is hand_to_person, with the link. An answer or upload may also come from an agent holding the party's delegated key; sign, consent and decide need the party's own link, and any other caller is refused needs_the_party. The record:

Call
→ POST /translation {"document":"lease.pdf","to":"fr-CA","key":"grow","max_usd":49}
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"}
# 2 hours later, the buyer answers through the link
→ POST /asks/ask_7Hq2?t=8FaWT5EoCj4h-uMyjlXp3_ {"items":{"terms":{"answer":"Lessee, Lessor, Premises"}}}

← job_7Hq2 · delivered · lease.fr-CA.pdf · $49.00 charged
  record  glossary.terms · answered | glossary · buyer · answer · clock paused 2h | translate · ai | harden · ai · adversarial · 2 lenses · pass
  ledger  +$49.00 · AI $1.26 · you $47.74

waiting reads "clock":"paused", and the record keeps the two hours as clock paused 2h, so the wait doesn't count against the 48-hour deadline. Deadlines lists everything that pauses it.

Run
npx services.dev diff grow/review.ts grow/ask.ts
magic values: none
grow/review.ts (8 lines) → grow/ask.ts (9 lines)
Translation · changed deliver, human · adds an ask step: a party acts through a hosted link

API   ~ operation service.order.translation  POST /translation  item.description
      ~ operation service.quote.translation  GET /translation.json  answers.deliver, answers.human.sentence, answers.surfaces.description
      + operation ask.get                    GET /asks/{id}
      + operation ask.complete               POST /asks/{id}
      + operation ask.reissue                POST /asks/{id}/reissue
      ~ operation brand.manifest             GET /manifest.json  answers.services
      + object ask_                          ask
      + 10 events                            job.paused, job.ended, ask.opened, item.done, ask.reminded, ask.done, ask.expired, ask.voided, ask.relinked, consent.given
      + 4 errors                             needs_the_party, ask_expired, ask_voided, signature_void
CLI   + operation ask.get                    services.dev asks get <ask>
      + operation ask.reissue                services.dev asks reissue <ask>
      + 10 events                            job.paused, job.ended, ask.opened, item.done, ask.reminded, ask.done, ask.expired, ask.voided, ask.relinked, consent.given
      + 4 errors                             needs_the_party, ask_expired, ask_voided, signature_void
MCP   ~ operation order_translation          order_translation  description
      + operation reissue_ask                reissue_ask
SDK   + operation ask.reissue                asks.reissue()
      ~ object job_ (translation)            Job<"translation">  + stage: "glossary" | "translate" | "harden"

An ask is the first key that adds calls: ask.get, ask.complete and ask.reissue, the hosted page at /a/:id, ten events and four errors, each on every surface that reaches it.

In a brand, the legal example asks the client for consent before any work starts, and for a decision when a reply comes back.

Add a signer: a sworn translator signs every job.