Sending to outside systems

The running jobjob_7Hq2 · working · $49.00 held

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?

services/translation.ts
    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 },
Run
npx services.dev check
magic values: none (metadata.sandbox.outside defaults to ack)
services/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 · routed

A 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

KeyWrittenLeft outDoes
submitUrl | SubmitFnrequiredThe outside system: its URL (on localhost with remote: false, the simulated one), or your function.
tostringrequiredIts name on the record and the timeline.
actstringthe step nameThe act this step performs, as the rules name it.
receipt<text output>requiredThe 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 }noneFan out: one submission and one receipt per element of a list. lead goes first; the rest wait for its acceptance.
waitWithin'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>)noneTyped 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.
perfectWithinnoneA repair resubmitted inside this window, counted from the first submission, keeps that submission's time.
fallback<step>noneAfter 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

KeyWrittenLeft outDoes
statusReceiptStatusrequired'transmitted', 'accepted', 'accepted_with_alerts' or 'rejected'.
codesstring[]noneIts codes: why it rejected, or the alerts it accepted with.
refstringnoneIts own number for the submission, such as an application number. The receipt field gets it, or the receipt's id.
fileFileRefnoneIts receipt as a file, kept in the job's documents plane by its sha256.
fee_usdnumbernoneWhat 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:

Run
curl -s http://localhost:8787/jobs/job_7Hq2 | jq '.receipts[0]'
magic values: none (metadata.sandbox.outside defaults to ack)
{
  "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:

Call
→ POST /translation {"document":"lease.pdf","to":"fr-CA","max_usd":49,"key":"release","metadata":{"sandbox":{"outside":"reject_once"}}}
magic values: 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.16

price.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:

Call
→ POST /translation {"document":"lease.pdf","to":"fr-CA","max_usd":49,"key":"rejected","metadata":{"sandbox":{"outside":"reject_once"}}}
magic values: 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.000Z

A 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:

Run
curl http://localhost:8787/jobs/job_7Hq2   # 2026-10-27: two days of silence
magic values: metadata.sandbox.outside = never
← 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.84

The 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:

services/registered-translation.ts
    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:

Call
→ POST /registered-translation {"document":"deed.pdf","to":"fr","max_usd":59,"key":"registered"}
magic values: none
← 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.32

What your code, or a URL, can hand back, and what the job does with it:

It answersThe job
accepted, or accepted_with_alerts with its codeskeeps the receipt, fills the receipt field with ref or the receipt's id, and goes on
transmittedkeeps the receipt, and the step counts the submission as filed: see when the answer comes later
rejected with codesmoves to the repair the codes name; after two rejections, to fallback
nothing, a throw, a non-2xx or anything that isn't a receiptsilence, 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_idkeeps 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 in rejected.
  • Silence is not a rejection. Return nothing when it fails to answer: the job waits, and past wait goes to fallback. Return rejected only 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 a Brand.credentials entry.

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:

tax · lib/tax.ts
const asked = (job: Filing) => (job.in_doubt ? taken.get(job.submission_id) : undefined)
  const held = asked(job)
  if (held) return held

each 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.

OpAPICLIMCPSDKSiteDocsApp
job.files.putPUT /jobs/:id/files/:sha256, POST /jobs/:id/filesjobs files put <job> <path>never: a file is bytes: send it over HTTP or the SDK, and name it by URL in an orderjobs.files.put()2 error pagescustomer portal, owner console
job.files.getGET /jobs/:id/files/:sha256jobs files get <job> <sha256>never: a file is bytes: send it over HTTP or the SDK, and name it by URL in an orderjobs.files.get()1 error pagecustomer portal, owner console

Objects: rcp_. Events: submission.sent, receipt.accepted, receipt.rejected, receipt.timed_out.

Operations has every column of every row.

check gateWhy
release_unprovenprice.release names no step that runs.
WrittenWhat 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: 49Type '"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.

legal · services/formation.ts · 30 lines
    form_entity: { submit: 'https://filings.example.test/secretary-of-state', to: 'the state', receipt: 'file_number', rejected: { name_unavailable: 'rename' } },
npx services.dev check · captured/legal-walk.txt
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 checklist

The legal example runs every one of its files, captured.

tax · services/amended-return.ts · 22 lines
    file: { submit: efile, to: 'IRS and states', receipt: 'receipts', each: { of: 'jurisdictions', lead: j => j === 'US' }, rejected: { '*': 'prepare' }, when: job => job.input.entity !== 'partnership' },
npx services.dev check · captured/tax-check.txt
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.adjusted

The tax example runs every one of its files, captured.