---
title: "Signals"
description: "Ask a model one question about each request and get the answer back as a column: who broke it, is it worth paging, did the customer get what they came for."
canonical_url: "https://www.evlog.dev/use-cases/signals/overview"
---
# Signals

> Ask a model one question about each request and get the answer back as a column: who broke it, is it worth paging, did the customer get what they came for.

Your logs tell you a request returned 502. A signal tells you it was Stripe, that a retry would have worked, and that it is not worth waking anyone. Your logs tell you a checkout returned 200. A signal tells you the customer left without an order.

`@evlog/signals` asks a decision model one question per event and writes the answer onto the event as a column, with a confidence. You query it like any other field.

## What you get

A signal is a question in English, attached to the events it applies to. When an event matches, the model reads it, answers with a probability for each option, and the answer is written back onto the event as a typed column:

<signal-ask>



</signal-ask>

The question is the whole configuration. There is no prompt to tune and no parser to write: a yes/no gives a boolean, a list of options gives one of them, an ordered rubric gives a level and a position. Every column carries the model's confidence, so `WHERE signals.fault.value = 'upstream' AND signals.fault.confidence > 0.9` is a query, not a guess.

Three things that gives you, that a rule on the event's fields could not:

- **Every error gets a reason.** `fault=upstream` on the Stripe 502, `fault=app` on the null deref. Two 5xx that look identical on a status chart are two rows in a `GROUP BY`. Over a week that is a number: how much of your error budget is yours.
- **A 200 can be a failure.** A checkout that answered 200 and created no order, a webhook acknowledged and dropped, an agent looping on the same tool call. No field on those events says anything is wrong; a signal with `keep` carries them past sampling, and the next section shows that.
- **It costs nothing you would notice.** A decision model answers in one short call, every due signal in it: 15 requests in the demo run came to 14 calls, 1,569 input tokens, $0.00007. [What It Costs](https://www.evlog.dev/use-cases/signals/cost) has what that becomes at your traffic.

## Add your first signal

<prompt :actions="["copy","cursor","claude"]" description="Add signals to an evlog app" icon="i-lucide-scan-search">

Add @evlog/signals to my evlog app.

- Install: pnpm add @evlog/signals ai (ai 7.0.105 or later)
- Define signals with defineSignal({ name, when, ask }) plus choice: {...} for a pick-one answer or score: <span>

...

</span>

 for a rubric; keep: v => ... promotes the event past sampling
- Register with createSignals({ signals }): on Nuxt or Nitro hook plugin.keep on 'evlog:emit:keep' and plugin.enrich on 'evlog:enrich' in a server plugin; on Next.js or standalone pass it in initLogger({ plugins: <span>

...

</span>

 }); on Hono, Express, Fastify or Elysia pass it in the middleware's plugins option
- Set AI_GATEWAY_API_KEY (default model typesafe-ai/jev through AI Gateway)
- Verdicts land on event.signals.<name>

.{ value, confidence } in stdout and in every drain

</name>
- In tests, pass evaluate from scriptedEvaluate() in @evlog/signals/testing so no model is called

Docs: [https://www.evlog.dev/use-cases/signals/overview](https://www.evlog.dev/use-cases/signals/overview)

</prompt>

<steps level="3">

### Install

```bash [Terminal]
pnpm add @evlog/signals ai
```

The default model is `typesafe-ai/jev` through Vercel AI Gateway. Set `AI_GATEWAY_API_KEY`, or nothing on Vercel, where OIDC is picked up.

### Write the question

A signal is a name, a predicate that decides which events it applies to, and the question. This one labels every failure:

```typescript [server/signals.ts]
import { defineSignal } from '@evlog/signals'

export const fault = defineSignal({
  name: 'fault',
  when: e => (e.status ?? 0) >= 400,
  ask: 'Who is responsible for this failure?',
  choice: {
    client: 'Bad input, expired session, client mistake',
    app: 'A bug or misconfiguration in our own code',
    upstream: 'A third-party dependency failed',
  },
})
```

### Register it

`createSignals` returns an evlog [plugin](https://www.evlog.dev/extend/plugins). Nuxt and Nitro expose the two hooks directly; every other integration takes it in a `plugins` array:

<framework-tabs>

```typescript [Nuxt]server/plugins/evlog-signals.ts
import { createSignals } from '@evlog/signals'
import { fault } from '../signals'

export default defineNitroPlugin((nitroApp) => {
  const plugin = createSignals({ signals: [fault] })
  nitroApp.hooks.hook('evlog:emit:keep', plugin.keep)
  nitroApp.hooks.hook('evlog:enrich', plugin.enrich)
})
```

```typescript [Nitro]server/plugins/evlog-signals.ts
import { definePlugin } from 'nitro'
import { createSignals } from '@evlog/signals'
import { fault } from '../signals'

export default definePlugin((nitroApp) => {
  const plugin = createSignals({ signals: [fault] })
  nitroApp.hooks.hook('evlog:emit:keep', plugin.keep)
  nitroApp.hooks.hook('evlog:enrich', plugin.enrich)
})
```

```typescript [Next.js]lib/evlog.ts
import { initLogger } from 'evlog'
import { createSignals } from '@evlog/signals'
import { fault } from './signals'

initLogger({ plugins: [createSignals({ signals: [fault] })] })
```

```typescript [SvelteKit]src/hooks.server.ts
import { createEvlogHooks } from 'evlog/sveltekit'
import { createSignals } from '@evlog/signals'
import { fault } from './signals'

export const { handle, handleError } = createEvlogHooks({ plugins: [createSignals({ signals: [fault] })] })
```

```typescript [TanStack Start]server/plugins/evlog-signals.ts
import { definePlugin } from 'nitro'
import { createSignals } from '@evlog/signals'
import { fault } from '../signals'

export default definePlugin((nitroApp) => {
  const plugin = createSignals({ signals: [fault] })
  nitroApp.hooks.hook('evlog:emit:keep', plugin.keep)
  nitroApp.hooks.hook('evlog:enrich', plugin.enrich)
})
```

```typescript [React Router]app/root.tsx
import { evlog } from 'evlog/react-router'
import { createSignals } from '@evlog/signals'
import { fault } from './signals'

export const middleware: Route.MiddlewareFunction[] = [
  evlog({ plugins: [createSignals({ signals: [fault] })] }),
]
```

```typescript [Hono]src/index.ts
import { evlog } from 'evlog/hono'
import { createSignals } from '@evlog/signals'
import { fault } from './signals'

app.use(evlog({ plugins: [createSignals({ signals: [fault] })] }))
```

```typescript [Express]src/index.ts
import { evlog } from 'evlog/express'
import { createSignals } from '@evlog/signals'
import { fault } from './signals'

app.use(evlog({ plugins: [createSignals({ signals: [fault] })] }))
```

```typescript [Fastify]src/index.ts
import { evlog } from 'evlog/fastify'
import { createSignals } from '@evlog/signals'
import { fault } from './signals'

await app.register(evlog, { plugins: [createSignals({ signals: [fault] })] })
```

```typescript [Elysia]src/index.ts
import { evlog } from 'evlog/elysia'
import { createSignals } from '@evlog/signals'
import { fault } from './signals'

app.use(evlog({ plugins: [createSignals({ signals: [fault] })] }))
```

```typescript [NestJS]src/app.module.ts
import { EvlogModule } from 'evlog/nestjs'
import { createSignals } from '@evlog/signals'
import { fault } from './signals'

EvlogModule.forRoot({ plugins: [createSignals({ signals: [fault] })] })
```

```typescript [oRPC]server/orpc.ts
import { withEvlog } from 'evlog/orpc'
import { createSignals } from '@evlog/signals'
import { fault } from './signals'

const handler = withEvlog(new RPCHandler(router), { plugins: [createSignals({ signals: [fault] })] })
```

```typescript [Cloudflare Workers]src/worker.ts
import { withEvlog } from 'evlog/workers'
import { createSignals } from '@evlog/signals'
import { fault } from './signals'

export default withEvlog(handler, { plugins: [createSignals({ signals: [fault] })] })
```

```typescript [Standalone]
import { initLogger } from 'evlog'
import { createSignals } from '@evlog/signals'
import { fault } from './signals'

initLogger({ plugins: [createSignals({ signals: [fault] })] })
```

</framework-tabs>

### Read the column

The next 500 that reaches your drain carries the answer:

```json [Wide Event]
{
  "method": "POST",
  "path": "/api/checkout",
  "status": 502,
  "error": { "name": "FetchError", "message": "request to https://api.stripe.com/... failed, reason: read ECONNRESET" },
  "signals": {
    "fault": { "value": "upstream", "confidence": 0.93 }
  }
}
```

`value` is the answer, `confidence` is its probability (always on a yes/no, on choice and score when the model returns a distribution). Columns are written before the console line, so they show in the dev terminal, in platform logs such as Vercel, and in every [drain](https://www.evlog.dev/integrate/adapters/overview).

</steps>

## Then keep what sampling drops

Add `keep` to a signal and a `true` verdict forces the event past sampling. Six requests on a production rule, `info` kept at 0%: sampling keeps the two errors, and three of the four 200s come back because a signal said so:

<signals-judge>



</signals-judge>

This is the signal that rescued the silent checkout:

```typescript [server/signals.ts]
export const silentFailure = defineSignal({
  name: 'silent-failure',
  when: e => e.status === 200 && e.path === '/api/checkout',
  ask: 'Returned 200, but the customer did not get what they came for',
  keep: v => v.value && v.confidence > 0.8,
})
```

`keep` only promotes. It never drops an event a [sampling rule](https://www.evlog.dev/learn/sampling) would have kept, and the promoted event carries `kept: true` on the column so your counts stay honest.

## Where to next

<card-group>
<card icon="i-lucide-message-circle-question" title="What You Can Ask" to="https://www.evlog.dev/use-cases/signals/ask">

Yes/no, pick one, or a rubric. Which shape gives you which column, and how to write a question the model can actually answer.

</card>

<card icon="i-lucide-coins" title="What It Costs" to="https://www.evlog.dev/use-cases/signals/cost">

Price per event, the rate limit that binds first, latency on the keep path, and what leaves your process.

</card>

<card icon="i-lucide-book-open" title="Reference" to="https://www.evlog.dev/use-cases/signals/reference">

Every option of `defineSignal` and `createSignals`, the column format, `stats()`, and the test helpers, on one page.

</card>

<card icon="i-lucide-lightbulb" title="Recipes" to="https://www.evlog.dev/use-cases/signals/recipes">

Three signals most apps want on day one, and nine more to copy from.

</card>
</card-group>

---

- [What You Can Ask](https://www.evlog.dev/use-cases/signals/ask)
- [What It Costs](https://www.evlog.dev/use-cases/signals/cost)
- [Reference](https://www.evlog.dev/use-cases/signals/reference)


## Sitemap

See the full [sitemap](https://www.evlog.dev/sitemap.md) for all pages.
