Message annotations

Message annotations enable clients to add metadata to existing messages on a channel. You can use annotations to implement features like:

  • Message reactions - add emoji reactions (👍, ❤️, 😂) to messages
  • Content categorization - tag messages with categories such as "important" or "urgent"
  • Community moderation - flag inappropriate content for review
  • Read receipts - mark messages as "read" or "delivered"

When clients publish or delete an annotation, Ably automatically creates a summary that provides an aggregated view of all annotations for that message.

Enable annotations

Annotations can be enabled for a channel or channel namespace with the Message annotations, updates, and deletes channel rule.

  1. Go to the Settings tab of an app in your dashboard.
  2. Under channel rules, click Add new rule.
  3. Enter the channel name or channel namespace on which to enable message annotations.
  4. Check Message annotations, updates, and deletes to enable message annotations.
  5. Click Create channel rule to save.

Annotation types

Annotation types determine how annotations are processed and aggregated into summaries.

The annotation type is a string of the format namespace:summarization.version where:

  • namespace is a string (e.g. reactions) that groups related annotations. Only annotations in the same namespace will be aggregated together to produce summaries.
  • summarization specifies how annotations are aggregated to produce summaries, such as total, flag, distinct, unique, or multiple.
  • version specifies the version component which allows for future changes to summarization behavior.

Total

The total.v1 summarization method counts the number of annotations of a given type that were published for a message.

Deleting an annotation decrements the total count for that message.

Using total.v1 does not attribute counts to individual clients in the summary; it maintains only a simple count per annotation type and does not organize counts by name. Unidentified clients can publish total.v1 annotations. Use the identified channel rule if you want to prevent unidentified clients from publishing annotations.

If the same client publishes an annotation of a given type to the same message twice, the total count is incremented twice.

JSON

1

2

3

4

5

{
  "metrics:total.v1": {
    "total": 42
  }
}

Flag

The flag.v1 summarization method counts how many distinct clients have published an annotation of a given type and maintains a list of those clientIds. Clients must be identified to publish flag.v1 annotations.

Deleting an annotation decrements the total count for that message and removes the clientId from the list of clients that contributed to the summary.

A given client can contribute to the summary only once per annotation type.

JSON

1

2

3

4

5

6

7

{
  "reactions:flag.v1": {
    "total": 3,
    "clientIds": ["client1", "client2", "client3"],
    "clipped": false
  }
}

Distinct

The distinct.v1 summarization method counts how many unique clients have published an annotation with a given name for each annotation type along with the corresponding list of clientIds that published it. Clients must be identified to publish distinct.v1 annotations.

A given client can contribute to the summary for a particular annotation name only once, but the same client may publish additional annotations with different names.

Deleting an annotation removes the clientId from the list of clients that contributed to the summary for that name, and decrements the total count for that name.

JSON

1

2

3

4

5

6

7

8

9

10

11

12

13

14

{
  "categories:distinct.v1": {
    "important": {
      "total": 2,
      "clientIds": ["client1", "client3"],
      "clipped": false
    },
    "urgent": {
      "total": 3,
      "clientIds": ["client1", "client2", "client3"],
      "clipped": false
    }
  }
}

Unique

The unique.v1 summarization method counts how many unique clients have published an annotation with a given name for each annotation type, while guaranteeing that each client contributes to the summary for only one name at a time. The summary for each annotation name holds a total count of the number of distinct clients that have published an annotation with that name along with the corresponding list of clientIds that published it. Clients must be identified to publish unique.v1 annotations.

A given client can contribute to the summary for a particular annotation name only once. Publishing an annotation with a different name automatically removes that client from the summary for the previous name and adds them to the new one, updating the affected total values and list of clientIds.

Deleting an annotation removes the clientId from the list of clients that contributed to the summary for that name, and decrements the total count for that name.

JSON

1

2

3

4

5

6

7

8

9

10

11

12

13

14

{
  "status:unique.v1": {
    "important": {
      "total": 2,
      "clientIds": ["client1", "client3"],
      "clipped": false
    },
    "urgent": {
      "total": 1,
      "clientIds": ["client2"],
      "clipped": false
    }
  }
}

Multiple

The multiple.v1 summarization method counts both a total and a per-client count of the number of annotations that were published with a given name for each annotation type. Additionally it includes a count for the number of annotations published with a given name from unidentified clients. Use the identified channel rule if you want to prevent unidentified clients from publishing annotations.

A given client can contribute to the summary for a particular annotation name multiple times. The same client may also publish additional annotations with different names.

If a client specifies a count when publishing an annotation, the client's contribution to the summary is incremented by the specified value. If not, it defaults to incrementing by 1.

Deleting an annotation removes all contributions made by that clientId for that name.

JSON

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

{
  "voting:multiple.v1": {
    "option-a": {
      "total": 7,
      "clientCounts": {
        "client1": 3,
        "client2": 2
      },
      "totalUnidentified": 2,
      "clipped": false,
      "totalClientIds": 2
    },
    "option-b": {
      "total": 4,
      "clientCounts": {
        "client1": 2,
        "client3": 1
      },
      "totalUnidentified": 1,
      "clipped": false,
      "totalClientIds": 2
    }
  }
}

Publish annotations

To publish an annotation for a message, use the annotations.publish() method on a channel. Pass in either a message instance or the serial of the message to annotate. This method will publish an annotation with the action annotation.create.

Certain annotation summarization methods require a client to be identified for them to be able to publish an annotation. Their clientId will then be included in the associated published annotation.

Specify the annotation type using the type field of the annotation object.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

// Create an Ably Realtime client specifying the clientId that will
// be associated with annotations published by this client
const realtime = new Ably.Realtime({ key: 'demokey:*****', clientId: 'my-client-id' });

// Create a channel in a namespace called `annotations`
// which has message annotations enabled
const channel = realtime.channels.get('annotations:example');

// Publish an annotation for a message that flags it as delivered
await channel.annotations.publish(message, {
  type: 'receipts:flag.v1',
  name: 'delivered'
});

// You can also use a message's `serial`
await channel.annotations.publish(message.serial, {
  type: 'receipts:flag.v1',
  name: 'delivered'
});
API key:
DEMO ONLY

In the case of the distinct, unique, or multiple aggregation types, you should also specify a name. For these types, each different name will be aggregated separately in the annotation summary.

In the case of the multiple aggregation type, you should specify both a name and a count, by which to increment a client's contribution to the summary.

1

2

3

4

5

await channel.annotations.publish(message.serial, {
  type: 'rating:multiple.v1',
  name: 'stars',
  count: 4
});

You can additionally specify a data payload when publishing an annotation. This is not included in an annotation summary, so only readable by someone subscribing to individual annotation events.

Delete annotations

To delete an annotation, use the annotations.delete() method on a channel. Pass in either a message instance or the serial of the message to annotate. This method will publish an annotation message with an action of annotation.delete.

Deleting an annotation does not remove the original annotation that was published. Instead, they affect the annotation summary for that message by removing the contribution specified by the annotation.

The clientId specified in the client options will be associated with the published delete annotation.

Specify the annotation type using the type field of the annotation object, and optionally specify a name for the annotation. The name is used to aggregate certain annotations when producing an annotation summary.

1

2

3

4

5

6

7

8

9

10

11

12

13

// Create an Ably Realtime client specifying the clientId that will
// be associated with annotations published by this client
const realtime = new Ably.Realtime({ key: 'demokey:*****', clientId: 'my-client-id' });

// Create a channel in a namespace called `annotations`
// which has message annotations enabled
const channel = realtime.channels.get('annotations:example');

// Delete a 'delivered' annotation
await channel.annotations.delete(message.serial, {
  type: 'receipts:flag.v1',
  name: 'delivered'
});
API key:
DEMO ONLY

Subscribe to annotation summaries

The recommended way to receive annotation updates is through annotation summaries. These events provide a summary of the complete, current state of all annotations for a message whenever an annotation is published or deleted.

Annotation summaries are delivered to subscribers as messages with an action of message.summary, and a serial matching the serial of the message that they are updating. They have an annotations field which contains a summary of all the annotations for the message.

The value of that summary field is an object where the keys are the annotation types. The structure of the value of each key depends on the summarization method used, for example total.v1 will have a total field, while flag.v1 will have total and clientIds fields.

1

2

3

4

5

6

7

8

9

10

11

12

13

// Create an Ably Realtime client specifying the clientId that will
// be associated with annotations published by this client
const realtime = new Ably.Realtime({ key: 'demokey:*****', clientId: 'my-client-id' });

// Create a channel in a namespace called `annotations`
// which has message annotations enabled
const channel = realtime.channels.get('annotations:example');

await channel.subscribe((message) => {
  if (message.action === 'message.summary') {
    console.log(message.annotations.summary);
  }
});
API key:
DEMO ONLY

Annotation summaries

When annotations for a message are published, Ably automatically generates a summary that provides an aggregated view of all annotations for that message.

A separate summary is produced for each distinct annotation type. The summarization method specified in the annotation type determines how annotations in the same namespace for a given message are aggregated into a summary. A summary is constructed from the set of individual annotation events (annotation messages with an action of annotation.create or annotation.delete).

The summary will be included in a summary field nested within the message's annotations field, and is an object whose keys are the annotation types and whose values describe the annotation summary for that type. For example:

JSON

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

{
  "metrics:total.v1": {
    "total": 42
  },
  "reactions:flag.v1": {
    "total": 3,
    "clientIds": ["client1", "client2", "client3"],
    "clipped": false
  },
  "categories:distinct.v1": {
    "important": {
      "total": 2,
      "clientIds": ["client1", "client3"],
      "clipped": false
    },
    "urgent": {
      "total": 3,
      "clientIds": ["client1", "client2", "client3"],
      "clipped": false
    }
  },
  "status:unique.v1": {
    "important": {
      "total": 2,
      "clientIds": ["client1", "client3"],
      "clipped": false
    },
    "urgent": {
      "total": 1,
      "clientIds": ["client2"],
      "clipped": false
    }
  },
  "voting:multiple.v1": {
    "option-a": {
      "total": 7,
      "clientCounts": {
        "client1": 3,
        "client2": 2
      },
      "totalUnidentified": 2,
      "clipped": false,
      "totalClientIds": 2
    },
    "option-b": {
      "total": 4,
      "clientCounts": {
        "client1": 2,
        "client3": 1
      },
      "totalUnidentified": 1,
      "clipped": false,
      "totalClientIds": 2
    }
  }
}

Large summaries

If many clients publish the same annotation to the same message, the list of client IDs in that annotation summary will get clipped in order to keep the event size within the maximum message size.

When a summary is clipped:

  • The total property shows the total number of annotations as expected, but the clientIds property will contain only a partial list of client IDs.
  • The clipped property is set to true.
  • For the multiple annotation type, use the totalClientIds property to determine the total number of clients that have published the annotation. For the other annotation types this is equal to total.

Subscribe to individual annotation events

It is also possible to subscribe to individual annotation events, rather than annotation summaries. These are the emitted when publishing or deleting an annotation.

Individual events can be useful for activity feeds or detailed logging, but generally, for most usecases, subscribed clients should rely on aggregated summaries. The aggregation of annotations for a message into a summary attached to the message is the primary benefit of using the annotations API; an app design oriented around every client needing to subscribe to raw annotation events may not be taking full advantage of the feature.

If you need to, you can subscribe to individual annotation events using the annotations.subscribe() method on a channel. To subscribe to individual annotations, you must request the ANNOTATION_SUBSCRIBE mode.

Annotations delivered to the annotations.subscribe() listener will have an action of annotation.create or annotation.delete.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

// Create an Ably Realtime client specifying the clientId that will
// be associated with annotations published by this client
const realtime = new Ably.Realtime({ key: 'demokey:*****', clientId: 'my-client-id' });

// Create a channel in a namespace called `annotations`
// which has message annotations enabled.
// Specify the ANNOTATION_SUBSCRIBE mode to enable annotation subscriptions.
const channel = realtime.channels.get('annotations:example', { modes: ['ANNOTATION_SUBSCRIBE', ... /* all other modes you need, such as MESSAGE_SUBSCRIBE */] });

await channel.annotations.subscribe((annotation) => {
  if (annotation.action === 'annotation.create') {
    console.log(`New ${annotation.type} annotation with name ${annotation.name} from ${annotation.clientId}`);
  } else if (annotation.action === 'annotation.delete') {
    console.log(`${annotation.clientId} deleted a ${annotation.type} annotation with name ${annotation.name}`);
  }
});
API key:
DEMO ONLY

Annotation message properties

Annotations are a special type of message with the following properties:

PropertyDescription
idAn Ably-generated ID used to uniquely identify the annotation.
actionThe action specifies whether this is an annotation being added (annotation.create) or removed (annotation.delete).
serialThis annotation's unique serial (lexicographically totally ordered).
messageSerialThe serial of the message that this annotation is annotating.
typeThe annotation type.
nameThe name of the annotation, used by some annotation types for aggregation.
clientIdThe client identifier of the user that published this annotation.
countAn optional count, only relevant to certain annotation types.
dataAn optional payload for the annotation. Available on an individual annotation but not aggregated or included in annotation summaries.
encodingThis is typically empty, as all annotations received from Ably are automatically decoded client-side using this value. However, if the annotation encoding cannot be processed, this attribute contains the remaining transformations not applied to the data payload.
timestampThe timestamp of when the annotation was received by Ably, as milliseconds since the Unix epoch.
Select...