Subjects
A subject is what outlives the job, keyed by an input or a receipt. It keeps typed facts across jobs, insert-only, tells every job that read a fact when an amendment restates it, holds one job of a Service per period, names who is of record, and keeps its open clocks in someone's hands between jobs and after a client leaves.
subject answers one question: what does this work belong to, that the next job should remember?
One line names the subject, the period each job holds, and the fact it keeps:
subject: { kind: 'account', key: 'account', period: 'year', keeps: { glossary: 'glossary' } },The second year reads what the first one kept, with no input to carry it:
→ POST /glossary {"account":"acme","year":"2026","terms":["Premises"],"max_usd":19,"key":"glossary-2026"}
output {"glossary":["Lessee","Lessor","Premises"],"last_year":["Lessee","Lessor"]}On the Service
| Key | Written | Left out | Does |
|---|---|---|---|
subject | SubjectSpec | none | What outlives the job: a subject keyed by an input or a receipt, the member of record on it, its kept facts, its periods, and the Service that watches its clocks. |
subject, written out
| Key | Written | Left out | Does |
|---|---|---|---|
kind | string | required | What it is, in your words: 'patent family', 'trademark', 'company'. |
key | (keyof I | keyof O) & string | required | The input, or the output a receipt fills, whose value names it: application_number, ein. |
of_record | { role: RoleWord; named_at: <step>; custodian?: (RoleWord | 'brand')[] } | none | The member of record: whoever attests at named_at stays of record on the subject across jobs. Withdrawal names a successor; a member whose credential or coverage lapses leaves it vacant, and custodian covers it, rung by rung, then the brand. |
keeps | K | none | Facts the subject keeps across jobs, each from the output that writes it: { carryforwards: 'carryforwards', tax: 'tax' }, typed as that output is. Written only when a job ends accepted, never when it is canceled or lapses, and keyed by the period the job held. Insert-only: each value records { value, job, step, at, period }, and nothing is overwritten. |
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. |
steward | AnyService | none | The Service that keeps the subject's clocks once a job ends with one still running: ordered with the subject's key, it keeps the date in someone's hands. Until it is, the customer portal offers it beside an ask in which the client acknowledges they own the date, and nothing lapses into a gap between jobs. |
In your code, on the job
| Key | Written | Left out | Does |
|---|---|---|---|
subject | SubjectView | required | The job's subject, as it stands: its kept facts, and the same Service's work on it last period. |
keeps maps a fact to the output that writes it. A fact is written only when a job ends accepted, keyed by the period that job held, and never overwritten.
Your code reads it on the job:
| In your code | Reads |
|---|---|
job.subject.fact('glossary') | On a job with a period, the latest value from the closest period before its own; on one without, the latest written. Undefined until an accepted job writes it. |
job.subject.fact('glossary', { period: '2025' }) | The latest value written for that period. 'prior' and 'latest' name the two defaults. |
job.subject.fact('pages', { period: '2027', all: true }) | Every value of that period and the periods inside it, 2027-Q1 to 2027-Q4, oldest first. |
job.subject.fact('glossary', { all: true }) | Every value, oldest first. |
job.subject.prior('draft') | That step's committed output on the latest ended job of this Service with a lower period. |
ViewOf<typeof glossary> | The job, typed from the definition, so fact('glossary') is a string[]. Code shared across Services takes AnyView and names the type: fact<string[]>('glossary'). |
So a late job for an earlier period never changes what the next one reads.
The record keeps every fact a job read, with the value's id. A newer value written to a period another job read raises fact.restated on that reader and the subject:
| Then | Who |
|---|---|
| the reader's end offers its follow-on, in the customer portal and the owner console | by default |
| the plan mints the follow-on | a plan with on.event: { 'fact.restated': amendedReturn } |
apis.tax amends 2025 in November 2027, and the 2026 return read its carryforwards. The Tax Plan routes the event, and amendments sit outside its allowances, so the mint is offered at its price:
$ npx services.dev events --since 2027-11-30 --on sbj_7Hq2 --test-clock tc_7Hq4
evt_9Ts1 job.accepted job_4Ty3 amended-return · period 2025 · the subject keeps carryforwards 2025 and tax 2025, newer values
evt_9Ts2 fact.restated sbj_7Hq2 carryforwards 2025 · read by job_4Tx3 (period 2026, at compute 2027-07-13)
$ npx services.dev jobs get job_4Tx3 --test-clock tc_7Hq4
job_4Tx3 · accepted · annual-return · period 2026 · $0.00 on sbn_7Hq2
read carryforwards 2025 · fct_7Hq4 (job_4Tx2) · restated by fct_7Hr9 (job_4Ty3) 2027-11-30
GET /subjects/:id is the whole record: every job that touched it, its history, each fact with every value, and the periods its jobs hold:
→ GET /subjects/sbj_7Hq2
{
"id": "sbj_7Hq2",
"object": "subject",
"livemode": false,
"kind": "account",
"key": {
"field": "account",
"value": "acme"
A clock with lives_on: 'subject' keeps running after the job that opened it ends. An agent reads a subject through its clocks, on any surface:
$ npx services.dev clocks list subject=sbj_7Hq2 --json # the subject's clocks, its job ended
"id": "clk_7Hq2",
"name": "franchise_tax",
"lives_on": "subject",
"status": "open",
$ curl http://localhost:8787/clocks?subject=sbj_7Hq2 # the subject's clocks, its job ended
"id": "clk_7Hq2",
"name": "franchise_tax",
"lives_on": "subject",
"status": "open",
→ tools/call list_clocks {"subject":"sbj_7Hq2"} # the subject's clocks, its job ended
"id": "clk_7Hq2",
"name": "franchise_tax",
"lives_on": "subject",
"status": "open",
await lingua.clocks.list({"subject":"sbj_7Hq2"}) // the subject's clocks, its job ended
"id": "clk_7Hq2",
"name": "franchise_tax",
"lives_on": "subject",
"status": "open",
Every subject with an open clock has someone who answers for it. When a job ends with a clock still running, the record says so, and the client is asked to own the date:
→ GET /owner/jobs/job_7Hq2
clock.outlived · franchise_tax · 2027-03-25 · the client owns it, or orders Company Watch
ask.opened · own_dates · sign · buyersteward names the Service that keeps the subject's dates instead. The customer portal offers it beside the ask: mark the date done, hand it to the steward, or sign that it is theirs:
→ GET /portal
franchise tax · due 25 Mar 2027 · you answer for it · notice 23 Feb
[Mark it done] clock.satisfy · POST /clocks/clk_7Hq2/satisfy
[Keep it with Company Watch · $9] service.order · POST /company-watch
[I'll own this date] ask.complete · POST /asks/ask_7Hq2→ GET /clocks?job=job_7Hq2
clk_7Hq2 · franchise_tax · due 2027-03-25 · buyer · open · on the subject · every year · the client signed for it 2026-10-25Once a job of the steward joins the subject, the ask is voided. Nothing lapses into a gap between jobs:
→ GET /owner/jobs/job_7Hq3
ask.opened · ask_7Hq3
ask.voided · ask_7Hq3 · Company Watch keeps the dates now: job_7Hq4.A client who leaves gets the same hand-back: when a canceled plan's last cycle ends, each open date the brand or a member of record answered for is offered to the client. apis.tax's steward, Carryforward Watch, is $9 a year, so the 2046 carryforward keeps an owner:
→ GET /portal/subjects/sbj_7Hq2/letter
Your Tax Plan ended on 20 January 2029. These dates are still running, and from today they are yours to keep:
2029-04-17 refund claims on your 2025 return close, and so does the IRS's time to assess it
2030-07-11 the same, for your 2026 return
of_record names the member who attests at a step. They stay of record on the subject across jobs, and a clock with owner: 'of_record' is theirs. Withdrawing names a successor who holds the role's credential. When the member's credential or coverage lapses, the seat turns vacant and custodian covers it, rung by rung, then the brand. Of record walks it.
With period, a subject holds one job of the Service per period, and a second order for one is refused with its follow-on: One job per period.
A subject keeps no timer of its own. Its jobs, its clocks and the people on it move it:
| When | The runtime | Event |
|---|---|---|
| a job ends accepted | writes each kept fact, keyed by the period that job held | job.accepted |
| a newer value is written to a period another job read | names each reader; the reader's end offers its follow-on, or a plan's on.event mints it | fact.restated |
| a job ends with a clock still running | keeps the clock on the subject, and asks the client to own the date until a steward's job joins | clock.outlived |
| the member of record's credential or coverage lapses | the seat turns vacant, and the custodian covers it, rung by rung, then the brand | clock.vacant |
| a canceled plan's last cycle ends | offers the client every open clock the brand or a member of record answered for | clock.outlived |
| the member of record withdraws | names a successor who holds the role's credential, who accepts it | succession.started, succession.accepted |
| App | Shows | Acts |
|---|---|---|
| Customer portal | each subject in your word for it: one row per period, each kept fact with the job that wrote it, who is of record, and its dates | keep a running date with the steward Service, or sign that it is yours |
| Delivery portal | the subjects the member is of record on, with the dates they answer for | withdraw, naming a successor |
| Owner console | a seat of record left vacant, and any date on a subject no one answers for | assign a member who holds the credential |
A subject has no price of its own: each job on it, and its steward, charges on its own terms. It keeps the plan it was ordered under, so its clocks run to their end after a downgrade. Subjects, periods and kept facts run live on Launch; the custodian ladder and a steward on Team. Every kind of time, by plan: Keeping time.
| Op | API | CLI | MCP | SDK | Site | Docs | App |
|---|---|---|---|---|---|---|---|
subject.list | GET /subjects, GET /owner/subjects | subjects list | never: an agent reads its clocks with list_clocks | never: the SDK reads its clocks with clocks.list() | 1 error page | customer portal, owner console | |
subject.get | GET /subjects/:id, GET /owner/subjects/:id | subjects get <subject> | never: an agent reads its clocks with list_clocks | never: the SDK reads its clocks with clocks.list() | 1 error page | customer portal, owner console | |
subject.withdraw | POST /subjects/:id/withdraw | subjects withdraw <subject> <successor> | never: a person acts | never: a person acts | 2 error pages | delivery portal | |
subject.calendar | GET /subjects/:id/calendar, .ics | subjects calendar <subject> | never: an agent reads the dates with list_clocks | never: the SDK reads the dates with clocks.list() | customer portal | ||
member.subjects | GET /me/subjects | me subjects | never: a person acts | never: a person acts | delivery portal |
Objects: sbj_.
Events: fact.restated, succession.started, succession.accepted.
Operations has every column of every row.
| Code | Category | HTTP | Why |
|---|---|---|---|
not_found | invalid_request | 404 | Nothing has that id. |
check gate | Why |
|---|---|
clock_unowned | A clock's owner can never resolve: a role the Service doesn't have, or of_record with no member of record on its subject. It would run vacant, with no one to answer for the date. |
| When the file won't load | Thrown by | Why |
|---|---|---|
writes_unknown | Service() | Service Glossary: subject.keeps.glossary is written from terms, which isn't an output. A kept fact is written from the output: glossary. |
The lines that use it in the example brands, as their files write them, and what check read aloud for each file.
subject: { kind: 'patent family', key: 'matter' },services/docket-watch.ts · 10 lines · $12 per month, charged at the start of each month.The legal example runs every one of its files, captured.
subject: {
kind: 'taxpayer', key: 'ein', period: { of: 'tax_year', ends: job => job.input.year_end ?? '12-31' },
of_record: { role: 'signer', named_at: 'sign', custodian: ['signer', 'brand'], until: 'assessment' },
keeps: { carryforwards: 'carryforwards', tax: 'tax', paid: 'payment_usd', first_installment: 'first_installment' }, steward: carryforwardWatch,
},services/annual-return.ts · 45 lines · $1,800 per job, filed 5 days before its deadline, 20% off if late, or we pay the penalty and its interest, up to $5,000, after we ask the IRS to take it off, charged when file is accepted.Two clients change, amend and leave in apis.tax.
A year, and the next reads one subject's facts across two years.