# What Is a Signal in Magnitude's event‑core and How Does It Differ from an Event?

> Discover the distinction between signals and events in Magnitude's event-core. Learn how signals are ephemeral streams and events are durable facts for state management.

- Repository: [Magnitude/magnitude](https://github.com/magnitudedev/magnitude)
- Tags: deep-dive
- Published: 2026-09-05

---

**A signal in Magnitude's event‑core is a strongly typed, ephemeral PubSub stream emitted exclusively by projections to communicate derived state changes, whereas events are durable domain facts that implement `BaseEvent` and persist to the `EventSink` for replay and hydration.**

In the `magnitudedev/magnitude` repository, the **event‑core** package provides the foundational messaging infrastructure for building event-sourced systems. Understanding the distinction between **signals** and **events** is critical for designing projections that communicate efficiently without polluting the persistent event log.

## Core Architectural Concepts

Magnitude's event‑core distinguishes between two fundamental notification types that serve different purposes in the event-sourced lifecycle.

### What Defines an Event in event‑core

An **event** is an object that implements `BaseEvent` and contains a mandatory `type` string. Created by application code and published to the **EventBusCore**, events represent domain facts that must survive system restarts. By default, events are persisted to the `EventSink` unless explicitly marked with `ephemeral: true`, making them replay‑able during hydration. Events can be emitted from anywhere in the system—whether from UI components, external services, background workers, or even inside projection logic.

### What Defines a Signal in event‑core

A **signal** begins as a `SignalDef<T>`—a lightweight descriptor containing only a name and a phantom type field defined in [`packages/event-core/src/signal/define.ts`](https://github.com/magnitudedev/magnitude/blob/main/packages/event-core/src/signal/define.ts). When a projection is instantiated, this definition becomes a concrete `Signal<T, TSourceState>` via the internal `fromDef` function. Unlike events, signals are never persisted; they exist solely as ephemeral derived notifications for the current logical execution. Signals are emitted only by projections (via the `emit` helper) and are consumed through a `PubSub` mechanism managed by the projection bus.

## Key Differences Between Signals and Events

The architectural separation between these notification types affects persistence, origin, and delivery semantics.

- **Persistence**: Events default to durable storage in the `EventSink` unless flagged as `ephemeral`. Signals are inherently transient and are never written to storage; they vanish after the current execution context completes.

- **Origin**: Events can be published from any layer of the application using `EventBusCore.publish()`. Signals are strictly confined to projection handlers and are emitted using the `emit(signal, value)` helper provided in [`packages/event-core/src/signal/define.ts`](https://github.com/magnitudedev/magnitude/blob/main/packages/event-core/src/signal/define.ts).

- **Purpose**: Events record indisputable domain facts (e.g., `UserCreated`, `OrderPlaced`) that drive state changes across the system. Signals communicate derived, temporary information between projections or to workers (e.g., `stateChanged`, `counterTick`) without contaminating the event log.

- **Delivery Mechanism**: Events flow through `EventBusCore` → `ProjectionBus` → (optional) `EventSink` → PubSub broadcast. Signals are buffered during the *signal‑flush* phase of `ProjectionBus` and delivered synchronously only after all event handlers for the triggering event have completed.

- **Type Safety**: Events use a simple union type (`BaseEvent`) with an optional flag. Signals leverage strong generics via `Signal<T, TSourceState>`, where `T` represents the value type inferred from the definition, and `TSourceState` attaches the source projection's state type for enhanced type safety.

## How Signals Work Under the Hood

Understanding the internal implementation of signals reveals why they provide efficient, loss‑tight communication between projections.

### Signal Definition and Creation

In [`packages/event-core/src/signal/define.ts`](https://github.com/magnitudedev/magnitude/blob/main/packages/event-core/src/signal/define.ts), signals originate as `SignalDef<T>` objects created via the `create` factory function:

```typescript
export const tick = create<number>('counter.tick')

```

When a projection is defined via `Projection.define`, each `SignalDef` in the `signals` configuration array transforms into a `Signal<T, TSourceState>` instance. This concrete `Signal` class holds an `Effect.Context.Tag` that points to a `PubSub` specialized for that signal's type, enabling type‑safe publication and subscription.

### The Signal Emission Flow

Inside a projection handler, calling `emit(CounterSignals.tick, newValue)` returns an `Effect` that publishes the value to the signal's `PubSub`. However, the framework does not immediately broadcast these values. Instead, `ProjectionBus` queues all signal emissions during event processing and executes a *signal‑flush* phase only after all handlers for the current event have finished, as implemented in [`packages/event-core/src/core/projection-bus.ts`](https://github.com/magnitudedev/magnitude/blob/main/packages/event-core/src/core/projection-bus.ts). This buffering ensures that downstream consumers receive signals only after the source projection's state has been fully synchronized.

### Consuming Signals

Workers and other projections consume signals through the `stream` helper or higher‑level `ProjectionBus` APIs. Because signals travel through `PubSub` rather than the persistent event store, subscribers receive values instantly without I/O overhead. Workers typically subscribe using the `onSignal` configuration:

```typescript
onSignal: {
  [CounterSignals.tick]: ({ value }) => {
    console.log('Received tick:', value)
  }
}

```

## Practical Example: Implementing a Counter with Signals

The following example demonstrates defining a signal, emitting it from a projection when state changes, and subscribing from a worker.

```typescript
// 1. Define the signal descriptor
import { create } from '@magnitudedev/event-core/signal'

export const CounterSignals = {
  tick: create<number>('counter.tick')
}

// 2. Emit the signal from a projection
import { emit } from '@magnitudedev/event-core/signal'
import { CounterSignals } from './counter-signals'

export const CounterProjection = define<{
  state: number
  signals: typeof CounterSignals
}>({
  name: 'counter',
  signals: CounterSignals,
  handlers: {
    on: {
      increment: ({ state }) => {
        const newValue = state + 1
        emit(CounterSignals.tick, newValue)
        return newValue
      }
    }
  }
})

// 3. Subscribe from a worker
import { CounterSignals } from './counter-signals'

export const CounterWorker = defineWorker({
  name: 'counter-worker',
  handlers: {
    onSignal: {
      [CounterSignals.tick]: ({ value }) => {
        console.log('Counter ticked to', value)
      }
    }
  }
})

```

In [`packages/event-core/src/projection/define.ts`](https://github.com/magnitudedev/magnitude/blob/main/packages/event-core/src/projection/define.ts), the projection's `signals` configuration binds the `SignalDef` to the projection context, while [`packages/event-core/src/worker/define.ts`](https://github.com/magnitudedev/magnitude/blob/main/packages/event-core/src/worker/define.ts) illustrates how workers listen via `onSignal` without needing to process the underlying events.

## Summary

- **Signals are ephemeral, events are durable**: Signals exist only for the current execution, while events persist to the `EventSink` for replay unless marked `ephemeral`.
- **Different type systems**: Signals use strongly typed `Signal<T, TSourceState>` via `SignalDef`, whereas events implement the `BaseEvent` interface with string `type` fields.
- **Emission restrictions**: Only projections can emit signals using the `emit` helper; events can be published from any system layer via `EventBusCore`.
- **Architectural flow**: Events traverse `EventBusCore` for persistence, while signals are buffered and flushed by `ProjectionBus` after event handlers complete.
- **Consumption model**: Signals leverage `PubSub` for instant, non‑persistent delivery, making them ideal for derived state communication between projections and workers.

## Frequently Asked Questions

### Can signals be persisted for replay like events?

No. According to the source implementation in [`packages/event-core/src/core/projection-bus.ts`](https://github.com/magnitudedev/magnitude/blob/main/packages/event-core/src/core/projection-bus.ts), signals are explicitly designed as ephemeral derived notifications that exist only for the current logical execution. Unlike events, which `EventBusCore` writes to the `EventSink` unless marked `ephemeral: true`, signals are never persisted and cannot be replayed during system hydration.

### Can any service emit a signal, or only projections?

Only projections can emit signals. The `emit` helper defined in [`packages/event-core/src/signal/define.ts`](https://github.com/magnitudedev/magnitude/blob/main/packages/event-core/src/signal/define.ts) is available exclusively within projection handler contexts. Events, conversely, can be published from anywhere—including UI components, services, workers, or within projection logic—using `EventBusCore.publish()`.

### How do I subscribe to a signal from a worker?

Workers subscribe to signals using the `onSignal` configuration object within `defineWorker`, mapping signal definitions to handler functions. Alternatively, you can use `Signal.stream(signal)` to access the underlying `PubSub` directly. Because signals are delivered via the `PubSub` mechanism implemented in [`packages/event-core/src/signal/define.ts`](https://github.com/magnitudedev/magnitude/blob/main/packages/event-core/src/signal/define.ts), subscribers receive values instantly without waiting for persistence operations.

### What happens if a projection emits multiple signals during one event handler?

All signal emissions during a single event processing cycle are buffered internally and flushed synchronously after all event handlers for that event have completed. This *signal‑flush* phase, orchestrated by `ProjectionBus` in [`packages/event-core/src/core/projection-bus.ts`](https://github.com/magnitudedev/magnitude/blob/main/packages/event-core/src/core/projection-bus.ts), ensures that signals are delivered atomically once the projection state has been fully updated, preventing race conditions in downstream consumers.