# Signals

> Release a worker slot until another process delivers a named JSON payload.

`HandlerContext.waitForSignal` records a durable boundary and releases the lease. When another
process delivers the named signal, Workhorse makes the same logical attempt ready and restarts the
handler from its entry point.

<Tabs items={["TypeScript", "Python", "Go"]}>
  <Tab value="TypeScript">
    ```ts
    const approval = await context.waitForSignal<{ approved: boolean }>("approval");
    if (approval.approved) await publishOrder();
    ```
  </Tab>
  <Tab value="Python">
    ```python
    approval = context.wait_for_signal("approval")
    if isinstance(approval, dict) and approval.get("approved") is True:
        publish_order()
    ```
  </Tab>
  <Tab value="Go">
    ```go
    value, err := handler.WaitForSignal("approval")
    if err != nil {
        return nil, err
    }
    approval, ok := value.(map[string]any)
    if ok && approval["approved"] == true {
        publishOrder()
    }
    ```
  </Tab>
</Tabs>

Checkpoint earlier effects because replay starts from the top. When replay reaches the same name,
`waitForSignal` returns the retained payload.

Go handlers can pass `ExternalWaitOptions` when the boundary needs a shorter lifetime. Python
handlers pass `timeout_ms` for the same boundary.

## Deliver once, retry safely

Applications call `Queue.sendSignal(jobId, name, payload, request)` with an idempotency key and a
trusted actor. An equivalent network retry returns the retained delivery. Reusing a key with
changed payload or attribution conflicts, and a competing late delivery returns the accepted
winner without resuming the handler again.

If another transition has already closed the boundary, the delivery returns `stale` and leaves the
retained outcome unchanged.

A delivery before the wait exists is rejected rather than buffered. A timeout, job deadline, or
cancellation closes the boundary, so replay cannot continue with an invented payload.

`Admin.listSignalWaits()` provides the actionable operator projection. The dashboard uses the same
public method, but its server replaces browser-supplied attribution with the authenticated actor.

Go applications deliver through `Queue.SendSignal` with `ExternalWaitDelivery`. The returned
`SignalDeliveryResult` contains the database status and retained payload.

TypeScript handlers call `ctx.waitForSignal` and pass `timeoutMs` when the wait needs a deadline. Go
uses `HandlerContext.WaitForSignal`; both return a `nextCursor` that prevents an earlier signal from
being consumed again.

## Next

- [Human waits](/docs/human-waits) — collect an operator decision with stored context
- [Durable execution](/docs/durable-execution) — understand handler replay
- [Agentic flow](/docs/agentic-flow) — wait for approval inside a durable loop

---

Exact signal statuses, bounds, and transitions:
[architecture reference](https://github.com/stablemates/workhorse/blob/main/docs/architecture.md#durable-signal-suspension).
