Sending to outside systems
A submit step sends the work to a system you don't control, waits for its answer and keeps a typed receipt. Rejections route by code, a timely repair stays timely, and silence falls back.
A submit step answers one question: did the outside system take it?
file: {
submit: 'http://localhost:8787/registry',
to: 'the registry',
receipt: 'filing',
rejected: { reject_once: 'correct', '*': 'translate' },
perfect: '2d',
fallback: 'hand_off',
},
correct: { ask: 'buyer', to: 'upload', routed: true },
hand_off: { ask: 'buyer', to: 'answer', routed: true },npx services.dev checkservices/translation.ts · 17 lines · $49 per job, within 48 hours, charged on file's receipt.
who AI delivers every job.
steps translate · ai → review · ai · adversarial → file · the registry → correct · buyer · upload · routed → hand_off · buyer · answer · routedA rejection coded reject_once asks the buyer for a corrected file; any other code sends the work back to translate. Two rejections, or silence, hand the filing to the buyer.
On a submit step
| Key | Written | Left out | Does |
|---|---|---|---|
submit | Url | SubmitFn | required | The outside system: its URL (on localhost with remote: false, the simulated one), or your function. |
to | string | required | Its name on the record and the timeline. |
act | string | the step name | The act this step performs, as the rules name it. |
receipt | <text output> | required | The output field the receipt fills: its number, or the receipt's id; a list with each. price.release can name this step, or <step>.accepted. |
each | { of: <list input> | <list output>; lead?: (element) => boolean } | none | Fan out: one submission and one receipt per element of a list. lead goes first; the rest wait for its acceptance. |
wait | Within | '2d' | How long an answer may take. Silence past it goes to fallback. |
rejected | <step> | { [code: string]: <step> | { to: <step>; items?: (job, element) => AskItem[] } } | ((codes, job) => <step>) | none | Typed rejections: one repair step; a map from the outside system's codes to steps, '*' for the rest, where the order you write the codes in is their priority; or a function of every code the rejection carries. |
perfect | Within | none | A repair resubmitted inside this window, counted from the first submission, keeps that submission's time. |
fallback | <step> | none | After two rejections, or silence past wait: this step instead. Without one, the job stops at $0. |
What the outside system answers, as your submit function returns it
| Key | Written | Left out | Does |
|---|---|---|---|
status | ReceiptStatus | required | 'transmitted', 'accepted', 'accepted_with_alerts' or 'rejected'. |
codes | string[] | none | Its codes: why it rejected, or the alerts it accepted with. |
ref | string | none | Its own number for the submission, such as an application number. The receipt field gets it, or the receipt's id. |
file | FileRef | none | Its receipt as a file, kept in the job's documents plane by its sha256. |
fee_usd | number | none | What it charged for the submission, in dollars, kept on the receipt. |
The send goes through the gate, so a guard can stop it. The job then waits on: 'outside'. The answer fills the receipt field, and the receipt is an object with its own file, stored by its sha256:
curl -s http://localhost:8787/jobs/job_7Hq2 | jq '.receipts[0]'{
"id": "rcp_7Hq2",
"object": "receipt",
"job": "job_7Hq2",
"step": "file",
"to": "the registry",
"at": "2026-10-25T14:02:11.000Z",
"status": "accepted",
"livemode": false,
"file": {
"url": "http://localhost:8787/jobs/job_7Hq2/files/82d85d1a577fa13c7b2b5949e8b1aea6e5f307e492ad3a279c7618ab8cbb6956",
"name": "rcp_7Hq2.txt"
},
"simulated": true
}A receipt is transmitted, accepted, accepted_with_alerts or rejected, with the outside system's codes. Every file a job holds lives at /jobs/:id/files/<sha256>, so a file sent twice is stored once.
A Service with a submit step charges on its receipt. You don't write it: check says charged on file's receipt. Through a rejection and a repair, the buyer's money stays held:
→ POST /translation {"document":"lease.pdf","to":"fr-CA","max_usd":49,"key":"release","metadata":{"sandbox":{"outside":"reject_once"}}}← job_7Hq2 · waiting · on party · $49.00 held
money charge.held → submission.sent → receipt.rejected
ledger AI $0.84 · you −$0.84
← job_7Hq2 · delivered · lease.fr-CA.pdf · $49.00 charged
money charge.held → submission.sent → receipt.rejected → submission.sent → receipt.accepted → charge.collected
ledger +$49.00 · AI $0.84 · you $48.16price.release can name '<step>.accepted' instead, so a transmitted answer isn't enough: the charge waits for accepted or accepted_with_alerts.
rejected is one repair step, a map from the system's codes to steps, with '*' for the rest, or a function of the codes. A rejection is a move, not an error. Here the buyer repairs it a day later, through the link:
→ POST /translation {"document":"lease.pdf","to":"fr-CA","max_usd":49,"key":"rejected","metadata":{"sandbox":{"outside":"reject_once"}}}← job_7Hq2 · waiting · on party · $49.00 held
stage rejected by the outside system · repairing
receipt rcp_7Hq2 · rejected · reject_once · at 2026-10-25T14:02:11.000Z
← job_7Hq2 · delivered · lease.fr-CA.pdf · $49.00 charged
receipts rcp_7Hq2 · rejected · reject_once · at 2026-10-25T14:02:11.000Z
rcp_7Hq3 · accepted · at 2026-10-25T14:02:11.000Z · sent 2026-10-26T14:02:11.000ZA repair resubmitted inside perfect keeps the first submission's time. The corrected file above went a day later, and its accepted receipt still says the 25th.
After two rejections, or silence past wait, the job goes to fallback. Here the registry never answers, the buyer files it themselves, and with no receipt the buyer pays nothing:
curl http://localhost:8787/jobs/job_7Hq2 # 2026-10-27: two days of silence← job_7Hq2 · waiting · on party · $49.00 held
record translate · ai | review · ai · pass | file · the registry · silent past wait · fallback hand_off
→ POST /asks/ask_7Hq2?t=8FaWT5EoCj4h-uMyjlXp3_ {"answer":"Filed it myself at the counter."}
← job_7Hq2 · delivered · lease.fr-CA.pdf · $49.00 held
record translate · ai | review · ai · pass | file · the registry · silent past wait · fallback hand_off | hand_off · buyer · answer
→ POST /jobs/job_7Hq2/accept
← job_7Hq2 · accepted · lease.fr-CA.pdf · $0
ledger AI $0.84 · you −$0.84The fallback is an ask, so accept closes the job at $0; the AI's $0.84 is the brand's.
Make submit a function and your code is the outside system's client: it sends one submission and returns the answer, built with job.accepted(ref, { fee_usd, file, codes }), job.rejected(...codes) or job.transmitted(ref). job.attempt counts this step's submissions from 0, and job.element is the element with each:
file: {
submit: async (_, job) => (job.attempt === 0 ? job.rejected('missing_certificate') : job.accepted('REG-2026-0042', { fee_usd: 12 })),
to: 'the registry',
receipt: 'filing',
rejected: { missing_certificate: 'translate' },
},The registry's code becomes the AI's feedback, the redo is reviewed again, and the second submission's ref fills filing:
→ POST /registered-translation {"document":"deed.pdf","to":"fr","max_usd":59,"key":"registered"}← job_7Hq2 · delivered · deed.fr.pdf · $59.00 charged
record translate · ai | review · ai · pass | file · the registry · rejected · missing_certificate · repairing at translate | translate · ai · redo | review · ai · pass | file · the registry · accepted · REG-2026-0042
filing REG-2026-0042
ledger +$59.00 · AI $1.68 · you $57.32What your code, or a URL, can hand back, and what the job does with it:
| It answers | The job |
|---|---|
accepted, or accepted_with_alerts with its codes | keeps the receipt, fills the receipt field with ref or the receipt's id, and goes on |
transmitted | keeps the receipt, and the step counts the submission as filed: see when the answer comes later |
rejected with codes | moves to the repair the codes name; after two rejections, to fallback |
| nothing, a throw, a non-2xx or anything that isn't a receipt | silence, with the error on the record: past wait, the job goes to fallback |
on a retry with job.in_doubt, the receipt the system already holds for job.submission_id | keeps it as if this send had made it: nothing is filed twice |
A URL gets POST <url> with a services-job header and the JSON body { job, step, to, attempt, element, submission_id, in_doubt, output }, and answers the same receipt as JSON.
Answer transmitted with their number as ref, and the step counts the submission as filed. The verdict arrives later by receipt.answer, from the system or your poller: accepted releases a charge on <step>.accepted, and a rejection routes by its code.
Beside job.attempt and job.element, your function reads the order's files with text(), and the job's output so far, its policy and its feedback.
- One id per submission. Send the outside system
job.submission_id,${job.id}:${job.element}:${job.attempt}, so a retry is never filed twice: a send in doubt. - Map its codes. Pass the system's own rejection codes to
job.rejected(), and route them inrejected. - Silence is not a rejection. Return nothing when it fails to answer: the job waits, and past
waitgoes tofallback. Returnrejectedonly when the system said no. - Credentials. Read the system's credentials from
process.env, set from the brand's secrets once deployed. A credential the brand holds is aBrand.credentialsentry.
Federal first, then the states wires one end to end, with a transmitter the capture serves.
job.attempt counts repairs, never redeliveries, so a second wake sends under the same submission_id. A send that crashed before its receipt was recorded retries with job.in_doubt: true: ask the system for that id, and answer with the receipt it holds. apis.tax's e-file functions start that way:
const asked = (job: Filing) => (job.in_doubt ? taken.get(job.submission_id) : undefined)
const held = asked(job)
if (held) return heldeach sends one submission per element of a list, the lead first. A rejection in one never holds back the others, and its repair can re-ask only what its code names: one submission per element.
A submit step sends as the brand, under the brand's own credentials, and the receipt names the brand as the sender. What the outside system needs from the buyer, such as a signature or an authorization, an ask collects first.
| Op | API | CLI | MCP | SDK | Site | Docs | App |
|---|---|---|---|---|---|---|---|
job.files.put | PUT /jobs/:id/files/:sha256, POST /jobs/:id/files | jobs files put <job> <path> | never: a file is bytes: send it over HTTP or the SDK, and name it by URL in an order | jobs.files.put() | 2 error pages | customer portal, owner console | |
job.files.get | GET /jobs/:id/files/:sha256 | jobs files get <job> <sha256> | never: a file is bytes: send it over HTTP or the SDK, and name it by URL in an order | jobs.files.get() | 1 error page | customer portal, owner console |
Objects: rcp_.
Events: submission.sent, receipt.accepted, receipt.rejected, receipt.timed_out.
Operations has every column of every row.
check gate | Why |
|---|---|
release_unproven | price.release names no step that runs. |
| Written | What TypeScript says |
|---|---|
input: { a: 'text' }, output: { filing: 'text' }, deliver: { draft: 'ai', file: { submit: 'https://registry.example.test/f', to: 'the registry', receipt: 'filing', rejected: 'drfat' } }, price: 49 | Type '"drfat"' is not assignable to type '…'. Did you mean '"draft"'? |
The lines that use it in the example brands, as their files write them, and what check read aloud for each file.
form_entity: { submit: 'https://filings.example.test/secretary-of-state', to: 'the state', receipt: 'file_number', rejected: { name_unavailable: 'rename' } },services/formation.ts · 30 lines · $299 per job, within 48 hours, charged on form_entity's receipt.
steps consent · founders · consent → advise_entity · attorney · client · 1-item checklist → review · ai · adversarial → form_entity · the state → rename · founders · answer · routed → documents · ai → adopt · founders · sign → advise_83b · attorney · client · 1-item checklistThe legal example runs every one of its files, captured.
file: { submit: efile, to: 'IRS and states', receipt: 'receipts', each: { of: 'jurisdictions', lead: j => j === 'US' }, rejected: { '*': 'prepare' }, when: job => job.input.entity !== 'partnership' },services/amended-return.ts · 22 lines · $900 per job, within 30 days, charged when file or aar is accepted.
steps compute · code · cites depreciation and carryforward schedules → draft · ai · cites the workpapers and the records the organizer took → check · ai · adversarial · 2 lenses → prepare · preparer · * · 1-item checklist → sign · signer · * · stamped in the paid-preparer block → authorize · owner · sign → file · IRS and states, unless a partnership → aar · IRS · Form 8082, for a partnership · emits k1.adjustedThe tax example runs every one of its files, captured.