---
title: "Signals"
url: https://services.dev/docs/service/signals
captured: [legal-demand.txt, legal-walk.txt, sales-check.txt, service-signals.txt]
codes: [input_invalid, job_ended]
---
# Signals

URL: https://services.dev/docs/service/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?

## Smallest form

```ts title="services/watched-translation.ts"
  signals: {
    long: job => job.input.pages > 30,
    source_changed: { inbound: true, goto: 'translate' },
    quiet: { silent: '3d', goto: 'nudge' },
  },
```

## Every form

**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 predicate

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:

```txt
→ POST /watched-translation {"document":"catalog.pdf","to":"de","pages":48,"max_usd":79,"key":"watched"}
```

```txt title="magic values: none"
← job_7Hq2 · waiting · on person · $79.00 held
  record  long · signal | translate · ai
```

## Inbound

`{ 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:

```txt
→ POST /jobs/job_7Hq2/signals {"signal":"source_changed"}
```

```txt title="200 · job_7Hq2 · working · $79.00 held · magic values: none"
  record  long · signal | translate · ai | source_changed · signal · goto translate | translate · ai · redo
```

Only inbound signals take a call. A predicate is raised by the work:

```txt
→ POST /jobs/job_7Hq2/signals {"signal":"long"}
```

```txt title="422 · magic values: none"
  "code": "input_invalid",
  "reason": "long isn't an inbound signal of Watched Translation: it takes source_changed.",
```

## A silence

`{ 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:

```bash
curl http://localhost:8787/owner/jobs/job_7Hq2   # 2026-10-28: three quiet days
```

```txt title="magic values: none"
← 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 · ai
```

`of` 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](https://services.dev/docs/service/timers.md#what-re-arms-a-timer) runs the change.

On localhost a test clock moves the time: `services.dev tick 3d`.

## What a signal can do

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](https://services.dev/docs/people/rules.md) can raise one of its own with `flags`, such as a letter whose recipient is in another state:

```txt
→ POST /reviews/tsk_7Hq2/attest {"checklist":["every claim is supported","no threat the client cannot carry out"]}   # as mbr_eligible
```

```txt title="magic values: metadata.sandbox.propose = { settlement: [3800] }, member = 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.00
```

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

## Seven surfaces

| 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](https://services.dev/docs/reference/operations.md) has every column of every row.

## Refusals

| Code | Category | HTTP | Why |
|---|---|---|---|
| [`input_invalid`](https://services.dev/docs/reference/errors.md#input_invalid) | invalid_request | 422 | The order doesn't match the Service's input. |
| [`job_ended`](https://services.dev/docs/reference/errors.md#job_ended) | conflict | 409 | The job has ended, for good. |

## 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/demand-letter.ts · 26 lines"
  signals: {
```

```txt title="npx services.dev check · captured/legal-walk.txt"
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 · decide
```

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

```ts title="sales · services/deal.ts · 42 lines"
  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' } },
```

```txt title="npx services.dev check · captured/sales-check.txt"
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 left
```

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

---

- Up: [Service()](https://services.dev/docs/service.md)
- Previous: [What the buyer sets, and what only the client sets](https://services.dev/docs/service/policy.md)
- Next: [How a job ends](https://services.dev/docs/service/ends.md)
