One job per period

The running jobjob_7Hq2 · working · $49.00 held

With subject.period, a subject holds one live or ended job of a Service per period. A second order for that period answers 409 period_taken, and its next is the follow-on the job's end offers, such as a revision.

period answers one question: can this work be ordered twice for the same stretch of time?

Two lines: the input that names the period, and the Service an accepted year offers instead of a second one.

services/glossary.ts
  subject: { kind: 'account', key: 'account', period: 'year', keeps: { glossary: 'glossary' } },
  ends: { accepted: { follow: [revision] } },
captured/service-period-taken.txt · 200 · job_7Hq2 · accepted · $19.00 final · magic values: none
→ POST /jobs/job_7Hq2/accept
  next    [{"do":"again","arguments":{"id":"job_7Hq2","service":"glossary-revision"},"tool":"reorder_job","http":"POST http://localhost:8787/jobs/job_7Hq2/again"}]

On the subject

KeyWrittenLeft outDoes
period(keyof I & string) | ((job) => string) | { of; ends?; many? }noneOne live or ended job of this Service per subject per period: the input that names the period, or a function of the order. A second order for a period answers 409 period_taken, its next the follow-on the end offers, such as an amendment. A canceled job frees its period. ends gives the period's last day as MM-DD, for a fiscal year; many: true holds any number of jobs per period, as amendments do.

On an end

KeyWrittenLeft outDoes
followAnyService[]noneThe follow-on Services this end offers, through POST /jobs/:id/again { service }.

The subject already holds 2025, so the order is refused before anything runs or is charged. The refusal names the job that holds the period, and its next is the follow-on, ordered from that job:

captured/period-tabs.txt · magic values: none
$ npx services.dev glossary account=acme year=2025 terms=Premises --max 19 --key glossary-2025-again --json   # a second order for 2025
  "code": "period_taken",
  "reason": "account acme has Glossary for 2025 already: job_7Hq2, accepted. A second one for 2025 is a follow-on from it: glossary-revision.",
  "period": "2025",
    "do": "again",
      "service": "glossary-revision"
    "tool": "reorder_job",
captured/period-tabs.txt · 409 · magic values: none
$ curl -X POST http://localhost:8787/glossary -H 'content-type: application/json' -d '{"account":"acme","year":"2025","terms":["Premises"],"max_usd":19,"key":"glossary-2025-again"}'   # a second order for 2025
  "code": "period_taken",
  "reason": "account acme has Glossary for 2025 already: job_7Hq2, accepted. A second one for 2025 is a follow-on from it: glossary-revision.",
  "period": "2025",
    "do": "again",
      "service": "glossary-revision"
    "tool": "reorder_job",
captured/period-tabs.txt · magic values: none
→ tools/call order_glossary {"account":"acme","year":"2025","terms":["Premises"],"max_usd":19,"key":"glossary-2025-again"}   # a second order for 2025
← isError: true, structuredContent:
  "code": "period_taken",
  "reason": "account acme has Glossary for 2025 already: job_7Hq2, accepted. A second one for 2025 is a follow-on from it: glossary-revision.",
  "period": "2025",
    "do": "again",
      "service": "glossary-revision"
    "tool": "reorder_job",
captured/period-tabs.txt · magic values: none
await lingua.glossary.order({"account":"acme","year":"2025","terms":["Premises"],"max_usd":19,"key":"glossary-2025-again"})   // a second order for 2025
× throws ConflictError<'service.order'> · 409
  "code": "period_taken",
  "reason": "account acme has Glossary for 2025 already: job_7Hq2, accepted. A second one for 2025 is a follow-on from it: glossary-revision.",
  "period": "2025",
    "do": "again",
      "service": "glossary-revision"
    "tool": "reorder_job",

An agent sends next as it comes. The follow-on is its own job, at its own price, with from on its record:

captured/service-period-taken.txt · 202 · job_7Hq3 · delivered · $9.00 charged · magic values: none
→ POST /jobs/job_7Hq2/again {"service":"glossary-revision","max_usd":9}
  from    job_7Hq2

The follow-on joins the same subject. A canceled job frees its period; a job that ended any other way holds it.

period names an input, such as a year or a quarter, or a function of the order. A job with a period reads its subject's facts from the period before its own, so a late job for an earlier period never changes what a later one reads: Subjects.

A subscription that mints the work on a schedule sets the period through each. A mint for a period the subject already holds is recorded on the subscription as refused, never dropped: Schedules and triggers.

Each Service holds its own periods on a subject, and a period's name nests: 2027 holds 2027-Q1 to 2027-Q4, so a yearly job reads every value its quarterly ones kept with { period: '2027', all: true } (Subjects).

A period follows the subject's own year. apis.tax writes period: { of: 'tax_year', ends: job => job.input.year_end ?? '12-31' }, so Beta Labs' period 2027, for a year that ends 30 June, runs 2026-07-01 to 2027-06-30, and its quarters nest as fiscal quarters: 2028-Q1 is July to September 2027. Your code reads job.period.name, .start and .end, so the return's dates come from the period, never from a calendar year:

tax · lib/tax.ts
const statutory = (job: AnyView, j: string) => {
  const due = JURISDICTIONS[j].due
  if (typeof due === 'string') return `${Number(job.period.end.slice(0, 4)) + 1}-${due}`
  return fifteenth(job.period.end, j === 'US' && june(job) ? 3 : due[String(job.input.entity)] ?? due['*'])
}
export const estimateDue = (job: AnyView) => fifteenth(job.period.start, [3, 5, 8, 11][Number(job.period.name.slice(-1)) - 1])

A schedule with each mints one job per period in the same terms: Schedules and triggers.

A period sets no timer of its own. What touches it:

WhenThe runtimeEvent
a schedule mints for a period the subject already holdsrecords the mint as refused, and charges nothingsubscription.minted
a job holding a period is canceledfrees the period for the next orderjob.canceled
AppShows
Customer portalone row per period under each subject, its job and how it ended, and the follow-on a held period offers
Owner consoleeach subject's periods beside its jobs, and every refused order with its next

A refused second order charges nothing and holds nothing. The follow-on is its own job, at its own price. A mint a subscription makes for a held period is recorded as refused and charges nothing either. Periods run live on Launch, with the subject. Every kind of time, by plan: Keeping time.

OpAPICLIMCPSDKSiteDocsApp
service.orderPOST /jobs, POST /:service<service> k=v …order_<service><service>.order()/<service>: the order form10 error pagescustomer portal
job.againPOST /jobs/:id/againjobs again <job>reorder_jobjobs.again()7 error pagescustomer portal

Operations has every column of every row.

CodeCategoryHTTPWhy
not_terminalconflict409Only an ended job is ordered again.
period_takenconflict409A job of this Service already holds the subject's period: one live or ended job per period.
When the file won't loadThrown byWhy
period_unknownService()Service Glossary: subject.period is edition, which isn't an input. Name the input that says the period, such as year: 'text', or write period as a function of the order.

The lines that use it in the example brands, as their files write them, and what check read aloud for each file.

tax · services/estimate.ts · 17 lines
  subject: { kind: 'taxpayer', key: 'ein', period: 'quarter', keeps: { paid: 'paid' } },
npx services.dev check · captured/tax-check.txt
services/estimate.ts · 17 lines · $79 per job, within 48 hours, charged on delivery.
  steps   compute · code · cites prior-year safe harbor, cumulative by quarter → approve · owner · decide → review · ai · adversarial → pay · Treasury and states · one for each jurisdiction with an installment → record · code

A second client runs a June year end in apis.tax.

After a year of its plan, the tax example's taxpayer holds annual returns by year and estimates by quarter:

captured/tax-years.txt · magic values: owner.email = signs@example.test, metadata.sandbox.asks = {"organizer.books":"after 160d","organizer.assets":"waive","organizer #2":"after 2d","w9":"after 19d"}, metadata.sandbox.outside = {"reject":{"file.US-NY":["ein_mismatch"]}}, metadata.sandbox.events = { notice.received: { at: 2027-11-14T15:00:00Z, data: { notice, reply_by, tax_years } } }
→ GET /taxpayers/sbj_7Hq2
← sbj_7Hq2 · taxpayer 12-3456789 · of record mbr_8Ea1, Katherine Johnson, until 2030-07-11
  period  2025     annual-return        job_4Tx2 · accepted
  period  2026     information-returns  job_4Tx1 · accepted
  period  2026     annual-return        job_4Tx3 · accepted
  period  2027-Q1  estimate             job_4Tx4 · accepted
  period  2027-Q2  estimate             job_4Tx5 · accepted
  period  2027-Q3  estimate             job_4Tx6 · accepted
  period  2027-Q4  estimate             job_4Tx8 · accepted
  period  2027     information-returns  job_4TxA · delivered
  period  2027     annual-return        job_4Tx9 · waiting

Amended return runs the refusal and its follow-on end to end.