Signals
A signal is a named fact about the job. A predicate turns true, a silence runs its span, or your system says so; a signal can move the job, and never blocks it.
A signal answers one question: what has happened on this job that the work should act on?
signals: {
long: job => job.input.pages > 30,
source_changed: { inbound: true, goto: 'translate' },
quiet: { silent: '3d', goto: 'nudge' },
},On the Service
| Key | Written | Left out | Does |
|---|---|---|---|
signals | { <name>: Signal } | none | Named facts that route the work: a predicate, silent for a span, with of for whose silence counts, or inbound; goto moves the job. |
A named fact. A predicate is raised when it turns true; silent is raised when nothing happens on the job for that long, a span or a function of the job that returns one, and of: '<party input>' counts only that party's silence; inbound is raised by POST /jobs/:id/signals. goto interrupts the graph and moves the job to that step, and job.record.interrupted names the step it left. A signal never blocks: that's a guard's job.
A function of the job is raised when it turns true, and the record shows it from then on. A 48-page catalog is long from the moment it's ordered:
→ POST /watched-translation {"document":"catalog.pdf","to":"de","pages":48,"max_usd":79,"key":"watched"}← job_7Hq2 · waiting · on person · $79.00 held
record long · signal | translate · ai{ inbound: true } is raised by your buyer's own system, through POST /jobs/:id/signals. Its goto moves the job to that step, and a step that already ran runs again:
→ POST /jobs/job_7Hq2/signals {"signal":"source_changed"} record long · signal | translate · ai | source_changed · signal · goto translate | translate · ai · redoOnly inbound signals take a call. A predicate is raised by the work:
→ POST /jobs/job_7Hq2/signals {"signal":"long"} "code": "input_invalid",
"reason": "long isn't an inbound signal of Watched Translation: it takes source_changed.",{ silent: '3d' } is raised when nothing happens on the job for that long. Three quiet days on the proofreader raise quiet, whose goto runs nudge, and the job goes back to waiting on them:
curl http://localhost:8787/owner/jobs/job_7Hq2 # 2026-10-28: three quiet days← job_7Hq2 · waiting · on person · $79.00 held
record long · signal | translate · ai | source_changed · signal · goto translate | translate · ai · redo | quiet · signal · goto nudge | nudge · aiof names whose silence counts. { silent: '14d', of: 'account' } counts only the account's: a member's own touches on the thread never hold it off, and only the party's reply resets it. Left out, anything on the job resets it. When silent reads a policy, a policy.set that changes it re-arms the timer from the last activity: api.forsale's gone_dark reads stall_days, and what re-arms a timer runs the change.
On localhost a test clock moves the time: services.dev tick 3d.
It can route the job with goto, to any step, a routed one included, and the step it interrupted stays on the record as job.record.interrupted, so a routed step can send the job back where it was: next: job => job.record.interrupted. Every predicate, when, next and guard reads raised signals as job.signals.
It also reaches the people on the job. Every Task the job posts carries its raised signals, and a Pool whose sees names them shows them in the teaser, before anyone claims. A rules row can raise one of its own with flags, such as a letter whose recipient is in another state:
→ POST /reviews/tsk_7Hq2/attest {"checklist":["every claim is supported","no threat the client cannot carry out"]} # as mbr_eligible← 200 tsk_7Hq2 · done · attorney · sign · US-NY · $75.00 · signals recipient_elsewhere · attested · 30 min · $150.00 an hour · due 2026-10-27 14:02
14:02 · step.committed · sign · person · bar admission · US-NY · mbr_eligible
14:02 · pay.accrued · mbr_eligible · attest · $75.00A signal can also staff a step with escalate and price a job by tier.
A signal never blocks. Refusing an output is a guard's job.
| Op | API | CLI | MCP | SDK | Site | Docs | App |
|---|---|---|---|---|---|---|---|
job.signal | POST /jobs/:id/signals | jobs signal <job> <signal> | send_job_signal | jobs.signal() | 3 error pages | customer portal |
Events: signal.raised.
Operations has every column of every row.
| Code | Category | HTTP | Why |
|---|---|---|---|
input_invalid | invalid_request | 422 | The order doesn't match the Service's input. |
job_ended | conflict | 409 | The job has ended, for good. |
The lines that use it in the example brands, as their files write them, and what check read aloud for each file.
signals: {services/demand-letter.ts · 26 lines · $149 per job, within 48 hours, charged on send's receipt.
steps consent · client · consent → draft · ai → attack · ai · adversarial · 3 lenses → sign · attorney · client · stamped · 2-item checklist → send · the recipient → courier · a courier · routed → reply · ai → settle · client · decideThe legal example runs every one of its files, captured.
signals: {
warm: job => job.record.replies > 0, committee: { inbound: true }, person: { inbound: true },
gone_dark: { silent: job => `${job.policy.stall_days}d`, of: 'account', goto: 'nurture' } },services/deal.ts · 42 lines · nothing on effort; 5% of closed_value at won, proven by the account's payment, held 60 days by bank, 120 by card.
signals warm · a reply on the record → book escalates to the setter
committee, person · inbound → meet escalates to the closer
gone_dark · the account silent for stall_days → nurture, and a revived deal returns to the step it leftThe sales example runs every one of its files.