---
title: "Schedules and triggers"
url: https://services.dev/docs/service/subscriptions
captured: [legal-walk.txt, subscription-event-replay.txt, subscription-on.txt, tax-cases.txt, tax-check.txt, tax-years.txt]
codes: [event_id_required, input_invalid, period_taken]
---
# Schedules and triggers

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

> On a subscription, on.schedule mints jobs on dates and periods, derived from the minted Service's own clocks, and on.event mints one when an event arrives, each routed to the Service it names. A gap, a late start, a client who changes and a client who leaves each mint by a stated rule.

`on` answers one question: what work happens on its own once someone subscribes? [Subscriptions](https://services.dev/docs/money/subscriptions.md) owns the `sbn_…` object and its charges; this page owns what it mints.

## Smallest form

Two lines on a plan beside the four-line Translation, from [Every month](https://services.dev/docs/grow/subscription.md):

```ts title="grow/subscription.ts"
  on: { schedule: { monthly: { at: 'every month', order: translation } }, event: { 'page.changed': translation } },
  price: { usd: 149, every: 'month', includes: { translation: 4 } },
```

## Every form

**On the Service**

| Key | Written | Left out | Does |
|---|---|---|---|
| `on` | `Triggers` | none | What starts jobs on a subscription: dates in `schedule`, events in `event`, each minting this Service or the one it names. Needs `price.every`. |

**`on`, written out**

| Key | Written | Left out | Does |
|---|---|---|---|
| `schedule` | `{ <name>: Schedule }` | none | Dates, by name: `{ check: 'every monday' }`, or `{ annual: { order: annualReturn, each: 'tax_year', at: '1mo after end' } }`. |
| `event` | `string[] \| Readonly` | none | Events, by name, from your other Services' jobs on the subject, or from the buyer's systems through `POST /events`. A list mints this Service; a map routes each event to the Service it names. |

**Each schedule, written out**

| Key | Written | Left out | Does |
|---|---|---|---|
| `order` | `AnyService` | this Service | The Service each date mints. |
| `at` | `string \| string[]` | required | When: a date below, or with `each`, a span before one of the minted Service's clocks or after its period ends. |
| `each` | `<period of the minted Service>` | none | One mint per period, its period set on the order: `'tax_year'`, `'quarter'`. |
| `when` | `(sub) => boolean` | always | Mints only when it holds, read at each date from the subscription as it stands. |
| `input` | `(on, sub) => fields` | the subscription's input, as the minted Service takes it | The order's fields. `on` is the civil date the occurrence was owed, never the time it fired. |
| `catch_up` | `'all' \| 'latest' \| 'open'` | `'all'` | What a gap mints, and with `'open'`, what a late start mints. |

**On the price**

| Key | Written | Left out | Does |
|---|---|---|---|
| `price.every` | `Every` | none | A subscription, `sbn_…`: [Subscriptions](https://services.dev/docs/money/subscriptions.md). `on` needs it. |

## One subscription, several small Services

One plan routes its dates and events to several small Services, each under 40 lines. Every job it mints shares the subscription's subject and policy, and names it as `from`.

```bash
cat services/plan.ts
```

```txt title="captured/subscription-on.txt · magic values: none"
import { Service } from 'services.dev'
import annualReturn from './annual-return.ts'
import estimate from './estimate.ts'
import noticeResponse from './notice-response.ts'
import question from './question.ts'

// The plan a business buys: its year routed to four small Services, each with its allowance. The
// schedule mints the return and the estimates on their dates; a question or a notice mints its Service when it comes.
export default Service({
  name: 'Tax Plan',
  input: { ein: 'text: such as 12-3456789', tax_year: 'text: such as 2026' },
  output: { letter: 'text' },
  subject: { kind: 'taxpayer', key: 'ein' },
  on: {
    schedule: { annual: { at: 'Feb 1', order: annualReturn }, estimate: { at: ['Apr 5', 'Jun 5', 'Sep 5', 'Dec 5'], order: estimate } },
    event: { 'question.asked': question, 'notice.received': noticeResponse },
  },
  price: { usd: 2_400, every: 'year', includes: { 'annual-return': 1, estimate: 4, question: 2, 'notice-response': 1 } },
})
```

The order is a subscription, with what it mints next:

```bash
npx services.dev plan ein=12-3456789 tax_year=2026 --max 2400
```

```txt title="captured/subscription-on.txt · magic values: none"
sbn_7Hq2 · plan · active · cycle 1 · 2026-10-25 → 2027-10-25 · $2,400.00 per year
  allowances  annual-return 0 of 1 · estimate 0 of 4 · question 0 of 2 · notice-response 0 of 1
  schedule    annual → annual-return · next 2027-02-01
  schedule    estimate → estimate · next 2026-12-05
  event       question.asked → question
  event       notice.received → notice-response
```

Each date mints its Service, and AI delivers it, with people only where its file puts them:

```bash
npx services.dev tick 2027-04-06T00:00:00Z
```

```txt title="captured/subscription-on.txt · magic values: none"
tc_7Hq2 · 2026-10-25 14:02:11 → 2027-04-06 00:00:00 · 28 fired
  2026-12-05 00:00:00  sbn_7Hq2  schedule estimate → job_7Hq2 estimate · $0.00 · allowance 1 of 4
  2026-12-05 00:00:00  job_7Hq2  draft → review
  2026-12-05 00:00:00  job_7Hq2  delivered · collected $0.00
  2026-12-12 00:00:00  job_7Hq2  accepted · earned $0.00
  2027-02-01 00:00:00  sbn_7Hq2  schedule annual → job_7Hq3 annual-return · period 2026 · $0.00 · allowance 1 of 1
  2027-02-01 00:00:00  job_7Hq3  draft → review
  2027-02-01 00:00:00  job_7Hq3  delivered · collected $0.00
  2027-02-08 00:00:00  job_7Hq3  accepted · earned $0.00
  2027-04-05 00:00:00  sbn_7Hq2  schedule estimate → job_7Hq4 estimate · $0.00 · allowance 2 of 4
  2027-04-05 00:00:00  job_7Hq4  draft → review
  2027-04-05 00:00:00  job_7Hq4  delivered · collected $0.00
```

An event sent to `POST /events` mints the Service it routes to, on the subject its data names:

```txt
→ POST /events {"type":"question.asked","id":"question-2","data":{"ein":"12-3456789","question":"Can I deduct the van?"}}
```

```txt title="captured/subscription-on.txt · 200 · subscription.minted · event question.asked → job_7Hv5 question · $0.00 · magic values: none"
← 200 · subscription.minted · event question.asked → job_7Hv5 question · $0.00
```

## The dates a schedule takes

| Written | Mints on |
|---|---|
| `'every day'` | each day |
| `'every monday'`, or any day of the week | that day, each week |
| `'every week'` | each Monday |
| `'every month'` | the 1st of each month |
| `'every quarter'` | 1 January, 1 April, 1 July and 1 October |
| `'every year'` | 1 January |
| `'Feb 1'` | that date each year; `'Feb 29'` only in a leap year |
| `'2027-02-01'` | that date, once |
| `['Apr 5', 'Jun 5', 'Sep 5', 'Dec 5']` | each of them |
| `{ each: 'quarter', at: '10d before pay' }` | each period of the minted Service, ten civil days before that job's own `pay` clock, rolled: 5 April for a 15 April date |
| `{ each: 'tax_year', at: '1mo after end' }` | each period, a month after it ends, counted from the day after its last day: 1 February for a calendar year, 1 August for a June year end |

Each date fires at the [calendar's](https://services.dev/docs/service/time.md#the-calendar) `rings_at`. With `each`, the date comes from the minted Service's own clock and [period](https://services.dev/docs/service/periods.md), so the Tax Plan lists no date the return already knows, and `plan` prints its source: `estimate → next 2027-04-05 (10d before pay 2027-04-15)`. `input` gets the civil date owed, never the time fired, so a mint 40 minutes late still orders the right year.

## After a gap

`catch_up` says what a schedule mints for dates it missed:

| `catch_up` | Mints |
|---|---|
| `'all'`, left out | every occurrence owed since the subscription started, each its own period, oldest first; `period_taken` keeps each once |
| `'latest'` | only the newest owed occurrence |
| `'open'` | as `'all'`, and at subscribe, each earlier period whose clocks are still open, oldest first, inside the allowance |

Beta Labs, whose year ends 30 June, buys the Tax Plan on 10 August 2027, its extension already filed. `plan` previews the late start:

```bash
npx services.dev plan --at 2027-08-10 ein=98-7654321 "entity=C corp" jurisdictions=US,US-CA year_end=06-30 extended_to=2028-04-15
```

```txt title="captured/tax-cases.txt · magic values: owner.email = signs@example.test, metadata.sandbox.outside = {"reject":{"file.US-NY":["ein_mismatch"]}}"
tax-plan for 98-7654321 · tax year ends 06-30 · from 2027-08-10
  annual    would mint now: period 2027 (2026-07-01 → 2027-06-30), its file date still open · catch_up 'open'
  annual    next 2028-08-01 (1mo after end 2028-06-30) · period 2028
  estimate  next 2027-10-05 (10d before pay 2027-10-15) · period 2028-Q1 (2027-07-01 → 2027-09-30)
  info      next 2028-01-05 (5d after end 2027-12-31) · the 1099s run on the calendar year · 2026's, due 2027-02-01, passed before the plan began
  skipped   period 2026 (2025-07-01 → 2026-06-30): due 2026-09-15, since a June year begun before 2026 keeps the third month; seven months extended, 2027-04-15, passed too
```

The order mints the open return at once, its file date extended, its date to pay unmoved:

```bash
npx services.dev tax-plan "business=Beta Labs <books@beta.example>" "owner=Lin Okafor <lin@beta.example>" ein=98-7654321 "entity=C corp" jurisdictions=US,US-CA books=https://books.example/beta year_end=06-30 extended_to=2028-04-15 --max 2400 --test-clock tc_7Hq5
```

```txt title="captured/tax-cases.txt · magic values: owner.email = signs@example.test, metadata.sandbox.outside = {"reject":{"file.US-NY":["ein_mismatch"]}}"
sbn_7Hr1 · tax-plan · active · cycle 1 · 2027-08-10 → 2028-08-10 · $2,400.00 per year
  minted    2027-08-10  catch_up open → job_4Ty1 annual-return · period 2027 · $0.00 · allowance 1 of 1
  clocks    job_4Ty1 file · due 2028-04-15, the client's extension (extended_to) · US, US-CA
            job_4Ty1 pay · due 2027-10-15 · hard · about $210 a month late
  schedule  estimate → estimate · next 2027-10-05 (10d before pay 2027-10-15)
```

## Events are occurrences

An event that mints needs your own id for it. The same id sent again answers the first result, `replayed: true`, and mints nothing more:

```txt
→ POST /events {"type":"notice.received","id":"CP2000-0042","data":{"ein":"12-3456789","notice":"CP2000"}}
```

```txt title="captured/subscription-event-replay.txt · 200 · subscription.minted · event notice.received → job_7Hq2 notice-response · $0.00 · trg_z2iVyv18fm · minted · replayed: true · magic values: none"
← 200 · subscription.minted · event notice.received → job_7Hq2 notice-response · $0.00 · trg_z2iVyv18fm · minted · replayed: true
```

## A client who changes, or leaves

| When the client | The mints |
|---|---|
| uses an allowance | $0 and counted; past it, offered at the job's own price: [Subscriptions](https://services.dev/docs/money/subscriptions.md#past-the-allowance) |
| [updates](https://services.dev/docs/money/subscriptions.md#update) the subscription | the next mint takes the new input; jobs already minted keep theirs |
| cancels | stop when the paid cycle ends; jobs in flight finish, and the subject's open dates are [handed back](https://services.dev/docs/service/subject.md#every-open-clock-has-an-owner) |

Acme registers in Texas in March 2027, so its 2027 return files five jurisdictions and its 2026 return stays at four:

```bash
npx services.dev subscriptions update sbn_7Hq2 jurisdictions=US,US-CA,US-NY,US-NJ,US-TX --test-clock tc_7Hq4
```

```txt title="captured/tax-cases.txt · magic values: owner.email = signs@example.test, metadata.sandbox.outside = {"reject":{"file.US-NY":["ein_mismatch"]}}"
sbn_7Hq2 · tax-plan · active · updated 2027-03-08 · jurisdictions + US-TX
  next mint   annual 2028-02-01 · period 2027 · US, US-CA, US-NY, US-NJ, US-TX · Texas due 05-15, its own extension on 05-164
  keeps       job_4Tx3 · period 2026 · US, US-CA, US-NY, US-NJ, as minted 2027-02-01
```

## What happens on its own

| When | The runtime | Event |
|---|---|---|
| a cycle ends | renews, resets every allowance and posts the cycle's charge, before that instant's schedules mint; [Subscriptions](https://services.dev/docs/money/subscriptions.md#renewal) has the charge, a declined one and a cancel | `subscription.renewed` |
| a schedule's date comes | mints its Service on the subscription's subject, inside an allowance at $0 | `subscription.minted`, `allowance.used` |
| a mint passes an allowance | offers the job at its own price, 402 OFFER, with the order in `next` | `allowance.exceeded` |
| a mint names a period the subject holds | records the mint as refused, and charges nothing | `subscription.minted` |
| a subscription starts after a schedule's date | with `catch_up: 'open'`, mints each earlier period whose clocks are still open, oldest first; otherwise waits for the next date | `subscription.minted` |
| a date comes while a renewal is unpaid | mints the job and holds it, never releasing it until the renewal collects | `subscription.minted` |
| the subscription is updated | the next mint takes the new input; jobs already minted keep theirs | `subscription.updated` |
| a cancel's cycle ends | minting stops; jobs in flight finish, and the subject's open dates are handed back to the client | `subscription.canceled`, `clock.outlived` |

[The tick](https://services.dev/docs/service/time.md#the-tick) runs renewals first and schedules second, so a job minted on a renewal day counts in the new cycle, and money last, so its charge collects in the same instant.

## In the apps

| App | Shows | Acts |
|---|---|---|
| [Customer portal](https://services.dev/docs/app/buyers.md#dates-periods-and-allowances) | each subscription: its cycle, each allowance used of included, what one more costs, and every date it mints on next | cancel at the end of the cycle |
| [Owner console](https://services.dev/docs/app/owner.md) | every subscription, its cycle's charge on the ledger, and each job it minted with its `from` | cancel one, at the end of its cycle |

The `sbn_` object, on four surfaces: [Subscriptions](https://services.dev/docs/money/subscriptions.md#cancel).

## Money and plan

A refused mint charges nothing. Schedules, `catch_up` included, and events inside an allowance run live on [Launch](https://services.dev/docs/money/plans.md#launch); an event routed past the subscription, such as apis.tax's `fact.restated`, on [Team](https://services.dev/docs/money/plans.md#team). 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 |
| `event.inbound` | `POST /events` | `events send <type>` | `send_event` | `events.send()` |  | 4 error pages | customer portal |
| `subscription.list` | `GET /subscriptions`, `GET /owner/subscriptions` | `subscriptions list` | `list_subscriptions` | `subscriptions.list()` |  | 1 error page | customer portal, owner console |
| `subscription.get` | `GET /subscriptions/:id`, `GET /owner/subscriptions/:id` | `subscriptions get <subscription>` | `get_subscription` | `subscriptions.get()` |  | 1 error page | customer portal, owner console |
| `subscription.cancel` | `POST /subscriptions/:id/cancel`, `POST /owner/subscriptions/:id/cancel` | `subscriptions cancel <subscription>` | `cancel_subscription` | `subscriptions.cancel()` |  | 1 error page | customer portal, owner console |

Objects: `sbn_`.
Events: `subscription.created`, `subscription.renewed`, `subscription.minted`, `subscription.updated`, `allowance.used`, `allowance.exceeded`, `subscription.canceled`.

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

## Refusals

| Code | Category | HTTP | Why |
|---|---|---|---|
| [`event_id_required`](https://services.dev/docs/reference/errors.md#event_id_required) | invalid_request | 422 | An event that mints a job on a subscription is one occurrence: it needs your id for it. |
| [`input_invalid`](https://services.dev/docs/reference/errors.md#input_invalid) | invalid_request | 422 | The order doesn't match the Service's input. |
| [`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 |
|---|---|---|
| [`subscription_unpriced`](https://services.dev/docs/reference/definition-errors.md#subscription_unpriced) | `Service()` | Service Glossary: on mints jobs on a subscription, and the price has no every. Add every: 'month', 'quarter' or 'year' to the price: one charge per cycle. |
| [`every_unknown`](https://services.dev/docs/reference/definition-errors.md#every_unknown) | `Service()` | Service Glossary: price.every is 'fortnight'. A subscription is charged every day, week, month, quarter, year. |
| [`trigger_unparsed`](https://services.dev/docs/reference/definition-errors.md#trigger_unparsed) | `Service()` | Service Glossary: on.schedule.refresh has 'the first Monday'. A date is 'Feb 1' each year, '2027-02-01' once, 'every monday', or every day, week, month, quarter or year. |
| [`allowance_uncounted`](https://services.dev/docs/reference/definition-errors.md#allowance_uncounted) | `Service()` | Service Glossary: price.includes['translation'] is 2.5. An allowance is a whole number of jobs per cycle, such as 12. |

## 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="legal · services/docket-watch.ts · 10 lines"
  on: { schedule: { check: 'every monday' }, event: { 'office_action.issued': trademarkResponse } },
  price: { usd: 12, every: 'month' },
```

```txt title="npx services.dev check · captured/legal-walk.txt"
services/docket-watch.ts · 10 lines · $12 per month, charged at the start of each month.
  steps   check · ai → ack · patent watcher → review · ai · adversarial
```

[The legal example](https://services.dev/docs/examples/legal.md) runs every one of its files, captured.

```ts title="tax · services/tax-plan.ts · 18 lines"
  on: {
    schedule: {
      annual: { order: annualReturn, each: 'tax_year', at: '1mo after end', catch_up: 'open' },
      estimate: { order: estimate, each: 'quarter', at: '10d before pay', when: sub => sub.input.entity === 'C corp' },
    },
    event: { 'question.asked': question, 'notice.received': noticeResponse, 'fact.restated': amendedReturn },
  },
  price: { usd: 2_400, every: 'year', includes: { 'annual-return': 1, 'information-returns': 25, estimate: 4, question: 12, 'notice-response': 2 } },
```

```txt title="npx services.dev check · captured/tax-check.txt"
services/tax-plan.ts · 18 lines · $2,400 per year, charged at the start of each year, 1 annual-return, 25 information-returns payees, 4 estimate, 12 question and 2 notice-response included each year.
  steps   draft · ai → review · ai · adversarial
```

[The agent's order](https://services.dev/docs/examples/tax/agent.md) buys a plan, and [a client who leaves](https://services.dev/docs/examples/tax/clients.md#a-client-who-leaves) cancels one.

A year of the Tax Plan, as it minted:

```bash
npx services.dev tick 1y --test-clock tc_7Hq4 --market normal
```

```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 } } }"
  2027-02-01 14:00:00  sbn_7Hq2  schedule annual → job_4Tx3 annual-return · period 2026 · $0.00 · allowance 1 of 1
  2027-04-05 13:00:00  sbn_7Hq2  schedule estimate → job_4Tx4 estimate · period 2027-Q1 · $0.00 · allowance 1 of 4
  2027-06-05 13:00:00  sbn_7Hq2  schedule estimate → job_4Tx5 estimate · period 2027-Q2 · $0.00 · allowance 2 of 4
  2027-09-05 13:00:00  sbn_7Hq2  schedule estimate → job_4Tx6 estimate · period 2027-Q3 · $0.00 · allowance 3 of 4
  2027-11-14 15:00:00  sbn_7Hq2  event notice.received → job_4Tx7 notice-response · $0.00 · allowance 1 of 2
  2027-12-05 14:00:00  sbn_7Hq2  schedule estimate → job_4Tx8 estimate · period 2027-Q4 · $0.00 · allowance 4 of 4
  2028-01-05 14:00:00  sbn_7Hq2  schedule information → job_4TxA information-returns · period 2027 · $0.00
  2028-01-20 09:00:00  sbn_7Hq2  renews · cycle 2 · $2,400.00 charged · allowances reset · ends 2029-01-20
```

[The plan](https://services.dev/docs/examples/tax/tax-plan.md) and [A year, and the next](https://services.dev/docs/examples/tax/years.md) run it whole.

---

- Up: [Service()](https://services.dev/docs/service.md)
- Previous: [One job per period](https://services.dev/docs/service/periods.md)
- Next: [Talk](https://services.dev/docs/service/talk.md)
