---
title: "Keeping time"
url: https://services.dev/docs/service/time
captured: [clock-calendar.txt, time-timers.txt]
---
# Keeping time

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

> Every kind of time a Service keeps, indexed with its key, its place in the tick, its money and its page. Each compiles to one timer on the record, fired once on the brand's calendar in a fixed order.

Time answers one question: what happens on its own, and when? It decides when a step runs with no one asking, who hears, when money moves, and when the work can no longer be done. Each kind of time is one key with one page that owns it, and each runs as a timer on the record.

## In one Service

apis.tax's annual return keeps seven kinds of time in one file, and its Tax Plan two more:

```ts title="tax · services/annual-return.ts"
  subject: {
    kind: 'taxpayer', key: 'ein', period: { of: 'tax_year', ends: job => job.input.year_end ?? '12-31' }, // A period
  },
  deliver: {
    organizer: { ask: 'owner', to: 'upload', items: requestList, expires: '180d' }, // A party's link
    file: {
      // …
      rejected: { ein_mismatch: { to: 'organizer', items: reask.ein_mismatch }, schedule_missing: { to: 'organizer', items: reask.schedule_missing }, math: 'draft', '*': 'prepare' }, perfect: '10d', // A repair window
    },
  },
  clocks: {
    file: { due: fileDue, each: { of: 'jurisdictions' }, owner: 'of_record', rungs: ['notice 90d', 'notice 30d', 'run tentative 3d', 'ack daily 5d'], moves: { extend: extendedDue }, exposure: lateFiling, guarantee: { covers: ['penalties', 'interest on them'], up_to: 5_000, after: 'notice-response', excludes: 'held' } }, // A date for each item
    pay: { due: payDue, owner: 'buyer', hard: true, rungs: ['notice 14d', 'escalate 3d'], satisfied_by: ['pay', 'file'], exposure: lateCost }, // A date nothing moves
    expiring: { due: earliestExpiry, owner: 'brand', lives_on: 'subject', rungs: ['notice 180d'] }, // A date that outlives the job
  },
  price: { usd: 1_800, within: '5d before file', late: '20%', release: 'file.accepted', lines: { state_fees: { at: 'cost', from: 'file' } } }, // The job's deadline
```

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

[A year, and the next](https://services.dev/docs/examples/tax/years.md) runs both on a test clock.

## Model it

| You need | Write | apis.tax writes |
|---|---|---|
| a deadline on the work | `price.within` | `within: '5d before file'`, so the deadline follows the filing date |
| a date the world sets, and the step that moves it | `clocks.<c>.moves` | `file: { …, moves: { extend: extendedDue } }` |
| a date an extension never moves | `clocks.<c>.hard` | `pay: { …, hard: true }` |
| the work every quarter | `on.schedule` with `each` | `estimate: { each: 'quarter', at: '10d before pay' }` |
| the client next year | `subject.keeps` and `on.schedule` | `keeps: { carryforwards, tax }` and `annual: { each: 'tax_year', at: '1mo after end' }` |
| a notice's reply date | `on.event` and the minted Service's clock | `'notice.received': noticeResponse`, whose `reply` clock is due on the notice's `reply_by` |

`GET /subjects/:id/calendar` lists every date, kept and ahead. Acme's, on 2 February 2028, out to a carryforward that expires in 2046:

```bash
npx services.dev subjects calendar sbj_7Hq2 --from 2027-02-01 --test-clock tc_7Hq4
```

```txt title="captured/time-timers.txt · magic values: none"
sbj_7Hq2 · taxpayer 12-3456789 · Acme Robotics · America/New_York · 21 dates · webcal at /subjects/sbj_7Hq2/calendar.ics
  date        what                                       kind      answers for    status
  2027-02-01  the 2026 return is minted                  schedule  the brand      minted job_4Tx3
  2027-04-05  the Q1 estimate is minted                  schedule  the brand      minted job_4Tx4
  2027-04-15  pay the 2026 tax                           hard      Ada Lovelace   kept 2027-04-12, with the extension
  2027-04-15  pay the Q1 estimate                        clock     Ada Lovelace   kept 2027-04-05
  2027-06-15  pay the Q2 estimate                        clock     Ada Lovelace   kept 2027-06-05
  2027-07-30  the organizer's link expires               ask       Ada Lovelace   answered 2027-07-13
  2027-09-15  pay the Q3 estimate                        clock     Ada Lovelace   kept 2027-09-05
  2027-10-15  file the 2026 return, US and US-NY         clock     mbr_8Ea1       extended from 04-15 · kept 2027-07-11, US-NY 2027-07-13
  2027-11-15  file the 2026 return, US-CA and US-NJ      clock     mbr_8Ea1       extended from 04-15 and 05-17 · kept 2027-07-11
  2027-12-14  reply to the CP161                         clock     mbr_8Ea1       kept 2027-11-14
  2027-12-15  pay the Q4 estimate                        clock     Ada Lovelace   kept 2027-12-05
  2028-01-20  the Tax Plan renews                        cycle     the buyer      renewed, cycle 2
  2028-02-01  the 2027 return is minted                  schedule  the brand      minted job_4Tx9
  2028-04-18  file the 2027 return, US, US-CA and US-NY  clock     mbr_8Ea1       open · rolled from 04-15
  2028-04-18  pay the 2027 tax                           hard      Ada Lovelace   open · about $324 a month late
  2028-05-15  file the 2027 New Jersey CBT-100           clock     mbr_8Ea1       open
  2028-05-15  file the 2028 Texas report, on 2027's books  clock   mbr_8Ea1       open
  2028-07-30  the organizer's link expires               ask       Ada Lovelace   open
  2029-04-17  refund claims and assessment on 2025 close  hard     mbr_8Ea1       open · bars amended-return.file when it claims a refund · rolled from 04-15
  2030-07-11  refund claims and assessment on 2026 close  hard     mbr_8Ea1       open · bars amended-return.file when it claims a refund
  2046-12-31  the earliest carryforward expires          clock     the brand      open · $13,800 of US credit from 2026
```

## Every kind of time

Four bands: on the job, dates the world sets, across jobs, after the end. **Fires as** is the kind's place in [the tick](#the-tick).

| Time | You write | Fires as | When it comes, the runtime | The money | Its page |
|---|---|---|---|---|---|
| The job's deadline | `price.within`: `'30d'`, or `'5d before file'` | 5 · deadline | At three quarters, at risk; at the end, late. It counts only while the work is the brand's, and written before a clock it follows the clock | `price.late` comes off the charge, from your line | [Deadlines](https://services.dev/docs/service/within.md) |
| A step's deadline | `deliver.<step>.within` | 5 · deadline | The same, for one step, and the due date on its Task | none of its own | [Deadlines](https://services.dev/docs/service/within.md) |
| A party's link | `deliver.<step>.expires` | 3 · ask | A reminder a day before; at expiry it opens once more, and the second expiry ends the job `lapsed` | the hold returns; paid acts already committed are charged | [Asks](https://services.dev/docs/service/ask.md#expiry-reminders-and-lapse) |
| A grant's term | `scope.for` | 3 · ask | Once it ends, a step that needs it is refused `needs_grant`, and the party is asked again | none | [Grants and decisions](https://services.dev/docs/service/grants.md#asked-again) |
| A Task's hold | `Pool.hold` | 4 · task | Warns the member at three quarters; at its end the Task opens again | none | [Tasks](https://services.dev/docs/people/pool/tasks.md#its-clocks) |
| A quiet job | `signals.<s>.silent` | 5 · deadline | Nothing happened for that long: the signal is raised, and its `goto` moves the job | none | [Signals](https://services.dev/docs/service/signals.md#a-silence) |
| A date the world sets | `clocks` | 6 · clock | Rungs give notice, escalate, ask for an acknowledgement and run a step; at the date, unsatisfied, its owner chooses | `exposure` prices a miss; `guarantee` credits a penalty | [Clocks](https://services.dev/docs/service/clocks.md) |
| A date nothing moves | `clocks.<c>.hard` | 6 · clock | Only the calendar moves it; once it passes, the gate refuses the steps it `bars` with `date_passed` | a charge waiting on a barred step never releases | [Clocks](https://services.dev/docs/service/clocks.md#a-date-nothing-moves) |
| A date for each item | `clocks.<c>.each` | 6 · clock | One occurrence per item, such as each state, moved and satisfied only by its own receipt | none | [Clocks](https://services.dev/docs/service/clocks.md#a-date-for-each-item) |
| A date that comes back | `clocks.<c>.every` | 6 · clock | Once satisfied, its next occurrence, counted from the first so it never drifts | none | [Clocks](https://services.dev/docs/service/clocks.md#two-dates-two-clocks) |
| A date that outlives the job | `clocks.<c>.lives_on: 'subject'` | 6 · clock | Stays on the subject when the job ends, in its owner's hands or a steward's | the steward Service charges on its own terms | [Subjects](https://services.dev/docs/service/subject.md#every-open-clock-has-an-owner) |
| A period | `subject.period` | none | Nothing fires: a subject holds one job of the Service per period, and a second order answers `period_taken` | a refused order charges nothing | [One job per period](https://services.dev/docs/service/periods.md) |
| A schedule | `on.schedule` | 2 · schedule | Mints its Service on each date at the calendar's `rings_at`; `catch_up` says what a gap mints | inside an allowance, $0 and counted | [Schedules and triggers](https://services.dev/docs/service/subscriptions.md) |
| A cycle | `price.every` | 1 · cycle | Renews, resets every allowance and posts the cycle's charge as owed | the charge collects at 8 · money | [Subscriptions](https://services.dev/docs/money/subscriptions.md) |
| A season on standby | `Pool.standby` | 8 · money | Standby members are offered first, and the retainer accrues each week of the window | the retainer accrues, from your line | [Pay in the Pool](https://services.dev/docs/people/pool/pay.md#other-lines-a-member-earns) |
| The acceptance window | `price.final_after`: `'7d'` | 7 · window | Seven days after delivery with no send-back, the job is accepted | a charge on acceptance releases | [Release](https://services.dev/docs/money/release.md) |
| A repair window | `deliver.<step>.perfect` | 7 · window | A repair resubmitted inside it keeps the first filing time | none | [Submit steps](https://services.dev/docs/service/submit.md) |
| An outcome's wait | `outcome.due` | 7 · window | No matching payment by then: the outcome lapses | nothing is charged, and share pay never accrues | [Outcomes](https://services.dev/docs/money/outcomes.md) |
| An outcome's window | `outcome.window`: `{ bank: '60d', card: '120d' }` | 7 · window | Armed at each collection, by how it was paid; with `collect: 'each'`, one per invoice, keyed `out_…:window:<invoice>`. Holds the charge and share pay until it closes; then both are final | a refund or chargeback inside it reverses both | [Outcomes](https://services.dev/docs/money/outcomes.md#the-window) |
| An outcome's tail | `outcome.tail`: `'90d'` | 7 · window | After a lost end, watches that long for the account's payment; one that matches turns the job won, charged once | the share and the member's pay, as if won | [Outcomes](https://services.dev/docs/money/outcomes.md#the-tail) |

## The calendar

Every date falls on one calendar, written on `Brand()` or on a Service. A `due` function returns a civil date; the calendar closes it at its minute in its zone, rolls a closed day forward, and records `clock.rolled` with why.

**`calendar`, on `Brand()` or on a Service**

| Key | Written | Left out | Does |
|---|---|---|---|
| `zone` | `string` | `'UTC'` | An IANA zone: `America/New_York`. |
| `weekends` | `Weekday[]` | Saturday and Sunday, once a calendar is written | The days nothing is due on. |
| `closed` | `(year) => string[]` | none | Each year's closed days, as civil dates: `year => ['2028-01-17', …]`. |
| `closes` | `TimeOfDay` | `'23:59'` | The local minute a due date closes. |
| `rings_at` | `TimeOfDay` | `'09:00'` with a zone, else `'00:00'` | The local minute a rung rings on its day. |
| `roll` | `'next open day' \| 'none'` | `'next open day'` | `'next open day'` rolls a date that falls on a weekend or a closed day; `'none'` keeps it. |
| `postpone` | `{ from, through, to, where?, notice }[]` | none | Dates the authority moved after the fact, such as a disaster relief notice: every open clock due from `from` through `through` on a subject `where` matches moves to `to`, hard dates included. Also `POST /owner/calendar/postponements`. |

| The question | The calendar's answer |
|---|---|
| when is a date on time | through the last instant of its `closes` minute: 23:59:59.999 local time |
| whose time decides | the gate's commit on the brand's clock; a submit step's outside receipt time when the system returns one |
| whose zone judges a due | the subject's: a due closes in its calendar's zone, and an e-filed return is on time by its postmark in the filer's own zone |
| which zone a time prints in | traces, timer lists and events print UTC, with `Z`; calendars and notices print the calendar's zone; every header names its zone |
| a proposal near the close | one already in the gate is judged by when it was proposed; one waiting on a person, by when it commits |
| a `rings_at` daylight saving skips | rings at the next minute that exists; in a repeated hour, the first time |
| a postponement | records `clock.moved { by: 'calendar' }` on each clock it moves, citing its notice; the one thing that moves a hard date |

apis.tax's 2027 return is due Saturday 15 April 2028, before a holiday Monday, so the federal, California and New York dates and the date to pay roll to the 18th; New Jersey's is its own, 15 May. A rung files the extension for the three dates it rang for: the date to file moves, the date to pay stays:

```txt
→ POST /test_clocks/default/advance {"to":"2028-04-16T00:00:00Z"}
```

```txt title="captured/clock-calendar.txt · magic values: owner.email = signs@example.test, metadata.sandbox.asks = {"organizer.books":"after 160d"}"
  2028-04-15 13:00Z  clock file · rung run tentative 3d → tentative proposed · committed · US, US-CA, US-NY
  2028-04-15 13:00Z  extend · US accepted → US-CA accepted, US-NY accepted · US-NJ runs its own run tentative 3d on 2028-05-12, three days before 05-15
  2028-04-15 13:00Z  clock file · moved · US, US-NY 2028-04-18 → 2028-10-16, rolled from 2028-10-15 · US-CA 2028-04-18 → 2028-11-15 · clock pay unmoved, still 2028-04-18
  2028-04-15 13:00Z  clock pay · satisfied · pay committed
```

## The tick

The brand keeps one clock. At each instant every timer owed by then fires, in its kind's place, so a cycle renews before its schedule mints and money moves last:

| Rule | The runtime |
|---|---|
| place | 1 cycle, 2 schedule, 3 ask, 4 task, 5 deadline, 6 clock, 7 window, 8 money |
| ties | at one instant, by owed time, then place, then when the timer's owner opened, then [key](https://services.dev/docs/service/timers.md#the-key), byte by byte |
| armed, not fired | a firing that arms a timer owed later records it `armed`; the trace prints it indented under the firing that armed it, and counts it apart from what fired |
| chains | a firing at T that arms a timer owed by T fires in the same instant; the tick repeats to a fixpoint |
| runaway | a chain deeper than 32 passes stops, records `tick.runaway`, and alarms the owner console |

On 1 February 2028 the Tax Plan mints the 2027 return. The mint arms the return's timers in the first pass, and the three `notice 90d` rungs owed on 19 January fire in place 6 in the second, in the same instant. New Jersey's and Texas's wait for 15 February:

```bash
npx services.dev tick --to 2028-02-01T20:00Z --test-clock tc_7Hq4 --trace --on sbn_7Hq2,job_4Tx9
```

```txt title="captured/time-timers.txt · magic values: none"
tc_7Hq4 · 2028-02-01 14:00Z → 20:00Z · UTC · 4 fired · 52 armed · 1 held · 2 passes
  pass  owed               fired              place  kind      on              what
  1     2028-02-01 14:00Z  2028-02-01 14:00Z  2      schedule  sbn_7Hq2        annual → job_4Tx9 · period 2027 · $0.00 · 1 of 1
          armed 1   2029-02-01 14:00Z  2  schedule  sbn_7Hq2   annual · period 2028, the next
          armed 2   2028-07-29 14:00Z  3  ask       job_4Tx9   organizer · 10 items from 2026 (job_4Tx3) · a reminder the day before, then its expiry
          armed 1   2028-04-14 04:00Z  5  deadline  job_4Tx9   late · within 5d before file
          held  1   —                  5  deadline  job_4Tx9   at risk · 54d 10h 30m of work left · held by ask_9Ts4 while the organizer waits
          armed 45  2028-01-19 14:00Z  6  clock     clk_8Tx1   file · 5 jurisdictions × 2 notices, a run, 5 acks and the date · 3 already owed
          armed 3   2028-04-04 13:00Z  6  clock     clk_8Tx2   pay · 2 rungs and the date
  2     2028-01-19 14:00Z  2028-02-01 14:00Z  6      clock     clk_8Tx1#US-CA  file · notice 90d · its day passed: fired in place
  2     2028-01-19 14:00Z  2028-02-01 14:00Z  6      clock     clk_8Tx1#US-NY  file · notice 90d · its day passed: fired in place
  2     2028-01-19 14:00Z  2028-02-01 14:00Z  6      clock     clk_8Tx1#US     file · notice 90d · its day passed: fired in place
```

A timer never writes the record. A rung that runs a step proposes it, and [the gate](https://services.dev/docs/service/gate.md) commits or refuses it.

## Timers and replay

Every kind of time compiles to one timer, `tmr_…`, under a key no move of a date rewrites, armed, fired, superseded or held, and pinned with the bundle it was armed under. [Timers and replay](https://services.dev/docs/service/timers.md) is the runtime contract: the key, the states and how each resumes, what re-arms a timer, a gap, a test clock, redelivery, and a job that outlives a deploy.

## Lapses

Nothing lapses silently. Each lapse is on the record, with who hears it:

| What lapses | When | The runtime | Who hears | Its page |
|---|---|---|---|---|
| a clock | its date comes, unsatisfied | waits on its owner's choice; an option that names a step runs it through the gate | its owner, then the owner console | [Clocks](https://services.dev/docs/service/clocks.md#what-happens-on-its-own) |
| a hard date | it passes, unsatisfied | the gate refuses the steps it bars with `date_passed` | its owner and the owner console | [Clocks](https://services.dev/docs/service/clocks.md#a-date-nothing-moves) |
| a job | a party's link expires twice | ends the job `lapsed`; the hold returns | buyer, owner | [Asks](https://services.dev/docs/service/ask.md#expiry-reminders-and-lapse) |
| a grant | its term ends | steps that need it are refused `needs_grant`, and a run rung that needs it is held | the party, asked again | [Grants and decisions](https://services.dev/docs/service/grants.md#asked-again) |
| an outcome | `outcome.due` passes with no payment | the outcome lapses, and nothing is charged | buyer, member, owner | [Outcomes](https://services.dev/docs/money/outcomes.md) |
| a seat of record | the member's credential or coverage lapses | the custodian covers the seat, rung by rung, and the console alarms | owner, then the custodian | [Of record](https://services.dev/docs/people/of-record.md) |
| a subscription | a renewal is still unpaid when dunning runs out | allowances freeze; jobs a schedule mints are held | buyer, owner | [Subscriptions](https://services.dev/docs/money/subscriptions.md#a-renewal-that-fails) |

## Time and money

Three money rules run across the kinds:

| When | The money | Its page |
|---|---|---|
| a clock's owner is the brand or of record, and it carries `guarantee` | a penalty notice for that period is credited from your line, up to the cap; AI and members' pay stay whole | [Clocks](https://services.dev/docs/service/clocks.md#price-a-missed-date) |
| a job lapses, or is canceled before delivery | the hold returns; paid acts already committed are charged, and their pay is the member's | [Release](https://services.dev/docs/money/release.md) |
| your plan or your file changes | a job, subject or subscription keeps the plan it was ordered under and the file it opened under, so a clock that runs for years runs to its end | [Across versions](https://services.dev/docs/service/timers.md#across-versions) |

## In the apps

| App | Shows | Acts |
|---|---|---|
| [Customer portal](https://services.dev/docs/app/buyers.md#dates-periods-and-allowances) | Coming up: each subject's calendar, every date ahead across clocks, schedules, links and grants, each with what a miss costs; each subscription's allowances | acknowledge, mark done and choose on their own clocks; keep a date with the steward; cancel at the cycle's end |
| [Delivery portal](https://services.dev/docs/app/members.md#the-dates-they-answer-for) | Your dates: each Task's due date and hold beside the clocks the member answers for, late first; the standby window | extend a hold once; acknowledge, mark done and choose on their own clocks; withdraw from a subject with a successor |
| [Owner console](https://services.dev/docs/operate/dates.md) | Dates at risk, sorted by what a miss costs, each with why and the control that settles it; timers that fired late; the next 30 days | assign an owner; mark the brand's own dates done; choose on the brand's own |

## The words

| Word | Means | Its page |
|---|---|---|
| occurrence | one coming of a date: a clock's next `every`, one state of an `each`, one period of a schedule | [Clocks](https://services.dev/docs/service/clocks.md) |
| owed, fired | when a timer was due, and when it ran; `late_by` is the gap | [Timers and replay](https://services.dev/docs/service/timers.md#every-timer-is-on-the-record) |
| rung | a step before a clock's date: notice, escalate, ack or run. Not the Pool's fill ladder, nor a custodian's | [Clocks](https://services.dev/docs/service/clocks.md#what-happens-on-its-own) |
| roll | a date moved off a weekend or closed day by the calendar | [The calendar](#the-calendar) |
| lapse | a date, link, grant or outcome that passed unkept, always on the record | [Lapses](#lapses) |
| hard date | a date no step moves; only the calendar's postponement does | [Clocks](https://services.dev/docs/service/clocks.md#a-date-nothing-moves) |

## Plans

Every key runs on every plan in test. Live, time that stays on the job runs on Free; time a Service holds for someone past the job runs on Launch; time that needs a market of people, or that you sell as a guarantee, runs on Team:

| Does | Key | Plan |
|---|---|---|
| A deadline, a late price, and escalation at risk or late | `within, ends, policy` | Free |
| Filings, and the repair window that keeps the first filing time | `deliver.<step>.submit` | Free |
| Dates the world sets on a job: rungs, moves, hard dates and lapse choices | `clocks` | Free |
| A clock that outlives its job, on the subject | `clocks.lives_on: 'subject'` | Launch |
| A subject, and one job of a Service per period | `subject, subject.period` | Launch |
| Facts kept for the next period | `subject.keeps` | Launch |
| Subscriptions with allowances | `price.every, price.includes` | Launch |
| Schedules that mint jobs on the minted Service's own dates | `on.schedule` | Launch |
| Events that mint a Service the subscription's allowance counts | `on.event` | Launch |
| Outcome windows that hold a charge and share pay | `price.share, outcome` | Launch |
| Events routed past the subscription, and grants kept on a subject | `on.event` | Team |
| Who keeps a subject's dates between jobs | `subject.of_record.custodian, subject.steward` | Team |
| A date sold as a promise, its penalties credited from your line | `clocks.<c>.guarantee` | Team |
| Committed hours for a dated season | `Pool.standby` | Team |

---

- Up: [Service()](https://services.dev/docs/service.md)
- Previous: [The job lifecycle](https://services.dev/docs/service/lifecycle.md)
- Next: [Timers and replay](https://services.dev/docs/service/timers.md)
