# Cluster reference: ids, errors, metrics, workflows

This page is a scannable reference for the smaller cluster modules that don't
warrant their own page: the **identifier** types that name entities and runners,
the **snowflake** ids the cluster uses for time-ordered request ids, the typed
**errors** cluster operations fail with, the **metrics** the runtime emits, and
the cluster-backed **workflow engine** that runs durable workflows on top of
sharding. Authoring entities lives on the [Entities](https://effect.plants.sh/cluster/entities/) page;
this page covers the building blocks around them.
**Unstable:** Everything here lives under `effect/unstable/cluster/*` and may change between
  minor releases.

## Identifiers and addresses

Cluster routing identity is built from a few branded values. Each `make` brands
a trusted value without validation; each module also exports a `Schema` of the
same name for decoding/encoding at boundaries.

### EntityId

A branded string naming **one entity instance** within an entity type. Routing
hashes the exact string, so choose stable, deterministic values (not display
names or emails).

```ts
import * as EntityId from "effect/unstable/cluster/EntityId"

const id = EntityId.make("user-42")
// => "user-42" (branded EntityId)

// Decode/validate at a boundary with the schema:
import { Schema } from "effect"
Schema.decodeUnknownSync(EntityId.EntityId)("user-42")
// => "user-42"
```

### EntityType

A branded string naming a **kind** of entity (the family), distinct from a
specific instance. Combined with an `EntityId` to address an entity.

```ts
import * as EntityType from "effect/unstable/cluster/EntityType"

const type = EntityType.make("User")
// => "User" (branded EntityType)
```

### EntityAddress

A `Schema.Class` combining `entityType`, `entityId`, and `shardId` — the full
routing target for an entity. This is what you receive from
`Entity.CurrentAddress` inside an entity handler. Equality and hashing include
all three fields, and `toString` formats them.

```ts
import * as EntityAddress from "effect/unstable/cluster/EntityAddress"
import * as EntityType from "effect/unstable/cluster/EntityType"
import * as EntityId from "effect/unstable/cluster/EntityId"
import * as ShardId from "effect/unstable/cluster/ShardId"

const address = EntityAddress.make({
  entityType: EntityType.make("User"),
  entityId: EntityId.make("user-42"),
  shardId: ShardId.make("default", 7)
})

address.entityId // => "user-42"
String(address)  // => "EntityAddress(User, user-42, default:7)"
```

Inside an entity handler you read the current address from context rather than
constructing it yourself:

```ts
import { Effect } from "effect"
import * as Entity from "effect/unstable/cluster/Entity"

const handler = Effect.gen(function* () {
  const address = yield* Entity.CurrentAddress
  yield* Effect.log(`handling ${address.entityId}`)
})
```
**Note:** `EntityAddress.make` does not choose the shard for you — pass the `ShardId`
  produced by the entity's shard-group logic (e.g. `Sharding.getShardId`). A
  different `shardId` is a different address even for the same type and id.

### MachineId

A branded `Int` identifying a runner. Its main job is to be the machine
component baked into [snowflake ids](#snowflake-ids); concurrent generators must
use **distinct** machine ids to stay unique.

```ts
import * as MachineId from "effect/unstable/cluster/MachineId"

const machine = MachineId.make(3)
// => 3 (branded MachineId)
```

Snowflakes encode the machine component in 10 bits, so only the value **modulo
1024** is represented in an id.

## Snowflake ids

A `Snowflake` is a branded `bigint`: a runner-unique, time-ordered id packed
from a millisecond timestamp, a machine id, and a per-machine sequence number.
The custom epoch is **2025-01-01 UTC**, the machine id occupies 10 bits, and the
sequence occupies 12 bits (4096 ids per millisecond per machine). The cluster
uses snowflakes for request/envelope ids; you mostly obtain them via
[`Sharding.getSnowflake`](https://effect.plants.sh/cluster/entities/), but the module is fully usable on
its own.

```ts
import * as Snowflake from "effect/unstable/cluster/Snowflake"
import * as MachineId from "effect/unstable/cluster/MachineId"

// Pack parts into an id, then decompose it back:
const id = Snowflake.make({
  timestamp: Date.UTC(2025, 5, 1),
  machineId: MachineId.make(3),
  sequence: 0
})
Snowflake.toParts(id)
// => { timestamp: 1748736000000, machineId: 3, sequence: 0 }
```

### Snowflake (constructor) and Snowflake (type)

`Snowflake(input)` brands a `bigint`, or a `bigint`-compatible string, as a
`Snowflake`. The `Snowflake` type is `Brand.Branded<bigint, TypeId>`.

```ts
import * as Snowflake from "effect/unstable/cluster/Snowflake"

Snowflake.Snowflake(123n)
// => 123n (branded Snowflake)
Snowflake.Snowflake("123")
// => 123n (branded Snowflake)
```

### Snowflake.make

Packs known `timestamp`, `machineId`, and `sequence` parts into a branded id.
Machine id is encoded modulo 1024 and sequence modulo 4096 — out-of-range values
**wrap** rather than error.

```ts
import * as Snowflake from "effect/unstable/cluster/Snowflake"
import * as MachineId from "effect/unstable/cluster/MachineId"

const id = Snowflake.make({
  timestamp: Date.UTC(2025, 0, 1, 0, 0, 1), // 1s after the epoch
  machineId: MachineId.make(3),
  sequence: 5
})
// => a branded bigint encoding (timestamp, machineId=3, sequence=5)
```

### timestamp / dateTime / machineId / sequence / toParts

Accessors that decode a snowflake back into its components. `toParts` returns all
three at once.

```ts
import * as Snowflake from "effect/unstable/cluster/Snowflake"
import * as MachineId from "effect/unstable/cluster/MachineId"

const id = Snowflake.make({
  timestamp: Date.UTC(2025, 0, 1, 0, 0, 1),
  machineId: MachineId.make(3),
  sequence: 5
})

Snowflake.timestamp(id) // => 1735689601000  (Unix ms)
Snowflake.dateTime(id)  // => DateTime.Utc(2025-01-01T00:00:01.000Z)
Snowflake.machineId(id) // => 3
Snowflake.sequence(id)  // => 5
Snowflake.toParts(id)
// => { timestamp: 1735689601000, machineId: 3, sequence: 5 }
```

### SnowflakeFromBigInt / SnowflakeFromString

Schemas for crossing transport/storage boundaries. `SnowflakeFromBigInt` brands
a `bigint`; `SnowflakeFromString` decodes from a string and encodes back to one
— show both directions.

```ts
import { Schema } from "effect"
import * as Snowflake from "effect/unstable/cluster/Snowflake"

// bigint <-> branded bigint
Schema.decodeSync(Snowflake.SnowflakeFromBigInt)(123n)
// => 123n (branded)

// string -> branded bigint, and back to string
const id = Schema.decodeSync(Snowflake.SnowflakeFromString)("123")
// => 123n (branded)
Schema.encodeSync(Snowflake.SnowflakeFromString)(id)
// => "123"
```

### Generator / makeGenerator / layerGenerator

`Snowflake.Generator` is a `Context.Service` wrapping a stateful generator with a
synchronous `nextUnsafe()` and an effectful `setMachineId`. `makeGenerator`
builds one (using `Clock`), and `layerGenerator` provides the service. The
generator starts with a random machine id, never moves time backward across
clock drift, resets the sequence each millisecond, and rolls into the next
millisecond if more than 4096 ids are requested.

```ts
import { Effect } from "effect"
import * as Snowflake from "effect/unstable/cluster/Snowflake"
import * as MachineId from "effect/unstable/cluster/MachineId"

const program = Effect.gen(function* () {
  const gen = yield* Snowflake.Generator
  yield* gen.setMachineId(MachineId.make(3))
  const a = gen.nextUnsafe()
  const b = gen.nextUnsafe()
  // a < b  (ids are monotonically increasing)
  return [a, b]
}).pipe(Effect.provide(Snowflake.layerGenerator))
```

### constEpochMillis

The custom snowflake epoch in Unix milliseconds.

```ts
import * as Snowflake from "effect/unstable/cluster/Snowflake"

Snowflake.constEpochMillis
// => 1735689600000  (Date.UTC(2025, 0, 1))
```

## Cluster errors

Cluster operations fail with **typed, schema-backed error classes** so callers
can distinguish routing, membership, serialization, persistence, and mailbox
failures. Each has a stable `_tag`, so you catch them with `Effect.catchTag` /
`Effect.catchTags`, and most expose a static `is` guard.

Only a few of these surface from **client calls** — `MailboxFull`,
`AlreadyProcessingMessage`, and `PersistenceError`. The rest
(`EntityNotAssignedToRunner`, `RunnerNotRegistered`, `RunnerUnavailable`) arise
during internal routing and are typically retried by higher-level cluster logic
because ownership and health can change while a message is in flight.

### EntityNotAssignedToRunner

A runner received a message for an entity it does not own. Carries the offending
`address`. Usually transient during rebalancing and retried internally.

```ts
// _tag: "EntityNotAssignedToRunner", address: EntityAddress
```

### MalformedMessage

A message failed at a schema encode/decode (serialization) boundary — not an
entity handler failure. Carries the underlying `cause`. `MalformedMessage.refail`
maps an effect's failures into this error.

```ts
import { Effect } from "effect"
import { MalformedMessage } from "effect/unstable/cluster/ClusterError"

const decoded = MalformedMessage.refail(
  Effect.fail("bad payload")
)
// fails with: new MalformedMessage({ cause: "bad payload" })
```

### PersistenceError

A message failed to be written to / read from the cluster's mailbox storage.
Carries the `cause`. `PersistenceError.refail` wraps a squashed cause from any
effect.

```ts
// _tag: "PersistenceError", cause: Defect
```

### RunnerNotRegistered

A target runner is not registered with the shard manager. Carries its
`RunnerAddress`. Internal routing/membership failure.

```ts
// _tag: "RunnerNotRegistered", address: RunnerAddress
```

### RunnerUnavailable

A target runner is unresponsive. Carries its `RunnerAddress`. Often retryable as
the failure detector and membership view update.

```ts
// _tag: "RunnerUnavailable", address: RunnerAddress
```

### MailboxFull

A bounded entity mailbox is at capacity. Carries the `address`. Volatile requests
fail immediately with this; persisted/durable messages are retried or resumed
from storage instead.

```ts
// _tag: "MailboxFull", address: EntityAddress
```

### AlreadyProcessingMessage

The same request envelope is already in flight — per-envelope duplicate
protection, not a general entity lock. Carries `address` and `envelopeId` (a
`SnowflakeFromString`).

```ts
// _tag: "AlreadyProcessingMessage", address: EntityAddress, envelopeId: Snowflake
```

### Catching errors from a client call

When you send to an entity client, handle the errors that surface at that
boundary with `Effect.catchTags`:

```ts
import { Effect } from "effect"

declare const sendToEntity: Effect.Effect<
  string,
  | import("effect/unstable/cluster/ClusterError").MailboxFull
  | import("effect/unstable/cluster/ClusterError").AlreadyProcessingMessage
  | import("effect/unstable/cluster/ClusterError").PersistenceError
>

const program = sendToEntity.pipe(
  Effect.catchTags({
    // back off and retry when the mailbox is saturated
    MailboxFull: (e) => Effect.logWarning(`mailbox full: ${e.address}`).pipe(Effect.as("dropped")),
    // the envelope is already being handled — treat as idempotent success
    AlreadyProcessingMessage: () => Effect.succeed("duplicate"),
    // storage problem — surface for supervision
    PersistenceError: (e) => Effect.failCause(e.cause as any)
  })
)
```

## Metrics

`ClusterMetrics` exports standard gauges describing the shape and health of a
running cluster. They update automatically as the runtime registers entities,
acquires shards, and tracks runner membership — you do not record them yourself.
See [Metrics](https://effect.plants.sh/observability/metrics/) for reading and exporting gauges.

| Export | Metric name | Measures |
| --- | --- | --- |
| `entities` | `effect_cluster_entities` | Active entity instances on the current runner (tagged by entity type) |
| `singletons` | `effect_cluster_singletons` | Singleton processes running on the current runner |
| `runners` | `effect_cluster_runners` | Registered runners known to the cluster |
| `runnersHealthy` | `effect_cluster_runners_healthy` | Registered runners currently considered healthy |
| `shards` | `effect_cluster_shards` | Shards currently acquired by the current runner |

All are bigint gauges. `entities`, `singletons`, and `shards` are **runner-local**
(aggregate per-runner in dashboards); `runners` and `runnersHealthy` reflect the
cluster-wide view, which can lag briefly during membership changes.

### Reading a gauge value

Gauges are ordinary `Metric` values, so you can read the current snapshot with
`Metric.value`:

```ts
import { Effect, Metric } from "effect"
import * as ClusterMetrics from "effect/unstable/cluster/ClusterMetrics"

const program = Effect.gen(function* () {
  const state = yield* Metric.value(ClusterMetrics.shards)
  return state // => MetricState.Gauge { value: 7n }
})
```

## Cluster workflow engine

`ClusterWorkflowEngine` implements the workflow engine
(`WorkflowEngine.WorkflowEngine`) **on top of cluster entities and persisted
messages**, so durable workflows run distributed across the cluster: each
execution becomes a persisted entity, and executions, activities, deferred
completions, resumes, interrupts, and durable-clock wakeups are all coordinated
through persisted cluster entity messages. Workflow names and execution ids
determine the entity address used for persistence, so they must stay stable
across deploys.

You do not author workflows here — define them with the workflow module
(`Workflow`, `Activity`, `DurableClock`, `DurableDeferred`) and provide this
layer to make them durable and distributed.

### ClusterWorkflowEngine.layer

Provides `WorkflowEngine.WorkflowEngine` backed by the cluster. It **requires
`Sharding` and `MessageStorage`**, and also registers the internal durable-clock
entity used for workflow wakeups.

```ts
import { Layer } from "effect"
import * as ClusterWorkflowEngine from "effect/unstable/cluster/ClusterWorkflowEngine"

declare const ShardingLayer: Layer.Layer<
  import("effect/unstable/cluster/Sharding").Sharding
>
declare const MessageStorageLayer: Layer.Layer<
  import("effect/unstable/cluster/MessageStorage").MessageStorage
>

// Provide a distributed, durable WorkflowEngine to the rest of the app:
const WorkflowEngineLayer = ClusterWorkflowEngine.layer.pipe(
  Layer.provide([ShardingLayer, MessageStorageLayer])
)
```

### ClusterWorkflowEngine.make

The underlying constructor effect (`Effect.Effect<WorkflowEngine, never, Sharding | MessageStorage>`)
that `layer` wraps. Prefer `layer` in applications; reach for `make` only when
composing the engine into a custom layer.

```ts
import { Effect, Layer } from "effect"
import * as ClusterWorkflowEngine from "effect/unstable/cluster/ClusterWorkflowEngine"
import * as WorkflowEngine from "effect/unstable/workflow/WorkflowEngine"

const customLayer = Layer.effect(WorkflowEngine.WorkflowEngine)(
  ClusterWorkflowEngine.make
)
```
**Tip:** See the workflow module for authoring durable workflows (activities, durable
  clocks, deferreds). This page only covers wiring the cluster-backed engine.