---
title: "OR-Flag (Observed-Remove Flag, enable-wins)"
url: "https://nsta1.github.io/Orleans.Lattice/docs/crdt/orflag.html"
source: "https://github.com/NSTA1/Orleans.Lattice/blob/release/9.9/docs/crdt/orflag.md"
documents: "Orleans.Lattice 9.9.0 (release line 9.9)"
built: "2026-10-04"
all-pages: "https://nsta1.github.io/Orleans.Lattice/llms.txt"
bundle: "https://nsta1.github.io/Orleans.Lattice/docs/crdt/llms-full.txt"
---
# OR-Flag (Observed-Remove Flag, enable-wins)

Part of the [CRDTs documentation](readme.md).

`tree.OrFlag(key)` -> `OrFlagAccessor`, merge mode `LatticeMergeMode.OrFlag`.

## Semantics

An **OR-Flag** is a single boolean presence bit - "enabled" or not - with
**enable-wins** convergence. It is the OR-Set idea reduced to a single implicit
element: the flag tracks *presence* rather than a payload, so you get OR-Set
semantics without carrying a set's element bytes.

Every `Enable` mints a fresh causal dot. `Disable` tombstones only the enable
dots it has **observed**. The flag is on whenever at least one enable dot
survives, so a `Disable` concurrent with an `Enable` it never saw leaves the flag
**enabled**.

Enabling a flag that is already on is safe to repeat: it does not make the
stored value grow. Turning the same flag on a million times costs no more space
than turning it on once.

Use it for: membership rows in a secondary index (a tag/key pair is present or
not), feature toggles, presence bits - where a concurrent enable should beat a
concurrent disable.

## Behaviour

Figure: A concurrent OR-Flag enable and disable: enable wins. An order diagram drawn as a chain of two states. At the bottom the flag is off, with no enable dots. Cluster A enables it, minting dot A1, which moves A up. Cluster B, at the same time, disables it, but it has observed no enable dots, so there is nothing to tombstone and B's state stays at the bottom. Merging lifts B to A's state: the flag is on, because B's disable never saw A1.

Both clusters hold enable A1: ENABLED. A disable cancels only the enable dots it has observed, and B's had observed none.

Disable tombstones only the enable dots it has observed, so a concurrent enable always survives: enable-wins.

```mermaid
sequenceDiagram
    participant A as Cluster A
    participant B as Cluster B
    A->>A: Enable (dot A1)
    B->>B: Disable (observed: none yet)
    A-->>B: merge ships enable dot A1
    B-->>A: merge ships disable tombstones
    Note over A,B: A1 was not observed by B's disable
    Note over A,B: converged = ENABLED (enable-wins)
```

## Example

```csharp verify
var beta = tree.OrFlag("tenant:5:beta");

// Cluster A turns the flag on.
await beta.EnableAsync("cluster-A", cancellationToken);

// Meanwhile cluster B turned it off before A's enable had reached it, so its
// disable observed no enable dots and cancelled nothing. Replication delivers
// B's flag; MergeAsync stands in for that here. (A DisableAsync on this cluster,
// after the enable, would observe A's dot and turn the flag off.)
var fromB = new OrFlag();
fromB.Disable();
await beta.MergeAsync(fromB, cancellationToken);

// The disable only cancels the enable dots it saw, so a concurrent enable
// survives - the flag converges ENABLED.
bool isOn = await beta.IsEnabledAsync(cancellationToken); // true
```

## Marking many flags at once

Enabling flags one at a time costs two round trips per key - a read to mint the
enable dot, then the apply. `EnableManyAsync` reads every current flag in one
batched call, mints all the deltas against that snapshot, and applies them through
a single batched CRDT write, so a presence- or membership-marking pass costs one
read and one write per leaf rather than two round trips per key.

The snapshot is treated as possibly incomplete. A batched read reports an absent
key by omission, so a row it did not return is indistinguishable from a row that
does not exist, and deriving the dot counter from the snapshot alone would mint
counter 1 for both. Because OR-Flag cancellation is coverage-based, such a dot is
cancelled outright by any tombstone the unread row already carries - the write
would report success while the flag stayed off, permanently, since the next
attempt would repeat the read and mint the same dead dot. The batched helpers
therefore mint each dot from a strictly increasing source instead. A dot only has
to be unused for its replica, never dense, so this costs nothing semantically and
makes an enable impossible to cancel by a tombstone authored before it.

Pass the same non-empty replica identity you would give `EnableAsync`; an empty
one is rejected, because writers sharing a blank identity could cancel each
other's enables.

```csharp verify
// Mark a whole batch of feature flags on in one write.
string[] keys = ["tenant:5:beta", "tenant:6:beta", "tenant:7:beta"];
await tree.EnableManyAsync(keys, "cluster-A", cancellationToken);

// Enabling is idempotent under OR-Flag merge, so a retried batch converges
// rather than clobbering a concurrent writer.
await tree.EnableManyAsync(keys, "cluster-A", cancellationToken);
```

The batch is **not atomic**: a partial failure leaves it half-applied. When the
marks must land all-or-nothing, stage them instead with `StageEnableManyAsync`,
which mints every delta the same way from one batched read, and hand the tokens
to the cross-tree atomic builder:

```csharp verify
IReadOnlyList<LatticeStagedCrdtWrite> staged =
    await tree.StageEnableManyAsync(["tenant:8:beta", "tenant:9:beta"], "cluster-A", cancellationToken);
```

See also: its remove-wins mirror [RW-Flag](rwflag.md), the fuller
[OR-Set](orset.md), and the [CRDT overview](readme.md).
