deliver
Who does the work on each job. Leave it out and AI does it, in one step. Name steps and each can be AI, your code, a person, a party or an outside system.
deliver answers one question: who does the work on each job.
Leave it out. The four-line Translation has one AI step, briefed by services/translation.md, and the review every AI job gets:
npx services.dev check steps draft · ai → review · ai · adversarialName the steps, and each one says who does it:
deliver: {
translate: 'ai',
harden: { review: 'adversarial', lenses: ['meaning', 'terms'], rounds: 2 },
},On the Service
| Key | Written | Left out | Does |
|---|---|---|---|
deliver | Deliver | 'ai', briefed by services/<id>.md | Who does the work: 'ai', a person or a role word, a URL, your function, or named steps in order. |
The step kinds
| Written | Does |
|---|---|
'ai', 'person', a role word, a URL, your function, another Service | 'ai', a person, a URL, your function, or another of your Services. A person is 'person', or a role word such as 'interpreter': the role names their app, /interpreters. Once you write roles, a role word is one of them. |
{ by, … } | Work: AI, a person, your code, a URL or another Service proposes output. |
{ review, … } | Review: a step that hardens the step before it (adversarial) or extends it, "yes, and" (affirmative). |
{ ask, to, … } | Ask: a party acts, through a hosted single-use link or the customer portal, with no account. The job waits on: 'party', and the step's within pauses. Every ask is an object, ask_…, with its own link. |
{ submit, to, receipt, … } | Submit: send to a system you don't control, and keep its receipt. The send goes through the gate, so a guard can stop it, and the job waits on: 'outside' for the answer. A rejection is a transition: the job moves to the repair step rejected names, never an error answer. |
A step is work, a review, an ask or a submit. check reads each one aloud, and the record prints who did it.
Work proposes output: AI from its brief, your code, another Service, or a person. A role word makes a step a person's, and the job waits in the delivery portal for that role:
npx services.dev check steps prep · ai → call · interpreter → recap · ai → review · ai · adversarialA review, an ask and a submit each have their page: review steps, asks and sending to outside systems.
One run of Grow 4's Translation, with all four:
npx services.dev jobs wait job_7Hq2 record glossary.terms · answered | glossary · buyer · answer · clock paused 2h | translate · ai | harden · ai · adversarial · 2 lenses · pass | sign · person · sworn translator · * · mbr_7Hq2 | file · the registry · rejected · missing_certificate · repairing at signA step stays AI's until the job says a person should take it. escalate: { to, when } names the role and the condition: a function of the job, read as the job reaches the step, or 'at_risk' or 'late', read on the deadline's tick while the step is open. When it holds, the step is the role's: its Task posts to the role's Pool, and the job waits on a claim, with the fill ladder running if no one takes it. A role with a span keeps the steps after it too.
The Deal Service hands the booking to a setter on a warm reply, and the meeting, with every step after it, to a closer from $50,000 or a buying committee:
cat services/deal.tsconst setter = job => job.policy.setter === 'person' || (job.policy.setter === 'on_warm' && job.signals.warm)
const person = job => job.input.value >= job.policy.humans_from || job.signals.committee || job.signals.person
book: { by: 'ai', escalate: { to: 'setter', when: setter } },
meet: { by: 'ai', escalate: { to: 'closer', when: person }, when: job => job.input.motion !== 'B2A',check prints each escalation on the step it moves, and the ledger counts the role's pay only on jobs that escalate.
Steps run in the order you write them, unless a step says otherwise. Branches, loops, joins and parallel steps are on the step graph. The step that last committed is the job's stage, so the steps are the state machine and you never declare one.
No step writes to the job directly. AI, your code, a person and an outside system each propose, and the gate commits or refuses. A person's work meets the same checks as the AI's.
| 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 | |
ask.get | GET /asks/:id | asks get <ask> | never: a person acts | never: a person acts | 1 error page | hosted link, owner console | |
ask.complete | POST /asks/:id | never: a person acts | never: a person acts | 6 error pages | hosted link | ||
ask.reissue | POST /asks/:id/reissue, POST /asks/:id/link | asks reissue <ask> | reissue_ask | asks.reissue() | 3 error pages | customer portal, owner console |
Objects: ask_, rcp_.
Events: job.paused, step.refused, ask.opened, item.done, ask.reminded, ask.done, ask.expired, ask.voided, ask.relinked, consent.given, job.ended, submission.sent, receipt.accepted, receipt.rejected, receipt.timed_out, review.done.
Operations has every column of every row.
| Code | Category | HTTP | Why |
|---|---|---|---|
needs_a_person | blocked | 403 | A person approves and signs, in the app. A key or a tool never can. |
not_waiting | conflict | 409 | The job isn't waiting on a person. |
reserved | blocked | 403 | The rules reserve this act to a credential the person who attested doesn't hold. Nothing was charged. |
check gate | Why |
|---|---|
step_unreachable | A step no edge reaches never runs. |
| Written | What TypeScript says |
|---|---|
input: { document: 'file' }, output: { document: 'file' }, deliver: { translate: translation }, price: 99 | Property 'to' is missing in type '{ readonly document: FileRef; }' but required in type '{ readonly document: FileRef; readonly to: string; }'. |
The lines that use it in the example brands, as their files write them, and what check read aloud for each file.
deliver: {
consent: { ask: 'client', to: 'consent', expires: '7d' },
flag: 'ai',
attack: { review: 'adversarial' },
advise: { by: 'attorney', stamp: true, est: '45min', checklist: ['every flag is addressed', 'the memo answers the goals'] },
send: { by: 'ai' },
},services/contract-review.ts · 16 lines · $99 per job, within 48 hours, charged on acceptance.
steps consent · client · consent → flag · ai → attack · ai · adversarial → advise · attorney · client · stamped · 2-item checklist → send · aiThe legal example runs every one of its files, captured.
deliver: {
read: { by: 'ai', cite: 'source' },
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.
deliver: {
source: 'ai',
qualify: { by: 'ai', next: job => (job.output.fit ? 'book' : 'lost') },
book: { by: 'ai', escalate: { to: 'setter', when: setter } },
meet: { by: 'ai', escalate: { to: 'closer', when: person }, when: job => job.input.motion !== 'B2A',
guard: { held: (m, job) => m.attended(job.input.account) >= 10 || 'no one from the account stayed 10 minutes' } },
propose: { by: 'ai' }, negotiate: { by: 'ai' },
close: { by: 'ai', next: job => (job.output.closed_value ? 'won' : 'lost'),
guard: { closed_value: (v, job) => v >= job.input.value * job.policy.floor || 'below the floor you set' } },
nurture: { by: 'nurturer', routed: true, next: job => (job.output.revived ? job.record.interrupted : 'lost') },
},services/deal.ts · 42 lines · nothing on effort; 5% of closed_value at won, proven by the account's payment, held 60 days by bank, 120 by card.
steps source · ai → qualify · ai → book · ai · setter when warm → meet · ai · closer from humans_from, committee or person · skipped for B2A · guard held: the account attends 10 minutes → propose · ai · the closer's span → negotiate · ai · the closer's span → close · ai · the closer's span · guard closed_value at least value × floor → nurture · nurturer · routedThe sales example runs every one of its files.