---
title: "One job per period"
url: https://services.dev/docs/service/periods
captured: [period-tabs.txt, service-period-taken.txt, tax-check.txt, tax-years.txt]
codes: [not_terminal, period_taken]
---
# One job per period

URL: https://services.dev/docs/service/periods

> 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?

## Smallest form

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

```ts title="services/glossary.ts"
  subject: { kind: 'account', key: 'account', period: 'year', keeps: { glossary: 'glossary' } },
  ends: { accepted: { follow: [revision] } },
```

```txt
→ POST /jobs/job_7Hq2/accept
```

```txt title="captured/service-period-taken.txt · 200 · job_7Hq2 · accepted · $19.00 final · magic values: none"
  next    [{"do":"again","arguments":{"id":"job_7Hq2","service":"glossary-revision"},"tool":"reorder_job","http":"POST http://localhost:8787/jobs/job_7Hq2/again"}]
```

## Every form

**On the subject**

| Key | Written | Left out | Does |
|---|---|---|---|
| `period` | `(keyof I & string) \| ((job) => string) \| { of; ends?; many? }` | none | One 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**

| Key | Written | Left out | Does |
|---|---|---|---|
| `follow` | `AnyService[]` | none | The follow-on Services this end offers, through `POST /jobs/:id/again { service }`. |

## A second order for the same period

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:

**CLI**

```bash title="CLI"
npx services.dev glossary account=acme year=2025 terms=Premises --max 19 --key glossary-2025-again --json   # a second order for 2025
```

```txt title="captured/period-tabs.txt · magic values: none"
  "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",
```

**API**

```bash title="API"
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
```

```txt title="captured/period-tabs.txt · 409 · magic values: none"
  "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",
```

**MCP**

```txt title="MCP"
→ tools/call order_glossary {"account":"acme","year":"2025","terms":["Premises"],"max_usd":19,"key":"glossary-2025-again"}   # a second order for 2025
```

```txt title="captured/period-tabs.txt · magic values: none"
← 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",
```

**SDK**

```ts title="SDK"
await lingua.glossary.order({"account":"acme","year":"2025","terms":["Premises"],"max_usd":19,"key":"glossary-2025-again"})   // a second order for 2025
```

```txt title="captured/period-tabs.txt · magic values: none"
× 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:

```txt
→ POST /jobs/job_7Hq2/again {"service":"glossary-revision","max_usd":9}
```

```txt title="captured/service-period-taken.txt · 202 · job_7Hq3 · delivered · $9.00 charged · magic values: none"
  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.

## Where the period comes from

`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](https://services.dev/docs/service/subject.md#kept-facts).

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](https://services.dev/docs/service/subscriptions.md).

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](https://services.dev/docs/service/subject.md#kept-facts)).

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:

```ts title="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](https://services.dev/docs/service/subscriptions.md#the-dates-a-schedule-takes).

## What happens on its own

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

| When | The runtime | Event |
|---|---|---|
| a schedule mints for a period the subject already holds | records the mint as refused, and charges nothing | `subscription.minted` |
| a job holding a period is canceled | frees the period for the next order | `job.canceled` |

## In the apps

| App | Shows |
|---|---|
| [Customer portal](https://services.dev/docs/app/buyers.md#dates-periods-and-allowances) | one row per period under each subject, its job and how it ended, and the follow-on a held period offers |
| [Owner console](https://services.dev/docs/app/owner.md) | each subject's periods beside its jobs, and every refused order with its `next` |

## Money and plan

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](https://services.dev/docs/money/plans.md#launch), with the subject. Every kind of time, by plan: [Keeping time](https://services.dev/docs/service/time.md#plans).

## Seven surfaces

| Op | API | CLI | MCP | SDK | Site | Docs | App |
|---|---|---|---|---|---|---|---|
| `service.order` | `POST /jobs`, `POST /:service` | `<service> k=v …` | `order_<service>` | `<service>.order()` | `/<service>`: the order form | 10 error pages | customer portal |
| `job.again` | `POST /jobs/:id/again` | `jobs again <job>` | `reorder_job` | `jobs.again()` |  | 7 error pages | customer portal |

[Operations](https://services.dev/docs/reference/operations.md) has every column of every row.

## Refusals

| Code | Category | HTTP | Why |
|---|---|---|---|
| [`not_terminal`](https://services.dev/docs/reference/errors.md#not_terminal) | conflict | 409 | Only an ended job is ordered again. |
| [`period_taken`](https://services.dev/docs/reference/errors.md#period_taken) | conflict | 409 | A job of this Service already holds the subject's period: one live or ended job per period. |

| When the file won't load | Thrown by | Why |
|---|---|---|
| [`period_unknown`](https://services.dev/docs/reference/definition-errors.md#period_unknown) | `Service()` | 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. |

## In the example brands

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

```ts title="tax · services/estimate.ts · 17 lines"
  subject: { kind: 'taxpayer', key: 'ein', period: 'quarter', keeps: { paid: 'paid' } },
```

```txt title="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](https://services.dev/docs/examples/tax/clients.md#a-second-client-on-its-own-year) 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:

```txt
→ GET /taxpayers/sbj_7Hq2
```

```txt title="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 } } }"
← 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](https://services.dev/docs/examples/tax/amended-return.md) runs the refusal and its follow-on end to end.

---

- Up: [Service()](https://services.dev/docs/service.md)
- Previous: [Subjects](https://services.dev/docs/service/subject.md)
- Next: [Schedules and triggers](https://services.dev/docs/service/subscriptions.md)
